Implement single file components

This commit is contained in:
Dylan Castillo 2024-01-13 17:19:33 +01:00 committed by Emil Stenström
parent 27521a5402
commit 70a2a01400
5 changed files with 238 additions and 24 deletions

View file

@ -242,6 +242,51 @@ The output from the above template will be:
This makes it possible to organize your front-end around reusable components. Instead of relying on template tags and keeping your CSS and Javascript in the static directory.
## Using single-file components
Components can also be defined in a single file, which is useful for small components. To do this, you can use the `template`, `js`, and `css` class attributes instead of the `template_name` and `Media`. For example, here's the calendar component from above, defined in a single file:
```python
# In a file called [project root]/components/calendar.py
from django_components import component
@component.register("calendar")
class Calendar(component.Component):
def get_context_data(self, date):
return {
"date": date,
}
template = """
<!DOCTYPE html>
<html>
<head>
<title>My example calendar</title>
{% component_css_dependencies %}
</head>
<body>
{% component "calendar" date="2015-06-19" %}
{% component_js_dependencies %}
</body>
<html>
"""
css = """
.calendar-component { width: 200px; background: pink; }
.calendar-component span { font-weight: bold; }
"""
js = """
(function(){
if (document.querySelector(".calendar-component")) {
document.querySelector(".calendar-component").onclick = function(){ alert("Clicked calendar!"); };
}
})()
"""
```
This makes it easy to create small components without having to create a separate template, CSS, and JS file.
## Using slots in templates
_New in version 0.26_:

View file

@ -60,9 +60,12 @@ class SimplifiedInterfaceMediaDefiningClass(MediaDefiningClass):
class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
# Must be set on subclass OR subclass must implement get_template_name() with
# Either template_name or template must be set on subclass OR subclass must implement get_template() with
# non-null return.
template_name: ClassVar[str]
template_name: ClassVar[Optional[str]] = None
template: ClassVar[Optional[str]] = None
js: ClassVar[Optional[str]] = None
css: ClassVar[Optional[str]] = None
media: Media
class Media:
@ -84,41 +87,51 @@ class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
def get_context_data(self, *args, **kwargs) -> Dict[str, Any]:
return {}
# Can be overridden for dynamic templates
def get_template_name(self, context) -> str:
try:
name = self.template_name
except AttributeError:
raise ImproperlyConfigured(
f"Template name is not set for Component {type(self).__name__}. "
f"Note: this attribute is not required if you are overriding any of "
f"the class's `get_template*()` methods."
)
return name
def get_template_name(self, context) -> Optional[str]:
return self.template_name
def get_template_string(self, context) -> str:
...
def get_template_string(self, context) -> Optional[str]:
return self.template
def render_dependencies(self):
"""Helper function to access media.render()"""
return self.media.render()
"""Helper function to render all dependencies for a component."""
dependencies = []
css_deps = self.render_css_dependencies()
if css_deps:
dependencies.append(css_deps)
js_deps = self.render_js_dependencies()
if js_deps:
dependencies.append(js_deps)
return mark_safe("\n".join(dependencies))
def render_css_dependencies(self):
"""Render only CSS dependencies available in the media class."""
"""Render only CSS dependencies available in the media class or provided as a string."""
if self.css is not None:
return mark_safe(f"<style>{self.css}</style>")
return mark_safe("\n".join(self.media.render_css()))
def render_js_dependencies(self):
"""Render only JS dependencies available in the media class."""
"""Render only JS dependencies available in the media class or provided as a string."""
if self.js is not None:
return mark_safe(f"<script>{self.js}</script>")
return mark_safe("\n".join(self.media.render_js()))
def get_template(self, context) -> Template:
template_string = self.get_template_string(context)
if template_string is not None:
return Template(template_string)
else:
template_name = self.get_template_name(context)
template: Template = get_template(template_name).template
return template
template_name = self.get_template_name(context)
if template_name is not None:
return get_template(template_name).template
raise ImproperlyConfigured(
f"Either 'template_name' or 'template' must be set for Component {type(self).__name__}."
f"Note: this attribute is not required if you are overriding the class's `get_template*()` methods."
)
def render(self, context):
template = self.get_template(context)

View file

@ -6,6 +6,7 @@
</head>
<body>
{% component "calendar" date=date %}
{% component "greeting" greet='Hello world' %}
{% component_js_dependencies %}
</body>
</html>

View file

@ -0,0 +1,25 @@
from django_components import component
@component.register("greeting")
class greeting(component.Component):
def get_context_data(self, greet, *args, **kwargs):
return {"greet": greet}
template = """
<div id="greeting">{{ greet }}</div>
"""
css = """
#greeting {
display: inline-block;
color: blue;
font-size: 2em;
}
"""
js = """
document.getElementById("greeting").addEventListener("click", (event) => {
alert("Hello!");
});
"""

View file

@ -18,7 +18,7 @@ class ComponentTest(SimpleTestCase):
pass
with self.assertRaises(ImproperlyConfigured):
EmptyComponent("empty_component").get_template_name(Context({}))
EmptyComponent("empty_component").get_template(Context({}))
def test_simple_component(self):
class SimpleComponent(component.Component):
@ -185,6 +185,136 @@ class ComponentTest(SimpleTestCase):
)
class InlineComponentTest(SimpleTestCase):
def test_inline_html_component(self):
class InlineHTMLComponent(component.Component):
template = "<div class='inline'>Hello Inline</div>"
comp = InlineHTMLComponent("inline_html_component")
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='inline'>Hello Inline</div>",
)
def test_html_and_css_only(self):
class HTMLCSSComponent(component.Component):
template = "<div class='html-css-only'>Content</div>"
css = ".html-css-only { color: blue; }"
comp = HTMLCSSComponent("html_css_component")
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='html-css-only'>Content</div>",
)
self.assertHTMLEqual(
comp.render_css_dependencies(),
"<style>.html-css-only { color: blue; }</style>",
)
def test_html_and_js_only(self):
class HTMLJSComponent(component.Component):
template = "<div class='html-js-only'>Content</div>"
js = "console.log('HTML and JS only');"
comp = HTMLJSComponent("html_js_component")
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='html-js-only'>Content</div>",
)
self.assertHTMLEqual(
comp.render_js_dependencies(),
"<script>console.log('HTML and JS only');</script>",
)
def test_html_string_with_css_js_files(self):
class HTMLStringFileCSSJSComponent(component.Component):
template = "<div class='html-string-file'>Content</div>"
class Media:
css = "path/to/style.css"
js = "path/to/script.js"
comp = HTMLStringFileCSSJSComponent(
"html_string_file_css_js_component"
)
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='html-string-file'>Content</div>",
)
self.assertHTMLEqual(
comp.render_dependencies(),
dedent(
"""\
<link href="path/to/style.css" media="all" rel="stylesheet">
<script src="path/to/script.js"></script>
"""
),
)
def test_html_js_string_with_css_file(self):
class HTMLStringFileCSSJSComponent(component.Component):
template = "<div class='html-string-file'>Content</div>"
js = "console.log('HTML and JS only');"
class Media:
css = "path/to/style.css"
comp = HTMLStringFileCSSJSComponent(
"html_string_file_css_js_component"
)
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='html-string-file'>Content</div>",
)
self.assertHTMLEqual(
comp.render_dependencies(),
dedent(
"""\
<link href="path/to/style.css" media="all" rel="stylesheet">
<script>console.log('HTML and JS only');</script>
"""
),
)
def test_html_css_string_with_js_file(self):
class HTMLStringFileCSSJSComponent(component.Component):
template = "<div class='html-string-file'>Content</div>"
css = ".html-string-file { color: blue; }"
class Media:
js = "path/to/script.js"
comp = HTMLStringFileCSSJSComponent(
"html_string_file_css_js_component"
)
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='html-string-file'>Content</div>",
)
self.assertHTMLEqual(
comp.render_dependencies(),
dedent(
"""\
<style>.html-string-file { color: blue; }</style><script src="path/to/script.js"></script>
"""
),
)
def test_component_with_variable_in_html(self):
class VariableHTMLComponent(component.Component):
def get_template(self, context):
return Template(
"<div class='variable-html'>{{ variable }}</div>"
)
comp = VariableHTMLComponent("variable_html_component")
context = Context({"variable": "Dynamic Content"})
self.assertHTMLEqual(
comp.render(context),
"<div class='variable-html'>Dynamic Content</div>",
)
class ComponentMediaTests(SimpleTestCase):
def test_component_media_with_strings(self):
class SimpleComponent(component.Component):