start to prep new comps

This commit is contained in:
Will Abbott 2024-09-15 10:13:56 +01:00
commit cf1ec6cb6e
6 changed files with 106 additions and 67 deletions

View file

@ -479,21 +479,20 @@ For full docs and demos, checkout <a href="https://django-cotton.com" target="_b
**Note:** This comparison was created due to multiple requests, apologies for any mistakes or if I have missed something from other packages - please get in touch / create an issue!
| **Feature** | **Cotton** | **django-components** | **Slippers** | **Django Template Partials** |
|-----------------------------------------------------------------------------------------------------|----------------------------|----------------------------------------|------------------------------------------------------------|---------------------------------------------------|
| **Intro** | UI-focused, expressive syntax | Holistic solution with backend logic | Enhances DTL for reusable components | Define and reuse inline HTML partials |
| **Definition of component** | An HTML template | A backend class with template | An HTML template | Inline specified partial |
| **Syntax Style** | HTML-like | Django Template Tags | Django Template Tags with custom tags | Django Template Tags |
| **Create component in one step?** | ✅ <br> (place in folder) | ❌ <br> (create additional class file) | ❌ <br> (need to register in YAML file or with function) | ✅ <br> (declare inline or load via include tag) |
| **Slots** <br> Pass HTML content between tags | ✅ | ✅ | ✅ | ❌ |
| **Named Slots** <br> Designate a slot in the component template | ✅ | ✅ | ✅ (using fragments) | ❌ |
| **Dynamic Components** <br> Dynamically render components based on a variable or expression | ✅ | ❌ | ❌ | ❌ |
| **Scoped Slots** <br> Reference component context in parent template | ❌ | ✅ | ❌ | ❌ |
| **Dynamic Attributes** <br> Pass string literals of basic Python types | ✅ | ❌ | ❌ | ❌ |
| **Boolean Attributes** <br> Pass valueless attributes as True | ✅ | ✅ | ❌ | ❌ |
| **Declare Variables in Component View** <br> Set defaults for UI states | ✅ | ❌ <br> (Use class properties) | ✅ | ❌ |
| **Implicit Attribute Passing** <br> Pass all defined attributes to an element | ✅ | ❌ | ✅ | ❌ |
| **Django Template Expressions in Attribute Values** <br> Use Django expressions in attribute values | ✅ | ❌ | ❌ | ❌ |
| **Auto-Registering Components** <br> Start using components without manual registration | ✅ | ❌ <br> (Create class with decorator) | ❌ <br> (Register in YAML file or with helper function) | ✅ |
| **Attribute Merging** <br> Replace existing attributes with component attributes | ✅ | ✅ | ❌ | ❌ |
| **Multi-line Component Tags** <br> Write component tags over multiple lines | ✅ | ❌ | ❌ | ❌ |
| **Feature** | **Cotton** | **django-components** | **Slippers** |
|-----------------------------------------------------------------------------------------------------|----------------------------|----------------------------------------|------------------------------------------------------------|
| **Intro** | UI-focused, expressive syntax | Holistic solution with backend logic | Enhances DTL for reusable components |
| **Definition of component** | An HTML template | A backend class with template | An HTML template |
| **Syntax Style** | HTML-like | Django Template Tags | Django Template Tags with custom tags |
| **One-step package install** | ✅ | ❌ | ❌ |
| **Create component in one step?** | ✅ <br> (place in folder) | ✅ <br> (Technically yes with single-file components) | ❌ <br> (need to register in YAML file or with function) |
| **Slots** <br> Pass HTML content between tags | ✅ | ✅ | ✅ |
| **Named Slots** <br> Designate a slot in the component template | ✅ | ✅ | ✅ (using fragments) |
| **Dynamic Components** <br> Dynamically render components based on a variable or expression | ✅ | ✅ | ❌ |
| **Scoped Slots** <br> Reference component context in parent template | ❌ | ✅ | ❌ |
| **Dynamic Attributes** <br> Pass string literals of basic Python types | ✅ | ❌ | ❌ |
| **Boolean Attributes** <br> Pass valueless attributes as True | ✅ | ✅ | ❌ |
| **Implicit Attribute Passing** <br> Pass all defined attributes to an element | ✅ | ❌ | ✅ |
| **Django Template Expressions in Attribute Values** <br> Use Django expressions in attribute values | ✅ | ❌ | ❌ |
| **Attribute Merging** <br> Replace existing attributes with component attributes | ✅ | ✅ | ❌ |
| **Multi-line Component Tags** <br> Write component tags over multiple lines | ✅ | ✅ | ❌ |

View file

@ -0,0 +1,2 @@
class CottonIncompleteDynamicComponentError(Exception):
pass

View file

@ -6,6 +6,8 @@ from django.template import Variable, TemplateSyntaxError, Context
from django.template.base import VariableDoesNotExist, Template
from django.utils.safestring import mark_safe
from django_cotton.utils import ensure_quoted
class UnprocessableDynamicAttr(Exception):
pass
@ -70,7 +72,9 @@ class Attrs(Mapping):
def __str__(self):
return mark_safe(
" ".join(
f'{k}="{v}"' for k, v in self._attrs.items() if k not in self._exclude_from_str
f"{k}={ensure_quoted(v)}"
for k, v in self._attrs.items()
if k not in self._exclude_from_str
)
)

View file

@ -1,4 +1,3 @@
import ast
import functools
from typing import Union, List
@ -9,23 +8,18 @@ from django.template.base import (
VariableDoesNotExist,
Node,
TemplateSyntaxError,
Template,
)
from django.template.loader import get_template
from django.utils.html import format_html_join
from django.utils.safestring import mark_safe
from django_cotton.exceptions import CottonIncompleteDynamicComponentError
from django_cotton.templatetags._context_models import Attrs, DynamicAttr, UnprocessableDynamicAttr
from django_cotton.utils import get_cotton_data
register = Library()
class CottonIncompleteDynamicComponentErrorV2(Exception):
pass
class UnprocessableValue:
def __init__(self, original_value):
self.original_value = original_value
class CottonComponentNode(Node):
@ -85,33 +79,6 @@ class CottonComponentNode(Node):
return output
def _process_dynamic_value(self, value, context):
"""Process a dynamic value, attempting to resolve it as a variable, template string, or literal."""
try:
# Try to resolve as a variable
return Variable(value).resolve(context)
except VariableDoesNotExist:
try:
# Try to parse as a template string
template = Template(
f"{{% with True as True and False as False and None as None %}}{value}{{% endwith %}}"
)
rendered_value = template.render(context)
# Check if the rendered value is different from the original
if rendered_value != value:
return rendered_value
else:
# If it's the same, move on to the next step
raise ValueError("Template rendering did not change the value")
except (TemplateSyntaxError, ValueError):
try:
# Try to parse as an AST literal
return ast.literal_eval(value)
except (ValueError, SyntaxError):
# Flag as unprocessable if none of the above worked
return UnprocessableValue(value)
def _get_cached_template(self, context, attrs):
cache = context.render_context.get(self)
if cache is None:
@ -133,7 +100,7 @@ class CottonComponentNode(Node):
"""Generate the path to the template for the given component name."""
if component_name == "component":
if is_ is None:
raise CottonIncompleteDynamicComponentErrorV2(
raise CottonIncompleteDynamicComponentError(
'Cotton error: "<c-component>" should be accompanied by an "is" attribute.'
)
component_name = is_
@ -148,13 +115,6 @@ class CottonComponentNode(Node):
return value[1:-1]
return value
def get_cotton_data(context):
if "cotton_data" not in context:
context["cotton_data"] = {"stack": [], "vars": {}}
return context["cotton_data"]
@register.tag("comp")
def cotton_component(parser, token):
bits = token.split_contents()[1:]
@ -291,3 +251,21 @@ def cotton_vars(parser, token):
parser.delete_first_token()
return CottonVarsNode(var_dict, empty_vars, nodelist)
@register.filter
def merge(attrs, args):
# attrs is expected to be a dictionary of existing attributes
# args is a string of additional attributes to merge, e.g., "class:extra-class"
for arg in args.split(","):
key, value = arg.split(":", 1)
if key in attrs:
attrs[key] = value + " " + attrs[key]
else:
attrs[key] = value
return format_html_join(" ", '{0}="{1}"', attrs.items())
@register.filter
def get_item(dictionary, key):
return dictionary.get(key)

View file

@ -521,3 +521,51 @@ class AttributeHandlingTests(CottonTestCase):
with self.settings(ROOT_URLCONF=self.url_conf()):
response = self.client.get("/view/")
self.assertTrue(response.status_code == 200)
def test_htmx_attribute_values_single_quote(self):
# tests for json-like values
self.create_template(
"cotton/htmx.html",
"""
<div {{ attrs }}><div>
""",
)
self.create_template(
"htmx_view.html",
"""
<c-htmx
hx-vals='{"id": "1"}'
/>
""",
"view/",
)
# Override URLconf
with self.settings(ROOT_URLCONF=self.url_conf()):
response = self.client.get("/view/")
self.assertContains(response, """'{"id": "1"}'""")
def test_htmx_attribute_values_double_quote(self):
# tests for json-like values
self.create_template(
"cotton/htmx2.html",
"""
<div {{ attrs }}><div>
""",
)
self.create_template(
"htmx_view2.html",
"""
<c-htmx2
hx-vals="{'id': '1'}"
/>
""",
"view/",
)
# Override URLconf
with self.settings(ROOT_URLCONF=self.url_conf()):
response = self.client.get("/view/")
self.assertContains(response, "\"{'id': '1'}\"")

View file

@ -12,7 +12,15 @@ def eval_string(value):
def ensure_quoted(value):
if isinstance(value, str) and value.startswith('"') and value.endswith('"'):
return value
else:
return f'"{value}"'
if isinstance(value, str):
if value.startswith('{"') and value.endswith("}"):
return f"'{value}'" # use single quotes for json-like strings
elif value.startswith('"') and value.endswith('"'):
return value # already quoted
return f'"{value}"' # default to double quotes
def get_cotton_data(context):
if "cotton_data" not in context:
context["cotton_data"] = {"stack": [], "vars": {}}
return context["cotton_data"]