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.
|
||||
|
||||
## 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_:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
</head>
|
||||
<body>
|
||||
{% component "calendar" date=date %}
|
||||
{% component "greeting" greet='Hello world' %}
|
||||
{% component_js_dependencies %}
|
||||
</body>
|
||||
</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
|
||||
|
||||
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):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue