mirror of
https://github.com/django-components/django-components.git
synced 2025-09-26 15:39:08 +00:00
Implement single file components
This commit is contained in:
parent
27521a5402
commit
70a2a01400
5 changed files with 238 additions and 24 deletions
45
README.md
45
README.md
|
@ -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.
|
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
|
## Using slots in templates
|
||||||
|
|
||||||
_New in version 0.26_:
|
_New in version 0.26_:
|
||||||
|
|
|
@ -60,9 +60,12 @@ class SimplifiedInterfaceMediaDefiningClass(MediaDefiningClass):
|
||||||
|
|
||||||
|
|
||||||
class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
|
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.
|
# 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
|
media: Media
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
|
@ -84,41 +87,51 @@ class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
def get_context_data(self, *args, **kwargs) -> Dict[str, Any]:
|
def get_context_data(self, *args, **kwargs) -> Dict[str, Any]:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Can be overridden for dynamic templates
|
def get_template_name(self, context) -> Optional[str]:
|
||||||
def get_template_name(self, context) -> str:
|
return self.template_name
|
||||||
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_string(self, context) -> str:
|
def get_template_string(self, context) -> Optional[str]:
|
||||||
...
|
return self.template
|
||||||
|
|
||||||
def render_dependencies(self):
|
def render_dependencies(self):
|
||||||
"""Helper function to access media.render()"""
|
"""Helper function to render all dependencies for a component."""
|
||||||
return self.media.render()
|
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):
|
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()))
|
return mark_safe("\n".join(self.media.render_css()))
|
||||||
|
|
||||||
def render_js_dependencies(self):
|
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()))
|
return mark_safe("\n".join(self.media.render_js()))
|
||||||
|
|
||||||
def get_template(self, context) -> Template:
|
def get_template(self, context) -> Template:
|
||||||
template_string = self.get_template_string(context)
|
template_string = self.get_template_string(context)
|
||||||
if template_string is not None:
|
if template_string is not None:
|
||||||
return Template(template_string)
|
return Template(template_string)
|
||||||
else:
|
|
||||||
template_name = self.get_template_name(context)
|
template_name = self.get_template_name(context)
|
||||||
template: Template = get_template(template_name).template
|
if template_name is not None:
|
||||||
return template
|
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):
|
def render(self, context):
|
||||||
template = self.get_template(context)
|
template = self.get_template(context)
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% component "calendar" date=date %}
|
{% component "calendar" date=date %}
|
||||||
|
{% component "greeting" greet='Hello world' %}
|
||||||
{% component_js_dependencies %}
|
{% component_js_dependencies %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
25
sampleproject/components/greeting.py
Normal file
25
sampleproject/components/greeting.py
Normal 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!");
|
||||||
|
});
|
||||||
|
"""
|
|
@ -18,7 +18,7 @@ class ComponentTest(SimpleTestCase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
with self.assertRaises(ImproperlyConfigured):
|
with self.assertRaises(ImproperlyConfigured):
|
||||||
EmptyComponent("empty_component").get_template_name(Context({}))
|
EmptyComponent("empty_component").get_template(Context({}))
|
||||||
|
|
||||||
def test_simple_component(self):
|
def test_simple_component(self):
|
||||||
class SimpleComponent(component.Component):
|
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):
|
class ComponentMediaTests(SimpleTestCase):
|
||||||
def test_component_media_with_strings(self):
|
def test_component_media_with_strings(self):
|
||||||
class SimpleComponent(component.Component):
|
class SimpleComponent(component.Component):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue