mirror of
https://github.com/wrabit/django-cotton.git
synced 2025-08-31 11:57:20 +00:00
merge confs
This commit is contained in:
commit
37e49caba5
46 changed files with 965 additions and 288 deletions
72
README.md
72
README.md
|
@ -10,6 +10,7 @@
|
|||
Bringing component-based design to Django templates.
|
||||
|
||||
- Docs site + demos: <a href="https://django-cotton.com" target="_blank">django-cotton.com</a>
|
||||
- <a href="https://discord.gg/4x8ntQwHMe" target="_blank">Discord community</a> (new)
|
||||
|
||||
## Contents
|
||||
|
||||
|
@ -27,11 +28,13 @@ Bringing component-based design to Django templates.
|
|||
[In-component Variables with `<c-vars>`](#in-component-variables-with-c-vars)
|
||||
[HTMX Example](#an-example-with-htmx)
|
||||
[Limitations in Django that Cotton overcomes](#limitations-in-django-that-cotton-overcomes)
|
||||
[Configuration](#configuration)
|
||||
[Caching](#caching)
|
||||
[Version support](#support)
|
||||
[Version support](#version-support)
|
||||
[Changelog](#changelog)
|
||||
[Comparison with other packages](#comparison-with-other-packages)
|
||||
|
||||
<hr>
|
||||
|
||||
## Why Cotton?
|
||||
|
||||
|
@ -45,6 +48,8 @@ Cotton aims to overcome [certain limitations](#limitations-in-django-that-cotton
|
|||
- **Encapsulates UI:** Keep layout, design and interaction in one file (especially when paired with Tailwind and Alpine.js)
|
||||
- **Compliments HTMX:** Create smart components, reducing repetition and enhancing maintainability.
|
||||
|
||||
<hr>
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
|
@ -67,6 +72,8 @@ If you have previously specified a custom loader, you should perform [manual set
|
|||
- Component filenames use snake_case: `my_component.html`
|
||||
- Components are called using kebab-case prefixed by 'c-': `<c-my-component />`
|
||||
|
||||
<hr>
|
||||
|
||||
## Walkthrough
|
||||
|
||||
### Your first component
|
||||
|
@ -86,7 +93,7 @@ If you have previously specified a custom loader, you should perform [manual set
|
|||
|
||||
Everything provided between the opening and closing tag is provided to the component as `{{ slot }}`. It can contain any content, HTML or Django template expression.
|
||||
|
||||
### Add attributes
|
||||
### Adding attributes
|
||||
|
||||
```html
|
||||
<!-- cotton/button.html -->
|
||||
|
@ -129,7 +136,7 @@ Named slots are a powerful concept. They allow us to provide HTML to appear in o
|
|||
</c-button>
|
||||
```
|
||||
|
||||
Named slots can also contain any django native template logic:
|
||||
Named slots can also contain any html or django template expression:
|
||||
|
||||
```html
|
||||
<!-- in view -->
|
||||
|
@ -250,7 +257,7 @@ This benefits a number of use-cases, for example if you have a select component
|
|||
|
||||
Django templates adhere quite strictly to the MVC model and does not permit a lot of data manipulation in views. Fair enough, but what if we want to handle data for the purpose of UI state only? Having presentation related variables defined in the back is overkill and can quickly lead to higher maintenance cost and loses encapsulation of the component. Cotton allows you define in-component variables for the following reasons:
|
||||
|
||||
#### 1. Using `<c-vars>` for default attributes
|
||||
#### Using `<c-vars>` for default attributes
|
||||
|
||||
In this example we have a button component with a default "theme" but it can be overridden.
|
||||
|
||||
|
@ -286,7 +293,7 @@ Now we have a default theme for our button, but it is overridable:
|
|||
</a>
|
||||
```
|
||||
|
||||
#### 2. Using `<c-vars>` to govern `{{ attrs }}`
|
||||
#### Using `<c-vars>` to govern `{{ attrs }}`
|
||||
|
||||
Using `{{ attrs }}` to pass all attributes from parent scope onto an element in the component, you'll sometimes want to provide additional properties to the component which are not intended to be an attributes. In this case you can declare them in `<c-vars />` and it will prevent it from being in `{{ attrs }}`
|
||||
|
||||
|
@ -341,6 +348,8 @@ You can also provide a template expression, should the component be inside a sub
|
|||
{% endfor %}
|
||||
```
|
||||
|
||||
<hr>
|
||||
|
||||
### An example with HTMX
|
||||
|
||||
Cotton helps carve out re-usable components, here we show how to make a re-usable form, reducing code repetition and improving maintainability:
|
||||
|
@ -369,6 +378,8 @@ Cotton helps carve out re-usable components, here we show how to make a re-usabl
|
|||
</c-form>
|
||||
```
|
||||
|
||||
<hr>
|
||||
|
||||
## Limitations in Django that Cotton overcomes
|
||||
|
||||
Whilst you _can_ build frontends with Django’s native tags, there are a few things that hold us back when we want to apply modern practices:
|
||||
|
@ -394,25 +405,25 @@ In addition, Cotton enables you to navigate around some of the limitations with
|
|||
### HTML in attributes
|
||||
❌ **Django native:**
|
||||
```html
|
||||
{% my_component header="<h1>Header</h1>" %}
|
||||
{% my_header icon="<svg>...</svg>" %}
|
||||
```
|
||||
✅ **Cotton:**
|
||||
```html
|
||||
<c-my-component>
|
||||
<c-slot name="header">
|
||||
<h1>Header</h1>
|
||||
<c-my-header>
|
||||
<c-slot name="icon">
|
||||
<svg>...</svg>
|
||||
</c-slot>
|
||||
</c-my-component>
|
||||
</c-my-header>
|
||||
```
|
||||
|
||||
### Template expressions in attributes
|
||||
❌ **Django native:**
|
||||
```html
|
||||
{% my_component model="todos.{{ index }}.name" extra="{% get_extra %}" %}
|
||||
{% bio name="{{ first_name }} {{ last_name }}" extra="{% get_extra %}" %}
|
||||
```
|
||||
✅ **Cotton:**
|
||||
```html
|
||||
<c-my-component model="todos.{{ index }}.name" extra="{% get_extra %} />
|
||||
<c-bio name="{{ first_name }} {{ last_name }}" extra="{% get_extra %} />
|
||||
```
|
||||
|
||||
### Pass simple python types
|
||||
|
@ -420,11 +431,13 @@ In addition, Cotton enables you to navigate around some of the limitations with
|
|||
```html
|
||||
{% my_component default_options="['yes', 'no', 'maybe']" %}
|
||||
{% my_component config="{'open': True}" %}
|
||||
{% my_component enabled="True" %}
|
||||
```
|
||||
✅ **Cotton:**
|
||||
```html
|
||||
<c-my-component :default_options="['yes', 'no', 'maybe']" />
|
||||
<c-my-component :config="{'open': True}" />
|
||||
<c-my-component :enabled="True" />
|
||||
|
||||
(provides a List and Dict to component)
|
||||
```
|
||||
|
@ -456,24 +469,42 @@ In addition, Cotton enables you to navigate around some of the limitations with
|
|||
<c-component is="subfolder1.subfolder2.{{ component_name }}" />
|
||||
```
|
||||
|
||||
<hr>
|
||||
|
||||
## Configuration
|
||||
|
||||
`COTTON_DIR` (default: "cotton")
|
||||
|
||||
The directory where your components are stored.
|
||||
|
||||
`COTTON_BASE_DIR` (default: None)
|
||||
|
||||
If you use a project-level templates folder then you can set the path here. This is not needed if your project already has a `BASE_DIR` variable.
|
||||
|
||||
`COTTON_SNAKE_CASED_NAMES` (default: True)
|
||||
|
||||
Whether to search for component filenames in snake_case. If set to False, you can use kebab-cased / hyphenated filenames.
|
||||
|
||||
<hr>
|
||||
|
||||
## Caching
|
||||
|
||||
Cotton is optimal when used with Django's cached.Loader. If you use <a href="https://django-cotton.com/docs/quickstart">automatic configuration</a> then the cached loader will be automatically applied. This feature has room for improvement, some desirables are:
|
||||
Cotton is optimal when used with Django's cached.Loader. If you use <a href="https://django-cotton.com/docs/quickstart">automatic configuration</a> then the cached loader will be automatically applied.
|
||||
|
||||
- Integration with a cache backend to survive runtime restarts / deployments.
|
||||
- Cache warming
|
||||
|
||||
For full docs and demos, checkout <a href="https://django-cotton.com" target="_blank">django-cotton.com</a>
|
||||
<hr>
|
||||
|
||||
## Version Support
|
||||
|
||||
- Python >= 3.8
|
||||
- Django >4.2,<5.2
|
||||
|
||||
<hr>
|
||||
|
||||
## Changelog
|
||||
|
||||
[See releases](https://github.com/wrabit/django-cotton/releases)
|
||||
|
||||
<hr>
|
||||
|
||||
## Comparison with other packages
|
||||
|
||||
|
@ -497,5 +528,8 @@ For full docs and demos, checkout <a href="https://django-cotton.com" target="_b
|
|||
|
||||
**Notes:**
|
||||
|
||||
- Some features here can be resolved with 3rd party plugins, for example for expressions, you can use something like `django-expr` package. So the list focus on comparison of core feature of that library.
|
||||
- This comparison was created due to multiple requests
|
||||
- Some features here can be resolved with 3rd party plugins, for example for expressions, you can use something like `django-expr` package. So the list focuses on a comparison of core feature of that library.
|
||||
|
||||
<hr>
|
||||
|
||||
For full docs and demos, checkout <a href="https://django-cotton.com" target="_blank">django-cotton.com</a>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
button!
|
|
@ -14,6 +14,4 @@ def index_view(request):
|
|||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("", index_view),
|
||||
]
|
||||
urlpatterns = [path("", index_view)]
|
||||
|
|
|
@ -85,7 +85,7 @@ def main():
|
|||
configure_django()
|
||||
|
||||
runs = 5
|
||||
iterations = 5000
|
||||
iterations = 200
|
||||
|
||||
print(f"Running benchmarks with {runs} runs, {iterations} iterations each")
|
||||
|
||||
|
|
1
dev/example_project/templates/cotton/app2/sub.html
Normal file
1
dev/example_project/templates/cotton/app2/sub.html
Normal file
|
@ -0,0 +1 @@
|
|||
i'm sub in project root
|
1
dev/example_project/templates/cotton/project_root.html
Normal file
1
dev/example_project/templates/cotton/project_root.html
Normal file
|
@ -0,0 +1 @@
|
|||
I'm from project root templates!
|
|
@ -121,13 +121,26 @@ class CottonCompiler:
|
|||
return replacements
|
||||
|
||||
def process_c_vars(self, html: str) -> Tuple[str, str]:
|
||||
"""Extract c-vars content and remove c-vars tags from the html"""
|
||||
match = self.c_vars_pattern.search(html)
|
||||
"""
|
||||
Extract c-vars content and remove c-vars tags from the html.
|
||||
Raises ValueError if more than one c-vars tag is found.
|
||||
"""
|
||||
# Find all matches of c-vars tags
|
||||
matches = list(self.c_vars_pattern.finditer(html))
|
||||
|
||||
if len(matches) > 1:
|
||||
raise ValueError(
|
||||
"Multiple c-vars tags found in component template. Only one c-vars tag is allowed per template."
|
||||
)
|
||||
|
||||
# Process single c-vars tag if present
|
||||
match = matches[0] if matches else None
|
||||
if match:
|
||||
attrs = match.group(1)
|
||||
vars_content = f"{{% vars {attrs.strip()} %}}"
|
||||
html = self.c_vars_pattern.sub("", html) # Remove all c-vars tags
|
||||
return vars_content, html
|
||||
|
||||
return "", html
|
||||
|
||||
def process(self, html: str) -> str:
|
||||
|
|
|
@ -2,6 +2,7 @@ import hashlib
|
|||
import os
|
||||
from functools import lru_cache
|
||||
|
||||
from django.conf import settings
|
||||
from django.template.loaders.base import Loader as BaseLoader
|
||||
from django.core.exceptions import SuspiciousFileOperation
|
||||
from django.template import TemplateDoesNotExist, Origin
|
||||
|
@ -50,14 +51,25 @@ class Loader(BaseLoader):
|
|||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_dirs(self):
|
||||
"""This works like the file loader with APP_DIRS = True."""
|
||||
"""Retrieves possible locations of cotton directory"""
|
||||
dirs = list(self.dirs or self.engine.dirs)
|
||||
|
||||
# Include any included installed app directories, e.g. project/app1/templates
|
||||
for app_config in apps.get_app_configs():
|
||||
template_dir = os.path.join(app_config.path, "templates")
|
||||
if os.path.isdir(template_dir):
|
||||
dirs.append(template_dir)
|
||||
|
||||
# Check project root templates, e.g. project/templates
|
||||
base_dir = getattr(settings, "COTTON_BASE_DIR", None)
|
||||
if base_dir is None:
|
||||
base_dir = getattr(settings, "BASE_DIR", None)
|
||||
|
||||
if base_dir is not None:
|
||||
root_template_dir = os.path.join(base_dir, "templates")
|
||||
if os.path.isdir(root_template_dir):
|
||||
dirs.append(root_template_dir)
|
||||
|
||||
return dirs
|
||||
|
||||
def reset(self):
|
||||
|
|
|
@ -2,7 +2,7 @@ import functools
|
|||
from typing import Union
|
||||
|
||||
from django.conf import settings
|
||||
from django.template import Library
|
||||
from django.template import Library, TemplateDoesNotExist
|
||||
from django.template.base import (
|
||||
Node,
|
||||
)
|
||||
|
@ -40,6 +40,10 @@ class CottonComponentNode(Node):
|
|||
value = self._strip_quotes_safely(value)
|
||||
if value is True: # Boolean attribute
|
||||
component_data["attrs"][key] = True
|
||||
elif key.startswith("::"): # Escaping 1 colon e.g for shorthand alpine
|
||||
key = key[1:]
|
||||
# component_data["slots"][key] = value
|
||||
component_data["attrs"][key] = value
|
||||
elif key.startswith(":"):
|
||||
key = key[1:]
|
||||
try:
|
||||
|
@ -86,13 +90,30 @@ class CottonComponentNode(Node):
|
|||
|
||||
template_path = self._generate_component_template_path(self.component_name, attrs.get("is"))
|
||||
|
||||
if template_path not in cache:
|
||||
if template_path in cache:
|
||||
return cache[template_path]
|
||||
|
||||
# Try to get the primary template
|
||||
try:
|
||||
template = get_template(template_path)
|
||||
if hasattr(template, "template"):
|
||||
template = template.template
|
||||
cache[template_path] = template
|
||||
return template
|
||||
except TemplateDoesNotExist:
|
||||
# If the primary template doesn't exist, try the fallback path (index.html)
|
||||
fallback_path = template_path.rsplit(".html", 1)[0] + "/index.html"
|
||||
|
||||
return cache[template_path]
|
||||
# Check if the fallback template is already cached
|
||||
if fallback_path in cache:
|
||||
return cache[fallback_path]
|
||||
|
||||
# Try to get the fallback template
|
||||
template = get_template(fallback_path)
|
||||
if hasattr(template, "template"):
|
||||
template = template.template
|
||||
cache[fallback_path] = template
|
||||
return template
|
||||
|
||||
def _create_isolated_context(self, original_context, component_state):
|
||||
# Get the request object from the original context
|
||||
|
@ -131,7 +152,13 @@ class CottonComponentNode(Node):
|
|||
)
|
||||
component_name = is_
|
||||
|
||||
component_tpl_path = component_name.replace(".", "/").replace("-", "_")
|
||||
component_tpl_path = component_name.replace(".", "/")
|
||||
|
||||
# Cotton by default will look for snake_case version of comp names. This can be configured to allow hyphenated names.
|
||||
snaked_cased_named = getattr(settings, "COTTON_SNAKE_CASED_NAMES", True)
|
||||
if snaked_cased_named:
|
||||
component_tpl_path = component_tpl_path.replace("-", "_")
|
||||
|
||||
cotton_dir = getattr(settings, "COTTON_DIR", "cotton")
|
||||
return f"{cotton_dir}/{component_tpl_path}.html"
|
||||
|
||||
|
@ -143,25 +170,60 @@ class CottonComponentNode(Node):
|
|||
|
||||
|
||||
def cotton_component(parser, token):
|
||||
"""
|
||||
Parse a cotton component tag and return a CottonComponentNode.
|
||||
|
||||
It accepts spaces inside quoted attributes for example if we want to pass valid json that contains spaces in values.
|
||||
|
||||
@TODO Add support here for 'complex' attributes so we can eventually remove the need for the 'attr' tag. The idea
|
||||
here is to render `{{` and `{%` blocks in tags.
|
||||
"""
|
||||
|
||||
bits = token.split_contents()[1:]
|
||||
component_name = bits[0]
|
||||
attrs = {}
|
||||
only = False
|
||||
|
||||
node_class = CottonComponentNode
|
||||
current_key = None
|
||||
current_value = []
|
||||
|
||||
for bit in bits[1:]:
|
||||
if bit == "only":
|
||||
# if we see `only` we isolate context
|
||||
only = True
|
||||
continue
|
||||
try:
|
||||
key, value = bit.split("=")
|
||||
attrs[key] = value
|
||||
except ValueError:
|
||||
attrs[bit] = True
|
||||
|
||||
if "=" in bit:
|
||||
# If we were building a previous value, store it
|
||||
if current_key:
|
||||
attrs[current_key] = " ".join(current_value)
|
||||
current_value = []
|
||||
|
||||
# Start new key-value pair
|
||||
key, value = bit.split("=", 1)
|
||||
if value.startswith(("'", '"')):
|
||||
if value.endswith(("'", '"')) and value[0] == value[-1]:
|
||||
# Complete quoted value
|
||||
attrs[key] = value
|
||||
else:
|
||||
# Start of quoted value
|
||||
current_key = key
|
||||
current_value = [value]
|
||||
else:
|
||||
# Simple unquoted value
|
||||
attrs[key] = value
|
||||
else:
|
||||
if current_key:
|
||||
# Continue building quoted value
|
||||
current_value.append(bit)
|
||||
else:
|
||||
# Boolean attribute
|
||||
attrs[bit] = True
|
||||
|
||||
# Store any final value being built
|
||||
if current_key:
|
||||
attrs[current_key] = " ".join(current_value)
|
||||
|
||||
nodelist = parser.parse(("endc",))
|
||||
parser.delete_first_token()
|
||||
|
||||
return node_class(component_name, nodelist, attrs, only)
|
||||
return CottonComponentNode(component_name, nodelist, attrs, only)
|
||||
|
|
|
@ -35,11 +35,7 @@ class CottonVarsNode(Node):
|
|||
except UnprocessableDynamicAttr:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
resolved_value = Variable(value).resolve(context)
|
||||
except (VariableDoesNotExist, IndexError):
|
||||
resolved_value = value
|
||||
attrs[key] = resolved_value
|
||||
attrs[key] = value
|
||||
attrs.exclude_from_string_output(key)
|
||||
|
||||
# Process cvars without values
|
||||
|
|
|
@ -483,3 +483,52 @@ class AttributeHandlingTests(CottonTestCase):
|
|||
self.assertTrue(
|
||||
"in default slot: <strong {% if 1 == 2 %}hidden{% endif %}></strong>" in compiled
|
||||
)
|
||||
|
||||
def test_colon_escaping(self):
|
||||
self.create_template(
|
||||
"cotton/colon_escaping.html",
|
||||
"""
|
||||
attrs: '{{ attrs }}'
|
||||
""",
|
||||
)
|
||||
|
||||
self.create_template(
|
||||
"colon_escaping_view.html",
|
||||
"""
|
||||
<c-colon-escaping
|
||||
::string="variable"
|
||||
:dynamic="variable"
|
||||
::complex-string="{'something': 1}"
|
||||
:complex-dynamic="{'something': 1}"/>
|
||||
""",
|
||||
"view/",
|
||||
context={"variable": "Ive been resolved!"},
|
||||
)
|
||||
with self.settings(ROOT_URLCONF=self.url_conf()):
|
||||
response = self.client.get("/view/")
|
||||
|
||||
self.assertContains(
|
||||
response,
|
||||
"""attrs: ':string="variable" dynamic="Ive been resolved!" :complex-string="{'something': 1}" complex-dynamic="{'something': 1}"'""",
|
||||
)
|
||||
|
||||
def test_attributes_can_contain_valid_json(self):
|
||||
self.create_template(
|
||||
"cotton/json_attrs.html",
|
||||
"""
|
||||
<button {{ attrs }}>{{ slot }}</button>
|
||||
""",
|
||||
)
|
||||
|
||||
self.create_template(
|
||||
"json_attrs_view.html",
|
||||
"""
|
||||
<c-json-attrs data-something='{"key=dd": "the value with= spaces"}' />
|
||||
""",
|
||||
"view/",
|
||||
)
|
||||
|
||||
with self.settings(ROOT_URLCONF=self.url_conf()):
|
||||
response = self.client.get("/view/")
|
||||
|
||||
self.assertContains(response, """{"key=dd": "the value with= spaces"}""")
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
from django_cotton.tests.utils import CottonTestCase
|
||||
|
||||
|
||||
class BasicComponentTests(CottonTestCase):
|
||||
def test_component_is_rendered(self):
|
||||
self.create_template(
|
||||
"cotton/render.html",
|
||||
"""<div class="i-am-component">{{ slot }}</div>""",
|
||||
)
|
||||
|
||||
self.create_template(
|
||||
"view.html",
|
||||
"""<c-render>Hello, World!</c-render>""",
|
||||
"view/",
|
||||
)
|
||||
|
||||
# Override URLconf
|
||||
with self.settings(ROOT_URLCONF=self.url_conf()):
|
||||
response = self.client.get("/view/")
|
||||
self.assertContains(response, '<div class="i-am-component">')
|
||||
self.assertContains(response, "Hello, World!")
|
||||
|
||||
def test_nested_rendering(self):
|
||||
self.create_template(
|
||||
"cotton/parent.html",
|
||||
"""
|
||||
<div class="i-am-parent">
|
||||
{{ slot }}
|
||||
</div>
|
||||
""",
|
||||
)
|
||||
|
||||
self.create_template(
|
||||
"cotton/child.html",
|
||||
"""
|
||||
<div class="i-am-child"></div>
|
||||
""",
|
||||
)
|
||||
|
||||
self.create_template(
|
||||
"cotton/nested_render_view.html",
|
||||
"""
|
||||
<c-parent>
|
||||
<c-child>d</c-child>
|
||||
</c-parent>
|
||||
""",
|
||||
"view/",
|
||||
)
|
||||
|
||||
with self.settings(ROOT_URLCONF=self.url_conf()):
|
||||
response = self.client.get("/view/")
|
||||
self.assertContains(response, '<div class="i-am-parent">')
|
||||
self.assertContains(response, '<div class="i-am-child">')
|
||||
|
||||
def test_cotton_directory_can_be_configured(self):
|
||||
custom_dir = "components"
|
||||
|
||||
self.create_template(
|
||||
f"{custom_dir}/custom_directory.html",
|
||||
"""<div class="i-am-component">{{ slot }}</div>""",
|
||||
)
|
||||
|
||||
self.create_template(
|
||||
"custom_directory_view.html",
|
||||
"""<c-custom-directory>Hello, World!</c-custom-directory>""",
|
||||
"view/",
|
||||
)
|
||||
|
||||
# Override URLconf
|
||||
with self.settings(ROOT_URLCONF=self.url_conf(), COTTON_DIR=custom_dir):
|
||||
response = self.client.get("/view/")
|
||||
self.assertContains(response, '<div class="i-am-component">')
|
||||
self.assertContains(response, "Hello, World!")
|
||||
|
||||
def test_self_closing_is_rendered(self):
|
||||
self.create_template("cotton/self_closing.html", """I self closed!""")
|
||||
self.create_template(
|
||||
"self_closing_view.html",
|
||||
"""
|
||||
1: <c-self-closing/>
|
||||
2: <c-self-closing />
|
||||
3: <c-self-closing />
|
||||
""",
|
||||
"view/",
|
||||
)
|
||||
|
||||
with self.settings(ROOT_URLCONF=self.url_conf()):
|
||||
response = self.client.get("/view/")
|
||||
self.assertContains(response, "1: I self closed!")
|
||||
self.assertContains(response, "2: I self closed!")
|
||||
self.assertContains(response, "3: I self closed!")
|
||||
|
||||
def test_loader_scans_all_app_directories(self):
|
||||
self.create_template(
|
||||
"app_outside_of_dirs_view.html", """<c-app-outside-of-dirs />""", "view/"
|
||||
)
|
||||
|
||||
with self.settings(ROOT_URLCONF=self.url_conf()):
|
||||
response = self.client.get("/view/")
|
||||
|
||||
self.assertContains(
|
||||
response,
|
||||
"""My template path was not specified in settings!""",
|
||||
)
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
from django_cotton.tests.utils import CottonTestCase, get_compiled
|
||||
|
||||
|
||||
|
@ -70,3 +71,29 @@ class CompileTests(CottonTestCase):
|
|||
"{% cotton_verbatim %}" not in compiled,
|
||||
"Compilation should not leave {% cotton_verbatim %} tags in the output",
|
||||
)
|
||||
|
||||
def test_raises_error_on_duplicate_cvars(self):
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
get_compiled(
|
||||
"""
|
||||
<c-vars />
|
||||
<c-vars />
|
||||
"""
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
str(cm.exception),
|
||||
"Multiple c-vars tags found in component template. Only one c-vars tag is allowed per template.",
|
||||
)
|
||||
|
||||
def test_raises_on_slots_without_name(self):
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
get_compiled(
|
||||
"""
|
||||
<c-slot />
|
||||
"""
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
"c-slot tag must have a name attribute:" in str(cm.exception),
|
||||
)
|
||||
|
|
|
@ -75,7 +75,7 @@ class ContextIsolationTests(CottonTestCase):
|
|||
self.assertContains(response, "From view context scope: ''")
|
||||
self.assertContains(response, "Direct attribute: 'yes'")
|
||||
|
||||
@override_settings(COTTON_CONTEXT_ISOLATION_ENABLED=False)
|
||||
@override_settings(COTTON_ENABLE_CONTEXT_ISOLATION=False)
|
||||
def test_legacy_context_behaviour(self):
|
||||
"""Test components do not have isolated context"""
|
||||
self.create_template(
|
||||
|
|
|
@ -380,3 +380,39 @@ class CvarTests(CottonTestCase):
|
|||
rendered = render_to_string("cotton/direct_render.html")
|
||||
|
||||
self.assertTrue("I'm all set" in rendered)
|
||||
|
||||
def test_cvars_template_basic_types_parsing(self):
|
||||
self.create_template(
|
||||
"cotton/cvars_template_basic_types.html",
|
||||
"""
|
||||
<c-vars
|
||||
none="None"
|
||||
number="1"
|
||||
boolean_true="True"
|
||||
boolean_false="False" />
|
||||
|
||||
{% if none == "None" %}
|
||||
I am string None
|
||||
{% endif %}
|
||||
|
||||
{% if number == "1" %}
|
||||
I am string 1
|
||||
{% endif %}
|
||||
|
||||
{% if boolean_true == "True" %}
|
||||
I am string True
|
||||
{% endif %}
|
||||
|
||||
{% if boolean_false == "False" %}
|
||||
I am string False
|
||||
{% endif %}
|
||||
|
||||
""",
|
||||
)
|
||||
|
||||
rendered = render_to_string("cotton/cvars_template_basic_types.html")
|
||||
|
||||
self.assertTrue("I am string None" in rendered)
|
||||
self.assertTrue("I am string 1" in rendered)
|
||||
self.assertTrue("I am string True" in rendered)
|
||||
self.assertTrue("I am string False" in rendered)
|
||||
|
|
173
django_cotton/tests/test_misc.py
Normal file
173
django_cotton/tests/test_misc.py
Normal file
|
@ -0,0 +1,173 @@
|
|||
from django.test import override_settings
|
||||
|
||||
from django_cotton.tests.utils import CottonTestCase, get_rendered
|
||||
|
||||
|
||||
class MiscComponentTests(CottonTestCase):
|
||||
def test_cotton_directory_can_be_configured(self):
|
||||
custom_dir = "components"
|
||||
|
||||
self.create_template(
|
||||
f"{custom_dir}/custom_directory.html",
|
||||
"""<div class="i-am-component">{{ slot }}</div>""",
|
||||
)
|
||||
|
||||
self.create_template(
|
||||
"custom_directory_view.html",
|
||||
"""<c-custom-directory>Hello, World!</c-custom-directory>""",
|
||||
"view/",
|
||||
)
|
||||
|
||||
# Override URLconf
|
||||
with self.settings(ROOT_URLCONF=self.url_conf(), COTTON_DIR=custom_dir):
|
||||
response = self.client.get("/view/")
|
||||
self.assertContains(response, '<div class="i-am-component">')
|
||||
self.assertContains(response, "Hello, World!")
|
||||
|
||||
def test_loader_scans_all_app_directories(self):
|
||||
self.create_template(
|
||||
"app_outside_of_dirs_view.html", """<c-app-outside-of-dirs />""", "view/"
|
||||
)
|
||||
|
||||
with self.settings(ROOT_URLCONF=self.url_conf()):
|
||||
response = self.client.get("/view/")
|
||||
|
||||
self.assertContains(
|
||||
response,
|
||||
"""My template path was not specified in settings!""",
|
||||
)
|
||||
|
||||
def test_context_isolation_by_default(self):
|
||||
"""Context should be isolated by default, but still include context from custom and built in processors."""
|
||||
pass
|
||||
|
||||
def test_only_gives_isolated_context(self):
|
||||
self.create_template(
|
||||
"cotton/only.html",
|
||||
"""
|
||||
<a class="{{ class|default:"donttouch" }}">test</a>
|
||||
""",
|
||||
)
|
||||
self.create_template(
|
||||
"no_only_view.html",
|
||||
"""
|
||||
<c-only />
|
||||
""",
|
||||
"no_only_view/",
|
||||
context={"class": "herebedragons"}, # this should pass to `class` successfully
|
||||
)
|
||||
|
||||
with self.settings(ROOT_URLCONF=self.url_conf()):
|
||||
response = self.client.get("/no_only_view/")
|
||||
self.assertNotContains(response, "donttouch")
|
||||
self.assertContains(response, "herebedragons")
|
||||
|
||||
self.create_template(
|
||||
"only_view.html",
|
||||
"""
|
||||
<c-only only />
|
||||
""",
|
||||
"only_view/",
|
||||
context={
|
||||
"class": "herebedragons"
|
||||
}, # this should not pass to `class` due to `only` being present
|
||||
)
|
||||
|
||||
with self.settings(ROOT_URLCONF=self.url_conf()):
|
||||
response = self.client.get("/only_view/")
|
||||
self.assertNotContains(response, "herebedragons")
|
||||
self.assertContains(response, "donttouch")
|
||||
|
||||
self.create_template(
|
||||
"only_view2.html",
|
||||
"""
|
||||
<c-only class="october" only />
|
||||
""",
|
||||
"only_view2/",
|
||||
context={
|
||||
"class": "herebedragons"
|
||||
}, # this should not pass to `class` due to `only` being present
|
||||
)
|
||||
|
||||
with self.settings(ROOT_URLCONF=self.url_conf()):
|
||||
response = self.client.get("/only_view2/")
|
||||
self.assertNotContains(response, "herebedragons")
|
||||
self.assertNotContains(response, "donttouch")
|
||||
self.assertContains(response, "october")
|
||||
|
||||
def test_only_with_dynamic_components(self):
|
||||
self.create_template(
|
||||
"cotton/dynamic_only.html",
|
||||
"""
|
||||
From parent comp scope: '{{ class }}'
|
||||
From view context scope: '{{ view_item }}'
|
||||
Direct attribute: '{{ direct }}'
|
||||
""",
|
||||
)
|
||||
|
||||
self.create_template(
|
||||
"cotton/middle_component.html",
|
||||
"""
|
||||
<c-component is="{{ comp }}" only direct="yes" />
|
||||
""",
|
||||
)
|
||||
|
||||
self.create_template(
|
||||
"dynamic_only_view.html",
|
||||
"""<c-middle-component class="mb-5" />""",
|
||||
"view/",
|
||||
context={"comp": "dynamic_only", "view_item": "blee"},
|
||||
)
|
||||
|
||||
with self.settings(ROOT_URLCONF=self.url_conf()):
|
||||
response = self.client.get("/view/")
|
||||
self.assertContains(response, "From parent comp scope: ''")
|
||||
self.assertContains(response, "From view context scope: ''")
|
||||
self.assertContains(response, "Direct attribute: 'yes'")
|
||||
|
||||
@override_settings(COTTON_SNAKE_CASED_NAMES=False)
|
||||
def test_hyphen_naming_convention(self):
|
||||
self.create_template(
|
||||
"cotton/some-subfolder/hyphen-naming-convention.html",
|
||||
"I have a hyphenated component name",
|
||||
)
|
||||
|
||||
html = """
|
||||
<c-some-subfolder.hyphen-naming-convention />
|
||||
"""
|
||||
|
||||
rendered = get_rendered(html)
|
||||
|
||||
self.assertTrue("I have a hyphenated component name" in rendered)
|
||||
|
||||
def test_multiple_app_subdirectory_access(self):
|
||||
self.create_template(
|
||||
"cotton/app_dir.html",
|
||||
"I'm from app templates!",
|
||||
)
|
||||
|
||||
html = """
|
||||
<c-app-dir />
|
||||
<c-project-root />
|
||||
<c-app2.sub />
|
||||
"""
|
||||
|
||||
rendered = get_rendered(html)
|
||||
|
||||
self.assertTrue("I'm from app templates!" in rendered)
|
||||
self.assertTrue("I'm from project root templates!" in rendered)
|
||||
self.assertTrue("i'm sub in project root" in rendered)
|
||||
|
||||
def test_index_file_access(self):
|
||||
self.create_template(
|
||||
"cotton/accordion/index.html",
|
||||
"I'm an index file!",
|
||||
)
|
||||
|
||||
html = """
|
||||
<c-accordion />
|
||||
"""
|
||||
|
||||
rendered = get_rendered(html)
|
||||
|
||||
self.assertTrue("I'm an index file!" in rendered)
|
|
@ -3,6 +3,74 @@ from django_cotton.tests.utils import get_compiled
|
|||
|
||||
|
||||
class TemplateRenderingTests(CottonTestCase):
|
||||
def test_component_is_rendered(self):
|
||||
self.create_template(
|
||||
"cotton/render.html",
|
||||
"""<div class="i-am-component">{{ slot }}</div>""",
|
||||
)
|
||||
|
||||
self.create_template(
|
||||
"view.html",
|
||||
"""<c-render>Hello, World!</c-render>""",
|
||||
"view/",
|
||||
)
|
||||
|
||||
# Override URLconf
|
||||
with self.settings(ROOT_URLCONF=self.url_conf()):
|
||||
response = self.client.get("/view/")
|
||||
self.assertContains(response, '<div class="i-am-component">')
|
||||
self.assertContains(response, "Hello, World!")
|
||||
|
||||
def test_nested_rendering(self):
|
||||
self.create_template(
|
||||
"cotton/parent.html",
|
||||
"""
|
||||
<div class="i-am-parent">
|
||||
{{ slot }}
|
||||
</div>
|
||||
""",
|
||||
)
|
||||
|
||||
self.create_template(
|
||||
"cotton/child.html",
|
||||
"""
|
||||
<div class="i-am-child"></div>
|
||||
""",
|
||||
)
|
||||
|
||||
self.create_template(
|
||||
"cotton/nested_render_view.html",
|
||||
"""
|
||||
<c-parent>
|
||||
<c-child>d</c-child>
|
||||
</c-parent>
|
||||
""",
|
||||
"view/",
|
||||
)
|
||||
|
||||
with self.settings(ROOT_URLCONF=self.url_conf()):
|
||||
response = self.client.get("/view/")
|
||||
self.assertContains(response, '<div class="i-am-parent">')
|
||||
self.assertContains(response, '<div class="i-am-child">')
|
||||
|
||||
def test_self_closing_is_rendered(self):
|
||||
self.create_template("cotton/self_closing.html", """I self closed!""")
|
||||
self.create_template(
|
||||
"self_closing_view.html",
|
||||
"""
|
||||
1: <c-self-closing/>
|
||||
2: <c-self-closing />
|
||||
3: <c-self-closing />
|
||||
""",
|
||||
"view/",
|
||||
)
|
||||
|
||||
with self.settings(ROOT_URLCONF=self.url_conf()):
|
||||
response = self.client.get("/view/")
|
||||
self.assertContains(response, "1: I self closed!")
|
||||
self.assertContains(response, "2: I self closed!")
|
||||
self.assertContains(response, "3: I self closed!")
|
||||
|
||||
def test_new_lines_in_attributes_are_preserved(self):
|
||||
self.create_template(
|
||||
"cotton/preserved.html",
|
||||
|
@ -142,7 +210,7 @@ class TemplateRenderingTests(CottonTestCase):
|
|||
|
||||
def test_cvars_isnt_changing_global_context(self):
|
||||
self.create_template(
|
||||
"cotton/child.html",
|
||||
"cotton/cvars_child.html",
|
||||
"""
|
||||
<c-vars />
|
||||
|
||||
|
@ -150,7 +218,7 @@ class TemplateRenderingTests(CottonTestCase):
|
|||
""",
|
||||
)
|
||||
self.create_template(
|
||||
"cotton/parent.html",
|
||||
"cotton/cvars_parent.html",
|
||||
"""
|
||||
name: parent (class: {{ class }}))
|
||||
|
||||
|
@ -161,9 +229,9 @@ class TemplateRenderingTests(CottonTestCase):
|
|||
self.create_template(
|
||||
"slot_scope_view.html",
|
||||
"""
|
||||
<c-parent>
|
||||
<c-child class="testy" />
|
||||
</c-parent>
|
||||
<c-cvars-parent>
|
||||
<c-cvars-child class="testy" />
|
||||
</c-cvars-parent>
|
||||
""",
|
||||
"view/",
|
||||
)
|
||||
|
|
|
@ -70,6 +70,12 @@ class CottonTestCase(TestCase):
|
|||
|
||||
def create_template(self, name, content, url=None, context={}):
|
||||
"""Create a template file in the temporary directory and return the path"""
|
||||
|
||||
# To test the non-default of allowing non-snake-cased names
|
||||
snake_cased_names = getattr(settings, "COTTON_SNAKE_CASED_NAMES", True)
|
||||
if not snake_cased_names:
|
||||
name = name.replace("_", "-")
|
||||
|
||||
path = os.path.join(self.temp_dir, name)
|
||||
|
||||
if os.path.exists(path):
|
||||
|
|
|
@ -9,7 +9,7 @@ COPY . .
|
|||
RUN ["npx", "tailwindcss", "-o", "./docs_project/static/app.css"]
|
||||
|
||||
# Use an official Python runtime as a base image
|
||||
FROM python:3.12-slim-bookworm as base
|
||||
FROM python:3.12-slim-bookworm AS base
|
||||
|
||||
# Setup env
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=on \
|
||||
|
@ -34,4 +34,7 @@ RUN poetry config virtualenvs.create false \
|
|||
|
||||
RUN SECRET_KEY=dummy STATIC_URL='/staticfiles/' python manage.py collectstatic --noinput --verbosity 2
|
||||
|
||||
# Now uninstall with poetry the container version of django-cotton, leaving the local version when we're developing
|
||||
RUN poetry remove django-cotton
|
||||
|
||||
CMD [ "python", "manage.py", "runserver", "0.0.0.0:8000" ]
|
|
@ -14,6 +14,10 @@
|
|||
|
||||
<h1 id="tabs">Re-usable tabs with alpine.js</h1>
|
||||
|
||||
<c-note>
|
||||
Be sure to checkout the notes on alpine.js support <a href="{% url 'components' %}#alpine-js-support">here</a>.
|
||||
</c-note>
|
||||
|
||||
<p>Let's tackle together a common UI requirement - tabs.</p>
|
||||
|
||||
<h3 class="mt-0 pt-2 mb-5" id="goals">We'll start by defining some goals:</h3>
|
||||
|
@ -69,7 +73,7 @@
|
|||
|
||||
<p>Now we have the design right, let's chop it up into components.</p>
|
||||
|
||||
<h4>Tabs component</h4>
|
||||
<h3>{{ '<c-tabs />'|force_escape }} component</h3>
|
||||
|
||||
<c-snippet label="cotton/tabs.html">{% cotton_verbatim %}{% verbatim %}
|
||||
<div class="bg-white rounded-lg overflow-hidden border shadow">
|
||||
|
@ -85,7 +89,7 @@
|
|||
{% endcotton_verbatim %}{% endverbatim %}
|
||||
</c-snippet>
|
||||
|
||||
<h4>Tab component</h4>
|
||||
<h3>{{ '<c-tab />'|force_escape }} component</h3>
|
||||
|
||||
<c-snippet label="cotton/tab.html">{% cotton_verbatim %}{% verbatim %}
|
||||
<div>
|
||||
|
@ -194,13 +198,13 @@
|
|||
|
||||
</c-snippet>
|
||||
|
||||
<c-navigation>
|
||||
<c-slot name="prev">
|
||||
<a href="{% url 'form-fields' %}">Form Inputs</a>
|
||||
</c-slot>
|
||||
<c-slot name="next">
|
||||
<a href="{% url 'layouts' %}">Layouts</a>
|
||||
</c-slot>
|
||||
</c-navigation>
|
||||
<c-navigation>
|
||||
<c-slot name="prev">
|
||||
<a href="{% url 'form-fields' %}">Form Inputs</a>
|
||||
</c-slot>
|
||||
<c-slot name="next">
|
||||
<a href="{% url 'layouts' %}">Layouts</a>
|
||||
</c-slot>
|
||||
</c-navigation>
|
||||
|
||||
</c-layouts.with-sidebar>
|
|
@ -11,6 +11,7 @@
|
|||
<c-index-sublink><a href="#boolean-attributes" class="no-underline">Boolean Attributes</a></c-index-sublink>
|
||||
<c-index-sublink><a href="#dynamic-components" class="no-underline">Dynamic Components</a></c-index-sublink>
|
||||
<c-index-sublink><a href="#context-isolation" class="no-underline">Context Isolation</a></c-index-sublink>
|
||||
<c-index-sublink><a href="#alpine-js-support" class="no-underline">Alpine.js Support</a></c-index-sublink>
|
||||
<c-index-sublink><a href="#summary" class="no-underline">Summary of concepts</a></c-index-sublink>
|
||||
</c-slot>
|
||||
|
||||
|
@ -19,7 +20,7 @@
|
|||
|
||||
<h2>1. The basic building block: {% verbatim %}{{ slot }}{% endverbatim %}</h2>
|
||||
|
||||
<p>The <c-highlight>{% verbatim %}{{ slot }}{% endverbatim %}</c-highlight> variable captures all content passed between a component's opening and closing tags.</p>
|
||||
<p>The <code>{% verbatim %}{{ slot }}{% endverbatim %}</code> variable captures all content passed between a component's opening and closing tags.</p>
|
||||
|
||||
<c-snippet label="cotton/box.html">{% cotton_verbatim %}{% verbatim %}
|
||||
<div class="box">
|
||||
|
@ -39,9 +40,9 @@
|
|||
</c-slot>
|
||||
</c-snippet>
|
||||
|
||||
<c-hr />
|
||||
<c-hr id="attributes" />
|
||||
|
||||
<h2 id="attributes">2. Adding Component Attributes</h2>
|
||||
<h2>2. Adding Component Attributes</h2>
|
||||
|
||||
<p>We can further customize components with attribute, which allow you to pass specific data into the component as key-value pairs.</p>
|
||||
|
||||
|
@ -58,11 +59,11 @@
|
|||
</c-slot>
|
||||
</c-snippet>
|
||||
|
||||
<c-hr />
|
||||
<c-hr id="named-slots" />
|
||||
|
||||
<h2 id="named-slots">3. Using Named Slots</h2>
|
||||
<h2>3. Using Named Slots</h2>
|
||||
|
||||
<p>If we want to pass HTML instead of just a string (or another data type) into a component, we can pass them as <c-highlight>named slots</c-highlight> with the <c-highlight>{{ '<c-slot name="...">...</c-slot>'|force_escape }}</c-highlight> syntax.</p>
|
||||
<p>If we want to pass HTML instead of just a string (or another data type) into a component, we can pass them as <c-highlight>named slots</c-highlight> with the <code>{{ '<c-slot name="...">...</c-slot>'|force_escape }}</code> syntax.</p>
|
||||
|
||||
<p>So as with normal attributes, you reference the slot content like normal variables, as in:</p>
|
||||
|
||||
|
@ -93,9 +94,13 @@
|
|||
</c-slot>
|
||||
</c-snippet>
|
||||
|
||||
<c-hr />
|
||||
<c-note>
|
||||
Component filenames should be <c-highlight>snake_cased</c-highlight> by default. To use <c-highlight>kebab-cased</c-highlight> / hyphenated filenames instead, set <code>COTTON_SNAKE_CASED_NAMES</code> to <code>False</code> in your settings.py, <a href="{% url 'configuration' %}">more</a>.
|
||||
</c-note>
|
||||
|
||||
<h2 id="dynamic-attributes">4. Dynamic Attributes with ":"</h2>
|
||||
<c-hr id="dynamic-attributes" />
|
||||
|
||||
<h2>4. Dynamic Attributes with ":"</h2>
|
||||
|
||||
<p>We saw how by default, all attributes that we pass to a component are treated as strings. If we want to pass HTML, we can use named slots. But what if we want to pass another data type like a template variable, boolean, integer, float, dictionary, list, dictionary?</p>
|
||||
|
||||
|
@ -176,9 +181,9 @@ context = { 'today': Weather.objects.get(...) }
|
|||
{% endcotton_verbatim %}{% endverbatim %}
|
||||
</c-snippet>
|
||||
|
||||
<c-hr />
|
||||
<c-hr id="attrs" />
|
||||
|
||||
<h2 id="attrs">5. Pass all attributes with {% verbatim %}{{ attrs }}{% endverbatim %}</h2>
|
||||
<h2>5. Pass all attributes with {% verbatim %}{{ attrs }}{% endverbatim %}</h2>
|
||||
|
||||
<p>Sometimes it's useful to be able to reflect all attributes provided in the parent on to an HTML element in the component. This is particularly powerful when you are building <a href="{% url 'form-fields' %}">form inputs</a>.</p>
|
||||
|
||||
|
@ -204,17 +209,17 @@ context = { 'today': Weather.objects.get(...) }
|
|||
{% endcotton_verbatim %}{% endverbatim %}
|
||||
</c-snippet>
|
||||
|
||||
<c-hr />
|
||||
<c-hr id="vars" />
|
||||
|
||||
<h2 id="vars">6. <c-highlight>{{ '<c-vars />'|force_escape }}</c-highlight>: Defining Local Variables</h2>
|
||||
<h2>6. Defining Local Variables with <code>{{ '<c-vars />'|force_escape }}</code></h2>
|
||||
|
||||
<p>{{ '<c-vars />'|force_escape }} gives components local state and default behavior, making them more self-sufficient and reducing the need for repetitive attribute declarations and maintaining UI state in the backend.</p>
|
||||
<p>The <code>{{ '<c-vars />'|force_escape }}</code> tag simplifies component design by allowing local variable definition, reducing the need for repetitive attribute declarations and maintaining backend state.</p>
|
||||
|
||||
<p>Vars are defined using a <c-highlight>{{ '<c-vars />'|force_escape }}</c-highlight> tag at the top of a component file. It can either contain key="value" pairs or just standalone keys (keep reading to understand why).</p>
|
||||
<p>Place a single <code>{{ '<c-vars />'|force_escape }}</code> at the top of a component to set key-value pairs that provide a default configuration.</p>
|
||||
|
||||
<h3 id="default-attributes">Use {{ '<c-vars />'|force_escape }} to set attribute defaults</h3>
|
||||
<h3>Example: Setting Default Attributes</h3>
|
||||
|
||||
<p>You may design a component that will often have a default behaviour and rarely needs overriding. In this case, you may opt to give a default value to your component.</p>
|
||||
<p>In components with common defaults, <code>{{ '<c-vars />'|force_escape }}</code> can pre-define attributes that rarely need overriding.</p>
|
||||
|
||||
<c-snippet label="cotton/alert.html">{% cotton_verbatim %}{% verbatim %}
|
||||
<c-vars type="success" />
|
||||
|
@ -235,9 +240,9 @@ context = { 'today': Weather.objects.get(...) }
|
|||
</c-slot>
|
||||
</c-snippet>
|
||||
|
||||
<h3 id="excluded">{{ '<c-vars />'|force_escape }} are excluded from {% verbatim %}{{ attrs }}{% endverbatim %}</h3>
|
||||
<h3>{{ '<c-vars />'|force_escape }} are excluded from {% verbatim %}{{ attrs }}{% endverbatim %}</h3>
|
||||
|
||||
<p>Keys defined in {{ '<c-vars />'|force_escape }} will not be included in {% verbatim %}{{ attrs }}{% endverbatim %}. This is useful when some of the properties you pass down to a component are for configuration purposes only and not intended as attributes.</p>
|
||||
<p>Keys in <code>{{ '<c-vars />'|force_escape }}</code> are omitted from <code>{% verbatim %}{{ attrs }}{% endverbatim %}</code>, making them ideal for configuration attributes that shouldn't appear in HTML attributes.</p>
|
||||
|
||||
<c-snippet label="cotton/input_group.html">{% cotton_verbatim %}{% verbatim %}
|
||||
<c-vars label errors />
|
||||
|
@ -271,12 +276,11 @@ context = { 'today': Weather.objects.get(...) }
|
|||
</c-slot>
|
||||
</c-snippet>
|
||||
|
||||
<c-note>Specifying an attribute as a 'var' will remove the item from {% verbatim %}{{ attrs }}{% endverbatim %}. It can also be a single key without a default value, this is when you know a particular attribute should not end up in {% verbatim %}{{ attrs }}{% endverbatim %}, whether it's defined in a parent or not.</c-note>
|
||||
<p>By specifying <code>label</code> and <code>errors</code> keys in <code>{{ '<c-vars />'|force_escape }}</code>, these attributes won’t be included in <code>{% verbatim %}{{ attrs }}{% endverbatim %}</code>, allowing you to control attributes that are designed for component configuration and those intended as attributes.</p>
|
||||
|
||||
<c-hr id="boolean-attributes" />
|
||||
|
||||
<c-hr />
|
||||
|
||||
<h2 id="boolean-attributes">7. Boolean attributes</h2>
|
||||
<h2>7. Boolean attributes</h2>
|
||||
|
||||
<p>Sometimes you just want to pass a simple boolean to a component. Cotton supports providing the attribute name without a value which will provide a boolean True to the component.</p>
|
||||
|
||||
|
@ -293,15 +297,15 @@ context = { 'today': Weather.objects.get(...) }
|
|||
<c-input name="telephone" required />
|
||||
{% endcotton_verbatim %}{% endverbatim %}
|
||||
<c-slot name="preview">
|
||||
<input type="text" autocomplete="off" class="border px-2 py-1 shadow rounded" name="telephone" placeholder="Telephone" /> <span class="text-red-500 text-3xl">*</span>
|
||||
<input type="text" autocomplete="off" class="border px-2 py-1 shadow rounded" name="telephone" placeholder="Telephone" /> <span class="text-red-500 text-2xl">*</span>
|
||||
</c-slot>
|
||||
</c-snippet>
|
||||
|
||||
<c-hr />
|
||||
<c-hr id="dynamic-components" />
|
||||
|
||||
<h2 id="dynamic-components">8. Dynamic Components</h2>
|
||||
<h2>8. Dynamic Components</h2>
|
||||
|
||||
<p>There can be times where components need to be included dynamically. For these cases we can reach for a special <c-highlight>{{ '<c-component>'|force_escape }}</c-highlight> tag with an <c-highlight>is</c-highlight> attribute:</p>
|
||||
<p>There can be times where components need to be included dynamically. For these cases we can reach for a special <code>{{ '<c-component>'|force_escape }}</code> tag with an <code>is</code> attribute:</p>
|
||||
|
||||
<c-snippet label="cotton/icon_list.html">{% cotton_verbatim %}{% verbatim %}
|
||||
{% for icon in icons %}
|
||||
|
@ -309,7 +313,7 @@ context = { 'today': Weather.objects.get(...) }
|
|||
{% endfor %}
|
||||
{% endcotton_verbatim %}{% endverbatim %}</c-snippet>
|
||||
|
||||
<p>The <c-highlight>is</c-highlight> attribute is similar to other attributes so we have a number of possibilities to define it:</p>
|
||||
<p>The <code>is</code> attribute is similar to other attributes so we have a number of possibilities to define it:</p>
|
||||
|
||||
<c-snippet label="cotton/icon_list.html">{% cotton_verbatim %}{% verbatim %}
|
||||
<!-- as variable -->
|
||||
|
@ -319,12 +323,12 @@ context = { 'today': Weather.objects.get(...) }
|
|||
<c-component is="icon_{{ icon_name }}" />
|
||||
{% endcotton_verbatim %}{% endverbatim %}</c-snippet>
|
||||
|
||||
<c-hr />
|
||||
<c-hr id="context-isolation" />
|
||||
|
||||
<h2 id="context-isolation">9. Context Isolation</h2>
|
||||
<h2>9. Context Isolation</h2>
|
||||
|
||||
<p>Cotton is inspired by patterns found in frontend frameworks like React, Vue and Svelte. When working with these
|
||||
patterns, state is not typically shared between components. This ensures data from other components do not 'leak' into
|
||||
patterns, state is not typically shared between components. This ensures data from other components does not 'leak' into
|
||||
others which can cause side effects that are difficult to trace.</p>
|
||||
|
||||
<p>Therefore, each component's context only contains, by default:</p>
|
||||
|
@ -339,22 +343,34 @@ context = { 'today': Weather.objects.get(...) }
|
|||
|
||||
<p>You can pass the <c-highlight>only</c-highlight> attribute to the component, which will prevent it from adopting any context (incl. context processors) other than it's direct attributes.</p>
|
||||
|
||||
<h3>Retain legacy behaviour</h3>
|
||||
<c-hr id="alpine-js-support" />
|
||||
|
||||
<p>Since context isolation is enabled by default since v2.0.0, there may be cases you wish to keep the legacy behaviour, for that reason, you can pass a `COTTON_CONTEXT_ISOLATION_ENABLED = False` in your settings.py. This is not recommend due to the aforementioned reason so allowing context isolation by default is encouraged.</p>
|
||||
<h2>10. Alpine.js support</h2>
|
||||
|
||||
<c-hr />
|
||||
<p>The following key features allow you to build re-usable components with alpine.js:</p>
|
||||
|
||||
<h2 id="summary">Summary of Concepts</h2>
|
||||
<c-ul>
|
||||
<li>
|
||||
<code>x-data</code> is accessible as <code>{% verbatim %}{{ x_data }}{% endverbatim %}</code> inside the component as cotton makes available snake_case versions of all kebab-cased attributes. (If you use <code>{% verbatim %}{{ attrs }}{% endverbatim %}</code> then the output will already be in the correct case).
|
||||
</li>
|
||||
<li><a href="https://alpinejs.dev/directives/bind" target="_blank">Shorthand x-bind</a> support (<code>:example</code>). Because single <code>:</code> attribute prefixing is reserved for cotton's <a href="{% url 'components' %}#dynamic-attributes">dynamic attributes</a>,
|
||||
we can escape the first colon using <code>::</code>. This will ensure the attribute maintains a single <code>:</code> inside <code>{% verbatim %}{{ attrs }}{% endverbatim %}</code>
|
||||
</li>
|
||||
</c-ul>
|
||||
|
||||
<c-hr id="summary" />
|
||||
|
||||
<h2>Summary of Concepts</h2>
|
||||
<ul>
|
||||
<li><c-highlight>{% verbatim %}{{ slot }}{% endverbatim %}</c-highlight> - Default content injection.</li>
|
||||
<li><code>{% verbatim %}{{ slot }}{% endverbatim %}</code> - Default content injection.</li>
|
||||
<li><c-highlight>Attributes</c-highlight> - Simple, straightforward customization.</li>
|
||||
<li><c-highlight>Named Slots</c-highlight> - Provide HTML or template partial as a variable in the component.</li>
|
||||
<li><c-highlight>`:` Dynamic Attributes</c-highlight> - Pass variables and other data types other than strings.</li>
|
||||
<li><c-highlight>{% verbatim %}{{ attrs }}{% endverbatim %}</c-highlight> - Prints attributes as HTML attributes.</li>
|
||||
<li><c-highlight>{{ '<c-vars />'|force_escape }}</c-highlight> - Set default values and other component state.</li>
|
||||
<li><c-highlight>Boolean attributes</c-highlight> - Attributes without values are passed down as `:bool = True`</li>
|
||||
<li><c-highlight>{{ '<c-component is=".." />'|force_escape }}</c-highlight> - Dynamically insert a component where the name is generated by a variable or template expression</li>
|
||||
<li><c-highlight><code>:</code> Dynamic Attributes</c-highlight> - Pass variables and other data types other than strings.</li>
|
||||
<li><code>{% verbatim %}{{ attrs }}{% endverbatim %}</code> - Prints attributes as HTML attributes.</li>
|
||||
<li><code>{{ '<c-vars />'|force_escape }}</code> - Set default values and other component state.</li>
|
||||
<li><c-highlight>Boolean attributes</c-highlight> - Attributes without values are passed down as <code>True</code></li>
|
||||
<li><c-highlight>Dynamic Components</c-highlight> - Insert a component where the name is generated by a variable or template expression: <code>{{ '<c-component :is="my_variable" />'|force_escape }}</code>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
@ -363,7 +379,7 @@ context = { 'today': Weather.objects.get(...) }
|
|||
<a href="{% url 'quickstart' %}">Quickstart</a>
|
||||
</c-slot>
|
||||
<c-slot name="next">
|
||||
<a href="{% url 'layouts' %}">Layouts</a>
|
||||
<a href="{% url 'usage-patterns' %}">Usage Patterns</a>
|
||||
</c-slot>
|
||||
</c-navigation>
|
||||
|
||||
|
|
|
@ -3,15 +3,66 @@
|
|||
|
||||
<p>The following configuration can be provided in your `settings.py`:</p>
|
||||
|
||||
<h4>COTTON_DIR</h4>
|
||||
<p>str (default: 'cotton')</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<code>COTTON_DIR</code>
|
||||
<div>str (default: 'cotton')</div>
|
||||
</div>
|
||||
<div>
|
||||
Change the default path in your templates directory where cotton components can be placed, for example "components".
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<code>COTTON_ENABLE_CONTEXT_ISOLATION</code>
|
||||
<div>boolean (default: True)</div>
|
||||
</div>
|
||||
<div>
|
||||
Set to `False` to allow global context to be available through all components. (see <a href="{% url 'components' %}#context-isolation">context isolation</a> for more information.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<c-hr />
|
||||
|
||||
<h4>COTTON_CONTEXT_ISOLATION_ENABLED</h4>
|
||||
<p>str (default: 'True')</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<code>COTTON_BASE_DIR</code>
|
||||
<div>str (default: None)</div>
|
||||
</div>
|
||||
<div>
|
||||
The base directory where - in addition to the app folders - cotton will search for the "templates" directory (see above).
|
||||
If not set, the `BASE_DIR` generated by `django-admin startproject` is used as a fallback, if it exists.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Set to `False` to allow global context to be available through all components. (see <a href="{% url 'components' %}#context-isolation">context isolation</a> for more information.</p>
|
||||
<c-hr />
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<code>COTTON_SNAKE_CASED_NAMES</code>
|
||||
<div>bool (default: True)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-4">By default cotton will look for snake case versions of your component names. To turn this behaviour off (useful if you want to permit hyphenated filenames) then set this key to <code>False</code>.</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h6 class="mb-1">Example:</h6>
|
||||
<code>{{ '<c-my-button />'|force_escape }}</code>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h6>As <code>True</code> (default)</h6>
|
||||
Filepath: `cotton/my_button.html`
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>As <code>False</code></h6>
|
||||
Filepath: `cotton/my-button.html`
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</c-layouts.with-sidebar>
|
||||
</c-layouts.with-sidebar>
|
||||
|
|
|
@ -1 +1 @@
|
|||
<span class="font-bold dark:text-white text-[#1a384a] font-semibold border-b-2 border-teal-600 border-opacity-70">{{ slot }}</span>
|
||||
<span class="dark:text-white text-[#1a384a] font-semibold border-b-2 border-teal-600 border-opacity-70">{{ slot }}</span>
|
|
@ -1 +1 @@
|
|||
<div {{ attrs }} class="h-[2px] bg-yellow-900 bg-opacity-10 my-8 w-full dark:bg-gray-700"></div>
|
||||
<div {{ attrs }} class="h-[1px] bg-yellow-900 bg-opacity-10 mb-6 mt-12 w-full dark:bg-gray-700"></div>
|
|
@ -0,0 +1,3 @@
|
|||
<svg {{ attrs }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
After Width: | Height: | Size: 219 B |
|
@ -0,0 +1,3 @@
|
|||
<svg {{ attrs }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 15.75 7.5-7.5 7.5 7.5" />
|
||||
</svg>
|
After Width: | Height: | Size: 219 B |
|
@ -0,0 +1,3 @@
|
|||
<svg {{ attrs }} fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 24 24">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M8 12a1 1 0 1 0 2 0a1 1 0 0 0 -2 0" /><path d="M14 12a1 1 0 1 0 2 0a1 1 0 0 0 -2 0" /><path d="M15.5 17c0 1 1.5 3 2 3c1.5 0 2.833 -1.667 3.5 -3c.667 -1.667 .5 -5.833 -1.5 -11.5c-1.457 -1.015 -3 -1.34 -4.5 -1.5l-.972 1.923a11.913 11.913 0 0 0 -4.053 0l-.975 -1.923c-1.5 .16 -3.043 .485 -4.5 1.5c-2 5.667 -2.167 9.833 -1.5 11.5c.667 1.333 2 3 3.5 3c.5 0 2 -2 2 -3" /><path d="M7 16.5c3.5 1 6.5 1 10 0" />
|
||||
</svg>
|
After Width: | Height: | Size: 665 B |
|
@ -1,13 +1,3 @@
|
|||
{% comment %}
|
||||
<svg {{ attrs }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"/>
|
||||
</svg>{% endcomment %}
|
||||
|
||||
<svg {{ attrs }} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"/>
|
||||
<path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7"/>
|
||||
</svg>
|
||||
<svg {{ attrs }} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /><path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7" />
|
||||
</svg>
|
Before Width: | Height: | Size: 894 B After Width: | Height: | Size: 412 B |
|
@ -1 +1 @@
|
|||
<li class="py-0.5 pl-1 ml-5 list-disc marker:text-teal-600 text-[15px] text-[#1a384a] dark:text-gray-400">{{ slot }}</li>
|
||||
<li class="py-0.5 pl-1 ml-5 list-disc marker:text-teal-600 text-[#1a384a] dark:text-gray-400">{{ slot }}</li>
|
|
@ -11,6 +11,7 @@
|
|||
<link rel="icon" type="image/png" href="{% static 'favicon/favicon.png' %}">
|
||||
|
||||
<script src="{% static 'highlight/highlight.min.js' %}"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
|
||||
<script defer src="{% static 'alpine3.min.js' %}"></script>
|
||||
<link href="{% static 'highlight/styles/atom-one-dark.min.css' %}" rel="stylesheet" />
|
||||
<script>
|
||||
|
|
|
@ -4,15 +4,76 @@
|
|||
<c-sidebar />
|
||||
</div>
|
||||
<div class="overflow-auto flex-1 pb-10">
|
||||
<div class="max-w-[760px] mx-auto px-0 pt-3 md:pt-0 md:px-5 py-0 prose !text-[16.5px] prose-a:text-teal-600 prose-h2:text-3xl prose-h3:text-2xl prose-headings:text-[#1a384a] text-[#1a384a] dark:text-white dark:text-opacity-70 prose-headings:font-semibold dark:prose-headings:text-white">
|
||||
<div class="
|
||||
max-w-[760px]
|
||||
mx-auto
|
||||
px-0
|
||||
pt-3
|
||||
md:pt-0
|
||||
md:px-5
|
||||
py-0
|
||||
!text-[16.5px]
|
||||
text-[#1a384a]
|
||||
|
||||
prose
|
||||
prose-a:text-teal-600
|
||||
prose-h1:text-3xl
|
||||
prose-h2:text-2xl
|
||||
prose-h3:text-xl
|
||||
prose-headings:text-[#1a384a]
|
||||
prose-headings:font-semibold
|
||||
prose-code:before:content-none
|
||||
prose-code:after:content-none
|
||||
prose-code:bg-yellow-900/10
|
||||
prose-code:rounded
|
||||
prose-code:px-1
|
||||
prose-code:inline-block
|
||||
prose-code:my-0
|
||||
|
||||
dark:text-white
|
||||
dark:text-opacity-70
|
||||
dark:prose-headings:text-white
|
||||
dark:prose-code:bg-gray-800
|
||||
dark:prose-code:text-gray-300/80
|
||||
dark:prose-strong:text-[#fff]
|
||||
">
|
||||
{% if page_index %}
|
||||
<div class="lg:hidden not-prose mb-6">
|
||||
|
||||
<c-ui.collapse>
|
||||
<c-slot name="trigger">
|
||||
|
||||
<c-ui.button theme="subtle" class="w-full">
|
||||
<span class="flex w-full justify-between items-center">
|
||||
<span>On this page</span>
|
||||
<template x-if="!expanded">
|
||||
<c-icons.chevron-down class="size-5" />
|
||||
</template>
|
||||
<template x-if="expanded">
|
||||
<c-icons.chevron-up class="size-5" />
|
||||
</template>
|
||||
</span>
|
||||
</c-ui.button>
|
||||
</c-slot>
|
||||
|
||||
<div class="mt-5">
|
||||
{{ page_index }}
|
||||
</div>
|
||||
|
||||
<c-hr />
|
||||
</c-ui.collapse>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ slot }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative h-full pl-10 border-opacity-10 hidden lg:block">
|
||||
<div class="relative h-full pl-10 border-opacity-10 hidden lg:block text-[15px]">
|
||||
{% if page_index %}
|
||||
<div class="sticky top-[68px] pt-0 pb-10">
|
||||
<c-sidebar-heading>On this page</c-sidebar-heading>
|
||||
<ul class="ml-0 pl-0 mt-4">
|
||||
<ul class="ml-0 pl-0 mt-4 ">
|
||||
{{ page_index }}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -43,13 +43,13 @@
|
|||
<div class="flex items-center space-x-1">
|
||||
<c-icons.logo class="w-8 h-8 text-teal-600" />
|
||||
<span class="font-bold text-2xl ml-1.5 text-[#1a384a] dark:text-gray-100">cotton</span>
|
||||
<div class="text-gray-700 dark:text-white text-sm opacity-50 mt-0.5 px-1">for</div>
|
||||
<c-icons.django class="h-6 text-teal-600 mt-1.5" />
|
||||
<div class=" text-gray-700 dark:text-white text-sm opacity-50 mt-0.5 px-1">for</div>
|
||||
<c-icons.django class=" h-6 text-teal-600 mt-1.5" />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<a href="#" @click.prevent="toggleDark" x-data="{
|
||||
'dark': false,
|
||||
init() {
|
||||
|
@ -64,10 +64,13 @@
|
|||
localStorage.theme = this.dark ? 'dark' : 'light'
|
||||
}
|
||||
}">
|
||||
<c-icons.sun class="size-8 text-yellow-900 opacity-60 dark:text-gray-100" stroke-width="1.5" />
|
||||
<c-icons.sun class="size-7 text-yellow-900/40 dark:text-gray-100/50" />
|
||||
</a>
|
||||
<a href="https://github.com/wrabit/django-cotton" target="_blank" class="text-yellow-900 opacity-60 dark:text-gray-100 flex items-center space-x-2">
|
||||
<c-icons.github class="size-7" />
|
||||
<a href="https://github.com/wrabit/django-cotton" target="_blank" class="text-yellow-900/40 dark:text-gray-100/50 flex items-center space-x-2">
|
||||
<c-icons.github class="size-6" />
|
||||
</a>
|
||||
<a href="https://discord.gg/4x8ntQwHMe" target="_blank" class="text-yellow-900/40 dark:text-gray-100/50 flex items-center space-x-2">
|
||||
<c-icons.discord class="size-7" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -83,4 +86,4 @@
|
|||
@click.away="isOpen = false" class="absolute shadow-md bg-white dark:bg-gray-800 top-[67px] inset-x-0 px-5 transition transform origin-top-right md:hidden">
|
||||
<c-sidebar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<div class="mt-6 border border-teal-600 rounded-lg px-8 py-5 dark:border dark:bg-transparent">
|
||||
<div class="mt-6 border-2 border-teal-500 bg-teal-500/10 rounded-lg px-8 py-5">
|
||||
{{ slot }}
|
||||
</div>
|
|
@ -1,5 +1,4 @@
|
|||
<div class="sticky top-[68px] pb-6 pt-2">
|
||||
|
||||
<c-sidebar-block>
|
||||
<c-slot name="title">Getting Started</c-slot>
|
||||
<ul>
|
||||
|
@ -9,6 +8,9 @@
|
|||
<c-sidebar-link url="{% url 'components' %}">
|
||||
Components
|
||||
</c-sidebar-link>
|
||||
<c-sidebar-link url="{% url 'usage-patterns' %}">
|
||||
Usage Patterns
|
||||
</c-sidebar-link>
|
||||
</ul>
|
||||
</c-sidebar-block>
|
||||
<c-sidebar-block>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="py-4 {{ class }} border-b border-yellow-900 border-opacity-15 dark:border-gray-700 dark:border-opacity-100 last:border-0 first:pt-0 leading-7">
|
||||
<div class="py-4 {{ class }} first:pt-0 leading-7">
|
||||
{% if title %}
|
||||
<c-sidebar-heading>{{ title }}</c-sidebar-heading>
|
||||
{% endif %}
|
||||
|
|
|
@ -1 +1 @@
|
|||
<div class="dark:text-white text-[#1a384a] dark:opacity-35 text-yellow-800 brightness-65 text-opacity-50 font-semibold uppercase text-[14px] mb-2 tracking-wider">{{ slot }}</div>
|
||||
<div class="dark:text-white dark:opacity-35 text-yellow-900/40 font-semibold uppercase text-[14px] mb-2 tracking-wider">{{ slot }}</div>
|
|
@ -0,0 +1,39 @@
|
|||
<c-vars
|
||||
:themes="{
|
||||
'primary': 'bg-sky-500',
|
||||
'subtle': 'bg-neutral-50 text-gray-800 dark:bg-gray-300/10 dark:text-neutral-300',
|
||||
'danger': 'bg-red-500',
|
||||
'warning': 'bg-yellow-500',
|
||||
'info': 'bg-blue-500',
|
||||
}"
|
||||
theme="primary"
|
||||
:outlined-themes="{
|
||||
'primary': 'border border-sky-500 text-sky-500',
|
||||
'subtle': 'border border-neutral-50 text-gray-800',
|
||||
'danger': 'border border-red-500 text-red-500',
|
||||
'warning': 'border border-yellow-500 text-yellow-500',
|
||||
'info': 'border border-blue-500 text-blue-500',
|
||||
}"
|
||||
:outlined="False"
|
||||
class
|
||||
/>
|
||||
|
||||
<button {{ attrs }} type="button" class="
|
||||
{% if outlined %}
|
||||
{{ outlined_themes|get_item:theme }}
|
||||
{% else %}
|
||||
{{ themes|get_item:theme }}
|
||||
{% endif %}
|
||||
cursor-pointer whitespace-nowrap rounded-md
|
||||
px-4 py-2 text-sm font-medium tracking-wide transition hover:opacity-75 text-center
|
||||
focus-visible:outline
|
||||
focus-visible:outline-2
|
||||
focus-visible:outline-offset-2
|
||||
focus-visible:outline-sky-500
|
||||
active:opacity-100
|
||||
active:outline-offset-0
|
||||
disabled:opacity-75
|
||||
disabled:cursor-not-allowed
|
||||
{{ class }}">
|
||||
{{ slot }}
|
||||
</button>
|
|
@ -0,0 +1,13 @@
|
|||
<c-vars trigger_text trigger />
|
||||
|
||||
<div x-data="{ expanded: false }">
|
||||
{% if trigger %}
|
||||
<span @click="expanded = ! expanded" class="cursor-pointer">{{ trigger }}</span>
|
||||
{% else %}
|
||||
<button @click="expanded = ! expanded">{% firstof trigger_text "Toggle" %}</button>
|
||||
{% endif %}
|
||||
|
||||
<div x-show="expanded" x-collapse x-cloak>
|
||||
{{ slot }}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,41 @@
|
|||
<c-vars trigger_text />
|
||||
|
||||
<div x-data="{ isOpen: false, openedWithKeyboard: false }" class="relative" @keydown.esc.window="isOpen = false, openedWithKeyboard = false">
|
||||
<!-- Toggle Button -->
|
||||
<button
|
||||
type="button"
|
||||
@click="isOpen = ! isOpen"
|
||||
class="inline-flex cursor-pointer items-center gap-2 whitespace-nowrap rounded-md border border-neutral-300 bg-neutral-50 px-4 py-2 text-sm font-medium tracking-wide transition
|
||||
hover:opacity-75
|
||||
focus-visible:outline
|
||||
focus-visible:outline-2
|
||||
focus-visible:outline-offset-2
|
||||
focus-visible:outline-neutral-800
|
||||
dark:border-neutral-700
|
||||
dark:bg-gray-800
|
||||
dark:focus-visible:outline-neutral-300"
|
||||
aria-haspopup="true"
|
||||
@keydown.space.prevent="openedWithKeyboard = true"
|
||||
@keydown.enter.prevent="openedWithKeyboard = true"
|
||||
@keydown.down.prevent="openedWithKeyboard = true"
|
||||
:class="isOpen || openedWithKeyboard ? 'text-neutral-900 dark:text-white' : 'text-neutral-600 dark:text-neutral-300'"
|
||||
aria-expanded="isOpen || openedWithKeyboard">
|
||||
{% firstof trigger_text "Select" %}
|
||||
<svg aria-hidden="true" fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4 totate-0">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"/>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Dropdown Menu -->
|
||||
<div x-cloak x-show="isOpen || openedWithKeyboard"
|
||||
x-transition
|
||||
x-trap="openedWithKeyboard"
|
||||
@click.outside="isOpen = false, openedWithKeyboard = false"
|
||||
@keydown.down.prevent="$focus.wrap().next()"
|
||||
@keydown.up.prevent="$focus.wrap().previous()"
|
||||
class="absolute top-11 left-0 flex w-full min-w-[12rem] flex-col overflow-hidden rounded-md border border-neutral-300 bg-neutral-50 py-1.5
|
||||
dark:border-neutral-700
|
||||
dark:bg-gray-800"
|
||||
role="menu">
|
||||
{{ slot }}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,8 @@
|
|||
<a href="#" class="bg-neutral-50 px-4 py-2 text-sm text-neutral-600
|
||||
hover:bg-neutral-900/5
|
||||
hover:text-neutral-900 focus-visible:bg-neutral-900/10 focus-visible:text-neutral-900 focus-visible:outline-none
|
||||
dark:bg-black/10
|
||||
dark:text-neutral-300
|
||||
dark:hover:bg-neutral-50/5 dark:hover:text-white
|
||||
dark:focus-visible:bg-neutral-50/10
|
||||
dark:focus-visible:text-white" role="menuitem">{{ slot }}</a>
|
|
@ -41,7 +41,7 @@
|
|||
|
||||
<h2 id="type">Change the input type</h2>
|
||||
|
||||
<p>You will probably need more than just a text input in your project. So let's declare an attribute `text` in <c-highlight>{{ '<c-vars />'|force_escape }}</c-highlight>. Adding it as a var will allow us to set "text" as the default. Additionally, it will be excluded from <c-highlight>{% verbatim %}{{ attrs }}{% endverbatim %}</c-highlight>:</p>
|
||||
<p>You will probably need more than just a text input in your project. So let's declare an attribute `text` in <code>{{ '<c-vars />'|force_escape }}</code>. Adding it as a var will allow us to set "text" as the default. Additionally, it will be excluded from <code>{% verbatim %}{{ attrs }}{% endverbatim %}</code>:</p>
|
||||
|
||||
<c-snippet label="cotton/input.html">{% cotton_verbatim %}{% verbatim %}
|
||||
<c-vars type="text" />
|
||||
|
@ -140,7 +140,7 @@
|
|||
|
||||
<c-navigation>
|
||||
<c-slot name="prev">
|
||||
<a href="{% url 'components' %}">Components</a>
|
||||
<a href="{% url 'usage-patterns' %}">Usage Patterns</a>
|
||||
</c-slot>
|
||||
<c-slot name="next">
|
||||
<a href="{% url 'alpine-js' %}">Tabs with Alpine.js</a>
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<div class="w-full shrink-0"></div>
|
||||
<span class="pt-0 md:pt-5 clear-both flex-wrap inline-flex justify-center items-center space-x-2">
|
||||
<span class="">Hello</span>
|
||||
<span class="shrink-0 px-3 py-1 bg-teal-600 font-mono text-xl sm:text-2xl md:text-4xl rounded-lg text-white inline-block font-normal leading-normal">{{ '<c-component />' }}</span>
|
||||
<span class="shrink-0 px-3 py-1 bg-teal-500/10 border-[3px] border-teal-600 font-mono text-xl sm:text-2xl md:text-4xl rounded-3xl text-teal-600 dark:text-white inline-block font-normal leading-normal">{{ '<c-comp />'|force_escape }}</span>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
|
@ -32,7 +32,7 @@
|
|||
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="col-span-1 flex flex-col overflow-x-auto">
|
||||
<h2 class="!font-normal !text-xl mb-3 mt-0 text-center"><span class="font-semibold">Before:</span> Strongly Coupled, Verbose</h2>
|
||||
<h2 class="!font-normal !text-lg mb-3 mt-0 text-center"><span class="font-semibold">Before:</span> Strongly Coupled, Verbose</h2>
|
||||
<div class="flex h-full rounded-xl overflow-hidden">
|
||||
<c-demo.snippet-tabs labels="view.html|product_layout.html" tabs_id="compare">
|
||||
<div class="flex flex-col h-full" x-show="active === 0">
|
||||
|
@ -44,7 +44,7 @@
|
|||
icon.png
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
{% block title %}
|
||||
Item Title
|
||||
{% endblock %}
|
||||
|
||||
|
@ -88,7 +88,7 @@ Item Title
|
|||
</div>
|
||||
</div>
|
||||
<div class="col-span-1 rounded-lg overflow-hidden flex flex-col">
|
||||
<h2 class="!font-normal !text-xl mb-3 mt-0 text-center"><span class="font-semibold">After:</span> Decoupled, Clean & Re-usable</h2>
|
||||
<h2 class="!font-normal !text-lg mb-3 mt-0 text-center"><span class="font-semibold">After:</span> Decoupled, Clean & Re-usable</h2>
|
||||
<div class="flex h-full rounded-xl overflow-hidden">
|
||||
<c-demo.snippet-tabs labels="view.html|product.html" tabs_id="compare">
|
||||
<div class="flex flex-col h-full" x-show="active === 0">
|
||||
|
@ -179,4 +179,4 @@ Item Title
|
|||
|
||||
</div>
|
||||
|
||||
</c-layouts.with-sidebar>
|
||||
</c-layouts.with-sidebar>
|
||||
|
|
|
@ -2,23 +2,19 @@
|
|||
<c-slot name="page_index">
|
||||
<c-index-link><a href="#install" class="no-underline">Install Cotton</a></c-index-link>
|
||||
<c-index-link><a href="#create-a-component" class="no-underline">Create a component</a></c-index-link>
|
||||
<c-index-link><a href="#templates-location" class="no-underline">Templates location</a></c-index-link>
|
||||
<c-index-link><a href="#include-a-component" class="no-underline">Include a component</a></c-index-link>
|
||||
<c-index-link><a href="#usage" class="no-underline">Usage</a></c-index-link>
|
||||
<c-index-sublink><a href="#basics" class="no-underline text-opacity-70">Basics</a></c-index-sublink>
|
||||
<c-index-sublink><a href="#naming" class="no-underline">Naming</a></c-index-sublink>
|
||||
<c-index-sublink><a href="#subfolders" class="no-underline">Subfolders</a></c-index-sublink>
|
||||
<c-index-sublink><a href="#tag-style" class="no-underline">Tag Style</a></c-index-sublink>
|
||||
</c-slot>
|
||||
|
||||
<h1 id="install">Quickstart</h1>
|
||||
|
||||
<h2>Install cotton</h2>
|
||||
<h2 class="mt-0">Install cotton</h2>
|
||||
<p>Run the following command:</p>
|
||||
<c-snippet language="python">pip install django-cotton</c-snippet>
|
||||
|
||||
<p>Then update your settings.py:</p>
|
||||
|
||||
<h3>Automatic configuration:</h3>
|
||||
<h3>For automatic configuration:</h3>
|
||||
|
||||
<c-snippet language="python" label="settings.py">{% cotton_verbatim %}{% verbatim %}
|
||||
INSTALLED_APPS = [
|
||||
|
@ -26,9 +22,9 @@ INSTALLED_APPS = [
|
|||
]
|
||||
{% endverbatim %}{% endcotton_verbatim %}</c-snippet>
|
||||
|
||||
<p>This will automatically handle the settings.py adding the required loader and templatetags.</p>
|
||||
<p>This will attempt to automatically handle the settings.py by adding the required loader and templatetags.</p>
|
||||
|
||||
<h3>Customised configuration</h3>
|
||||
<h3>For custom configuration</h3>
|
||||
|
||||
<p>If your project requires any non-default loaders or you do not wish Cotton to manage your settings, you should instead provide `django_cotton.apps.SimpleAppConfig` in your INSTALLED_APPS:</p>
|
||||
|
||||
|
@ -72,6 +68,21 @@ TEMPLATES = [
|
|||
</div>
|
||||
{% endverbatim %}{% endcotton_verbatim %}</c-snippet>
|
||||
|
||||
<c-hr id="templates-location" />
|
||||
|
||||
<h2>Templates location</h2>
|
||||
|
||||
<p>Cotton supports 2 common approaches regarding the location of the <code>templates</code> directory:</p>
|
||||
|
||||
<c-ul>
|
||||
<li><strong>App level</strong> - You can place your cotton folder in any of your installed app folders, like: <div><code>[project]/[app]/templates/cotton/row.html</code></div></li>
|
||||
<li>
|
||||
<strong>Project root</strong> - You can place your cotton folder in a project level templates directory, like: <div><code>[project]/templates/cotton/row.html</code></div>
|
||||
(<code>[project]</code> location is provided by `BASE_DIR` if present or you may set it with `COTTON_BASE_DIR`)
|
||||
</li>
|
||||
</c-ul>
|
||||
|
||||
<p>Any style will allow you to include your component the same way: <code>{{ '<c-row />'|force_escape }}</code></p>
|
||||
|
||||
<c-hr id="include-a-component" />
|
||||
|
||||
|
@ -105,34 +116,6 @@ def dashboard_view(request):
|
|||
</c-slot>
|
||||
</c-snippet>
|
||||
|
||||
<c-hr />
|
||||
|
||||
<h2 id="usage">Usage</h2>
|
||||
|
||||
<h3 id="basics">Basics</h3>
|
||||
<c-ul>
|
||||
<li>Cotton components should be placed in the <c-highlight>templates/cotton</c-highlight> folder (unless you have set COTTON_DIR).</li>
|
||||
</c-ul>
|
||||
|
||||
<h3 id="naming">Naming</h3>
|
||||
<p>Cotton uses the following naming conventions:</p>
|
||||
<c-ul>
|
||||
<li>Component file names are in snake_case: <c-highlight>my_component.html</c-highlight></li>
|
||||
<li>but are called using kebab-case: <c-highlight>{{ "<c-my-component />"|force_escape }}</c-highlight></li>
|
||||
</c-ul>
|
||||
|
||||
<h3 id="subfolders">Subfolders</h3>
|
||||
|
||||
<c-ul>
|
||||
<li>Components in subfolders can be defined using dot notation</li>
|
||||
<li>A component in <c-highlight>sidebar/menu/link.html</c-highlight> would be included as <c-highlight>{{ "<c-sidebar.menu.link />"|force_escape }}</c-highlight></li>
|
||||
</c-ul>
|
||||
|
||||
<h3 id="tag-style">Tag Style</h3>
|
||||
<c-ul>
|
||||
<li>Components can either be self-closing <c-highlight>{{ "<c-my-component />"|force_escape }}</c-highlight> or have a closing tag <c-highlight>{{ "<c-my-component></c-my-component>"|force_escape }}</c-highlight></li>
|
||||
</c-ul>
|
||||
|
||||
<c-navigation>
|
||||
<c-slot name="prev">
|
||||
<a href="{% url 'home' %}">Home</a>
|
||||
|
|
79
docs/docs_project/docs_project/templates/usage_patterns.html
Normal file
79
docs/docs_project/docs_project/templates/usage_patterns.html
Normal file
|
@ -0,0 +1,79 @@
|
|||
<c-layouts.with-sidebar>
|
||||
<h1 id="components">Usage Patterns</h1>
|
||||
|
||||
<c-slot name="page_index">
|
||||
<c-index-link><a href="#components" class="no-underline">Components</a></c-index-link>
|
||||
<c-index-sublink><a href="#basics" class="no-underline text-opacity-70">Basics</a></c-index-sublink>
|
||||
<c-index-sublink><a href="#naming" class="no-underline">Naming</a></c-index-sublink>
|
||||
<c-index-sublink><a href="#subfolders" class="no-underline">Subfolders</a></c-index-sublink>
|
||||
<c-index-sublink><a href="#index" class="no-underline">index.html</a></c-index-sublink>
|
||||
<c-index-sublink><a href="#tag-style" class="no-underline">Tag Style</a></c-index-sublink>
|
||||
</c-slot>
|
||||
|
||||
<h3 id="basics">Basics</h3>
|
||||
<c-ul>
|
||||
<li>Cotton components should be placed in the <c-highlight>templates/cotton</c-highlight> folder ('cotton' path is <a href="{% url 'configuration' %}">configurable</a> using <code>COTTON_DIR</code>).</li>
|
||||
<li>The <code>templates</code> folder can be located in either an app-level or top-level project root folder.</li>
|
||||
</c-ul>
|
||||
|
||||
<c-hr />
|
||||
|
||||
<h3 id="naming">Naming</h3>
|
||||
<p>Cotton uses the following naming conventions:</p>
|
||||
<c-ul>
|
||||
<li>By default, component file names are in snake_case (<c-highlight>my_component.html</c-highlight>).
|
||||
<li>kebab-case filenames (<c-highlight>my-component.html</c-highlight>) can be enabled with <code>COTTON_SNAKE_CASED_NAMES = False</code> see <a href="{% url 'configuration' %}">configuration</a>.</li>
|
||||
<li>Components are included in templates using kebab-case name of the component: <code>{{ "<c-my-component />"|force_escape }}</code></li>
|
||||
</c-ul>
|
||||
|
||||
<c-hr />
|
||||
|
||||
<h3 id="subfolders">Subfolders</h3>
|
||||
|
||||
<c-ul>
|
||||
<li>Components in subfolders can be called using dot notation to represent folder levels.</li>
|
||||
<li>A component in <c-highlight>sidebar/menu/link.html</c-highlight> would be included with <code>{{ "<c-sidebar.menu.link />"|force_escape }}</code></li>
|
||||
</c-ul>
|
||||
|
||||
<c-hr />
|
||||
|
||||
<h3 id="index">The index.html component</h3>
|
||||
|
||||
<p>When your component has sub-components, you can define the default component of a folder by adding an <code>index.html</code>. This helps to keep your project structure tidy and reduce additional code in the template.</p>
|
||||
|
||||
<c-snippet label="Project structure">
|
||||
{% cotton_verbatim %}{% verbatim %}
|
||||
templates/
|
||||
├── cotton/
|
||||
│ ├── card/
|
||||
│ │ ├── index.html
|
||||
│ │ ├── header.html
|
||||
{% endverbatim %}{% endcotton_verbatim %}
|
||||
</c-snippet>
|
||||
|
||||
<c-snippet label="Usage">
|
||||
{% cotton_verbatim %}{% verbatim %}
|
||||
<c-card>
|
||||
<c-card.header>...</c-card.header>
|
||||
</c-card>
|
||||
{% endverbatim %}{% endcotton_verbatim %}
|
||||
</c-snippet>
|
||||
|
||||
<c-hr />
|
||||
|
||||
<h3 id="tag-style">Tag Style</h3>
|
||||
<c-ul>
|
||||
<li>Components can either be self-closing <code>{{ "<c-my-component />"|force_escape }}</code> or have a closing tag <code>{{ "<c-my-component></c-my-component>"|force_escape }}</code></li>
|
||||
</c-ul>
|
||||
|
||||
|
||||
<c-navigation>
|
||||
<c-slot name="prev">
|
||||
<a href="{% url 'components' %}">Components</a>
|
||||
</c-slot>
|
||||
<c-slot name="next">
|
||||
<a href="{% url 'form-fields' %}">Form Inputs</a>
|
||||
</c-slot>
|
||||
</c-navigation>
|
||||
|
||||
</c-layouts.with-sidebar>
|
|
@ -10,8 +10,9 @@ urlpatterns = [
|
|||
name="home",
|
||||
),
|
||||
path("docs/quickstart", views.build_view("quickstart"), name="quickstart"),
|
||||
path("docs/installation", views.build_view("installation"), name="installation"),
|
||||
path("docs/usage", views.build_view("usage"), name="usage"),
|
||||
path(
|
||||
"docs/usage-patterns", views.build_view("usage_patterns"), name="usage-patterns"
|
||||
),
|
||||
# Features
|
||||
path("docs/components", views.build_view("components"), name="components"),
|
||||
path("docs/slots", views.build_view("slots"), name="slots"),
|
||||
|
|
|
@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|||
|
||||
[tool.poetry]
|
||||
name = "django-cotton"
|
||||
version = "1.2.1"
|
||||
version = "1.6.0"
|
||||
description = "Bringing component based design to Django templates."
|
||||
authors = [ "Will Abbott <willabb83@gmail.com>",]
|
||||
license = "MIT"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue