mirror of
https://github.com/wrabit/django-cotton.git
synced 2025-07-07 17:45:01 +00:00
start to prep new comps
This commit is contained in:
commit
cf1ec6cb6e
6 changed files with 106 additions and 67 deletions
35
README.md
35
README.md
|
@ -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 | ✅ | ✅ | ❌ |
|
||||
|
|
2
django_cotton/exceptions.py
Normal file
2
django_cotton/exceptions.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
class CottonIncompleteDynamicComponentError(Exception):
|
||||
pass
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'}\"")
|
||||
|
|
|
@ -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"]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue