Build reusable components in Django without writing a single line of Python.
{% #quote%}{%quote_photosrc="/project-hail-mary.jpg"%}{% #quote_text%}
“I penetrated the outer cell membrane with a nanosyringe."
"You poked it with a stick?"
"No!" I said. "Well. Yes. But it was a scientific poke
with a very scientific stick.”
{% /quote_text%}{% #quote_attribution%}
Andy Weir, Project Hail Mary
{% /quote_attribution%}{% /quote%}
What is Slippers?
The Django Template Language is awesome. It's fast, rich in features, and overall pretty great to work with.
Slippers aims to augment DTL, adding just enough functionality to make building interfaces just that bit more comfortable.
It includes additional template tags and filters, but its headline feature is reusable components.
{% #buttonvariant="primary"%}See how it works{% /button%}
The nodelist being defined as None for inline components is causing some incompatibility issues with other libraries, e.g. django-render-block, because they can't iterate over None. I think the value set should be an empty list or NodeList.
raise ImproperlyConfigured(context_processor_error_msg % "querystring")
django.core.exceptions.ImproperlyConfigured: Tag {% querystring %} requires django.template.context_processors.request to be in the template configuration in settings.TEMPLATES[]OPTIONS.context_processors) in order for the included template tags to function correctly
It seems that some context processors are not passed, or not yet passed?
Any idea how to fix this?
I discovered your project through a comment on HackerNews - and it got me curious. The idea looks really nice as I often struggle with managing my components in Django projects, so this fits the bill nicely.
I'm posting more as a discussion / idea issue rather than actual problem.
Have you thought about any way on how could it be possible to make discoverability of component parameters better?
Card accepts in this case children (that will be embedded) and parameter heading. But let's imagine a case where I've component consisting of 10 different parameters (and let's assume this is a must, and it's greatly reusable for my imaginary project). This can be for example parameter color that must be css-compatible color name, or size that must be either small, medium or large
Is there any way to make discoverability of parameters and their values better? Right now you have to go to the source code of the component and find your way through the HTML and discover every and each parameter and judge from the code if its required/obligatory or not and what is it even accepting.
Traceback (most recent call last):
File "C:\Users\Sharad Bhandari\Downloads\success_business_9aug\.venv\Lib\site-packages\django\template\base.py", line 505, in parse
compile_func = self.tags[command]
~~~~~~~~~^^^^^^^^^
KeyError: '#footer_card'
During handling of the above exception, another exception occurred:
...
django.template.exceptions.TemplateSyntaxError: Invalid block tag on line 5: '#footer_card'. Did you forget to register or load this tag?
[13/Aug/2023 00:05:55] "GET /htmx/footer/ HTTP/1.1" 500 160659
Imagine I pass a prop to a component, and that component just passes the prop through to another component. The inner component has a default specified. The outer component doesn't specify any defaults.
In this case, I would expect that, when I don't pass the prop to the outer component, then the inner component should render the default value, because it shouldn't be passed anything from its parent.
However, what I observe is that the inner component receives an empty string, and hence doesn't use the default value.
I've created a component named 'disqus' which accepts two parameters, namely 'url' and 'identifier'.
These paramters are dynamic in nature and value of the same will arrive from request object inside a django template.
I'm using the following code to execute the component but it does not parse the value of the variables request.build_absolute_uri and request.resolver_match.url_name. It just sends the plain string as the value, instead of getting actual dynamic value from django.
Following is the html code i've setup as the disqus component:
<div id="disqus_thread"></div>
<script>
/**
* RECOMMENDED CONFIGURATION VARIABLES: EDIT AND UNCOMMENT THE SECTION BELOW TO INSERT DYNAMIC VALUES FROM YOUR PLATFORM OR CMS.
* LEARN WHY DEFINING THESE VARIABLES IS IMPORTANT: https://disqus.com/admin/universalcode/#configuration-variables */
var disqus_config = function () {
this.page.url = '{{ url }}';
this.page.identifier = '{{ identifier }}';
};
(function() { // DON'T EDIT BELOW THIS LINE
var d = document, s = d.createElement('script');
s.src = 'https://{{ global_preferences.disqus__site_id }}.disqus.com/embed.js';
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})();
</script>
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
htmx attributes are represented in templates tags like other attributes.
potential caveat : they are defined with a hyphen hx-get , hx-target, hx-swap ...
this enhancement could also increase compatibility with alpine, hyperscript and other frameworks that make use of custom HTML attributes, and attrs that are defined with hyphens such as aria-label
This issue proposes changing the suggested name format of components to TitleCase from snake_case to improve readability by being distinct from normal template tags, keyword arguments, and variables.
{% #Quote%}{%QuotePhotosrc="/project-hail-mary.jpg"%}{% #QuoteText%}
“I penetrated the outer cell membrane with a nanosyringe."
"You poked it with a stick?"
"No!" I said. "Well. Yes. But it was a scientific poke
with a very scientific stick.”
{% /QuoteText%}{% #QuoteAttribution%}
Andy Weir, Project Hail Mary
{% /QuoteAttribution%}{% /Quote%}
I'm considering passing the request object from the parent context to the component transparently if it's available. My reasoning is that a bunch of existing template tags require the request object to function.
Hey there! Thanks so much for this library it has been a huge help in being able to clean up a lot of my templates.
I have what might be a pretty specific issue and there might be a way to solve it already I'm just not sure.
Say I have a relatively simple 'button' component that mostly just abstracts away the styling of the component.
{% var type=type|default:"button" %}
<button type={{ type }} class="p-4 bg-blue-400 rounded-md">{{ children }}</button>
This works great for being able to create a styled simple button that can optionally be turned into a 'submit' button for a form.
The use case where I am running into issues though is that I now need buttons with arbitrary data attributes specific to the javascript functionality I'm wiring up on some pages. This doesn't really play well with the current component system as the button component currently needs to be aware of all possible attributes passed to it. But I might have one page that needs an attribute data-refresh-trigger and another one that is data-delete-widget etc... it doesn't make sense that I would need to bloat the actual button component with logic to potentially add all of these arbitrary attributes.
Is it possible then to pass arbitrary attributes to a component and have them 'spread' into an element similar to python's **kwargs? If not I would be happy to discuss the appetite and possibility of including this and would be happy to explore and open a PR if that would be welcome.
My initial thought for how this feature would look from the consumer side is something like...
# component
<button type={{ type }} {{ attrs|spread }}>{{ children }}</button>
# template
{% #button type="submit" attrs="data-validate=true:data-refresh" %}Save and Refresh{% /button %}
# output
<button type="submit" data-validate="true" data-refresh>Save and Refresh</button>
Exceptions are raised if settings.DEBUG is True. This is OK, but to be more inline with Django's recommendations, we should check the value of debug in the template engine's settings instead.
Using slippers 0.6.2 and alpine.js. I'm trying to pass @click.outside="open=true" to the component but it doesn't work. Upon testing, other attributes work e.g. name. I also tried x-on:click.outside (like in the docs) but it doesn't work.
Component nav-bottom-menu: <div {% attrs name x-on:click.outside %}></div>
Using the component: {% nav-bottom-menu name="test" x-on:click.outside="open=true" %}
HTML Output: <div name="test"></div>
Is this a bug or intended behaviour? I'm new to slippers and alpine.js so I'm open to suggestions if there are other ways to achieve what I'm trying to do.
TLDR: Using Slippers and the component frontmatter, I was able to make a setup that mimics event handling - where parent component can decide how to handle child's events.
Event handling in Django templates sounds like a strange concept, because event handing happens on client-side, while the templates are rendered server-side.
However, in our case we're using Alpine.js, so we inline JS event handlers into the HTML. Hence, with this Slippers event handling feature, we can write components that handle client-side events in a decoupled manner similar to the likes of React or Vue.
Demo
In this demo, the event handler (the JS snippet that opens the alert) was defined in the parent component. Parent's event handler was able to access the child's event argument (the item value).
Screen.Recording.2023-12-01.at.19.11.04.mov
How it works
At the end of the day it's just dependency injection - child delegates the rendering of some part of the component to an "Events" class provided by the parent.
However, normally, parent can pass down only static data. Because we can use Python in the frontmatter, we can pass down an object with methods, and the child is able to call and pass arbitrary data to those methods!
Proof of concept
Consists of 6 parts:
1. Child component (example_child.html)
It doesn't do much, just renders items. However, notice the menu_item.item_attrs in the middle, becuase this is where we pass in the event handling.
Note that I've used Slippers frontmatter to define logic that is run with each rendering of the component. I've split HTML and Python for readability.
I use a class inheriting from ComponentABC to define the Slipper component sections - types, defaults, events, and setup.
- We'll get to ComponentABC later.
- types and defaults are from Slippers' Props class available in the frontmatter.
- setup is a callback called at the end of the frontmatter after all the rest has been prepared.
- Hence, types, defaults and setup are just sugar on top of Slippers frontmatter. Only events is really a new thing here.
The events used by ExampleChild are defined on ExampleChildEvents class.
We "emit" an event by calling events.on_item_delete(item) towards the end in the setup method.
- Again, what really happens is that events.on_item_delete(item) returns a string that defines client-side (JS) event handling.
Here we import the child component, and pass down event handlers via events prop.
You may have noticed we pass down the events prop, but we didn't define it on ExampleChild.types. This is because events prop is automatically generated and populated from the ExampleChild.events attribute because it inherits from ComponentABC.
---
from myapp.templates.components.example_parent import ExampleParent
ExampleParent(props)
---
{%example_childitems=itemsevents=child_events%}
4. Parent component logic (example_parent.py)
This is where we plug into child's events to
Print to console
Define custom client-side event handler logic using event's data
fromtypingimportAny, TypeVar, GenericfromabcimportABC, abstractmethodfromslippers.propsimportPropsclassBaseEventHandler(ABC):
"""Base class for the component event handlers"""passT=TypeVar('T', bound="BaseEventHandler")
defapply_component_defaults(props_obj: Props):
"""Apply defaults also for empty strings, not just None"""forkey, valinprops_obj.items():
# Ignore if no default is defined for this keyifkeynotinprops_obj.defaults:
continueifvalisNoneorval=="":
props_obj[key] =props_obj.defaults[key]
classComponentABC(ABC, Generic[T]):
types: dict[str, type[Any]] =Nonedefaults: dict[str, Any] =Noneevents: type[T] |None=Nonedef__init__(self, props: Props[T]) ->None:
super().__init__()
# Apply defaultsself.types=self.typesor {}
self.defaults=self.defaultsor {}
# Populate props objectprops.types=self.typesprops.defaults=self.defaultsprops.types['events'] =self.eventsprops.defaults['events'] =self.events() ifself.eventselseNoneapply_component_defaults(props)
# Replace class with instance, so in `setup`, we can do `self.events.callback()`self.events=props['events']
self.setup(props, self.events)
@abstractmethoddefsetup(self, props: Props[T], events: T) ->None:
...
Knowing how the ComponentABC looks like, we can now go back to ExampleChild component class. Notice that:
events is first defined as a class instead of instance, so that we can pass the class to props.types
At instantiation, the Events class is instiantiated too, and assigned to self.events
Also at instantiation, user can provide their own instance of Events class via events prop (props['events']). This is how user can plug into the child's events.
Next steps
I'd like to hear your feedback for this feature. The ideal outcome for me would be to get the ComponentABC (together with the "events" feature) into Slippers package, along with proper documentation. I'm happy to work on those.
Further thoughts
Furthermore, you can see that in the components' frontmatter, I'm explicitly passing the Props object to the component class, e.g.:
ExampleChild(props)
This has to be done because I'm interacting with Slippers from the outside. If the ComponentABC was integrated, it could possibly have a different interface, e.g. in the frontmatter, we could do:
setup_component(ExampleChild)
Where setup_component would be a function automatically imported into the scope (like with the typing lib). and setup_component could look like this behind the scenes:
... Templates [...] are built and then included wherever they are needed.
{% url "project:add_data" as add_data_url %}
{% include "patterns/molecules/button/button.html" with label="Add data" href=add_data_url %}
And then it goes on to say:
As you can see, the syntax for this is quite verbose.
It would be great to see this exact example written in slippers. Perhaps I'm missing something really trivial, but I cannot see how the slippers code is much less verbose. Wouldn't it look like this?
{% url "project:add_data" as add_data_url %}
{% button with label="Add data" href=add_data_url %}
I can see how you don't have to type the full path to the partial, but everything else is more or less the same, which doesn't seem to support the verbosity claim very well. Am I missing something?
I was excited to use it but was disappointed to see my IDE (IntelliJ) refusing to accept the newly created template tags.
Prefixing any tag with a special character (besides underscores _ and dashes - ) throws out errors from the inspector.
After days of trying, I haven't found a way to disable the inspector for that specific error.
Nor could I register the symbol-containing tags by creating some kind of new rule.
It just didn't work.
I just lived with it, but after a couple of days of writing templates full of fake errors, I got so bugged out that I gave up and looked for an alternative.
Then I found django-components, which did a similar job to Slippers, but in my opinion, is way too verbose and so I came back to slippers.
Unfortunately, picking up another IDE is not an option after years of getting used to it.
So this time, I tried to fix this issue by monkey-patching the code inside templatetags/slippers.py (really ran out of options).
OPENING_SYMBOL = "__"
CLOSING_SYMBOL = "___"
# Copied from slippers source code
#
# Replaces opening / closing symbols for tags
# #card and /card --> __card and ___card
#
# Fixes IntelliJ IDE template inspector throwing errors
# Component tags
def create_component_tag(template_path):
def do_component(parser, token):
tag_name, *remaining_bits = token.split_contents()
# Block components start with OPENING_SYMBOL
# Expect a closing tag
if tag_name.startswith(OPENING_SYMBOL):
nodelist = parser.parse((f"{CLOSING_SYMBOL}{tag_name[len(CLOSING_SYMBOL):]}",))
parser.delete_first_token()
else:
nodelist = None
For motives unbeknownst to me, the template tags won't be registered when applying this monkey patch, but I have not given up on this solution yet and would come up with a pull request once everything is working well.
Any chance of adding the option to change the template tag prefixes (possibly using a settings.py variable)?
Massive respect for creating this library,
Chris, a slippers fan :)
Currently context variables are not passed down to the componenets, which is really a pain when using components extensively.
I have to repeat the context variables multiple times like this: {% header_title_component count=object_list.count tab_name=tab_name title=title %}
It's redundant and error prone, and also a bit against the whole concept of using components.
Components should allow us to write less code, by having to repeat these variables i feel like we are going against that concept.
Clearly the intent is for the h1 to have id="bar", but it's not happening, seemingly because the component context isn't available when rendering the children. Is there some way to pass this context that I'm missing? To be clear, I mean just the context passed to the component - id and labelid.
Note that since blocks must have unique names, I'm enable to directly repeat that content despite wanting the exact same markup.
I would LOVE to be able to use fragments outside of a block, like so:
// some-page.html
{% extends 'base.html' %}
{% fragment as menu %}
<ul><li><ahref="#">My DRY Menu</a></li></ul>
{% endfragment %}
{% block menu_mobile %}{{ menu }}{% endblock %}
{% block menu_desktop %}{{ menu }}{% endblock %}
{% block content %}
My content here
{% endblock %}
Unfortunately this isn't working in my experimentation. I'm guessing this is a scoping issue with block, but figured it was worth confirming whether this is possible.
I really liked this component approach that slippers does, an idea came to me for a component to have more than one child.
Do you think this would have adoption in the lib? I can think of some ideas for that.