Giter VIP home page Giter VIP logo

slippers's Introduction

Slippers


PyPI version PyPI Supported Python Versions PyPI Supported Django Versions GitHub Actions (Code quality and tests)

Build reusable components in Django without writing a single line of Python.

{% #quote %}
  {% quote_photo src="/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.

{% #button variant="primary" %}See how it works{% /button %}

See how it works

Installation

pip install slippers

Add it to your INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    'slippers',
    ...
]

Documentation

Full documentation can be found on the Slippers documentation site.

Contributors

Contributors

License

MIT

slippers's People

Contributors

adamchainz avatar jlopinto avatar jnns avatar jurooravec avatar keirwl avatar leetrout avatar mixxorz avatar mschoettle avatar topher235 avatar zerolab avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

slippers's Issues

Not working with third party libraries like django_tables2?

I have a component with a django_tables2 table in it:

components/table.html

{% load django_tables2 %}
{% render_table table %}

But this is not working as i'm having the error:

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?

Discoverability of component parameters - typed parameters

Hey,

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?

Let's use the examples from your website: https://mitchel.me/slippers/docs/getting-started/

{% #card heading="I am the heading" %}

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.

I've always thought that components in django miss some kind of typing like React's propTypes: https://www.npmjs.com/package/prop-types

Do you think slippers could be a project where this kind of typing could be useful?

I've been thinking about hacking with your project to implement such thing but wanted to hear on if you have had any vision about that :)

Invalid block tag on line 5: '#footer_card'. Did you forget to register or load this tag?

Please resolve problem with installation.

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

settings.py of project

INSTALLED_APPS = ['slippers']
TEMPLATES = [
    {
        .....
      'OPTIONS': {
                  'context_processors': [
                      ...
                  ],
                  "builtins": ["slippers.templatetags.slippers"],
              },

       ....
   }
]

Location of components.yml : /templates/components.yaml
components: footer_card: "components/footer-card.html"

Component template is inside /templates/components/footer-card.html

<div class="col-xl-4 col-lg-6 mb-4">
    <div class="card">
      <div class="card-body">
        {{children}}
      </div>
    </div>
</div>

Loading component from /templates/sections/footer-list.html

{% load slippers %}

<div class="row"  id="Footer-Area"  hx-swap-oob="true" >

    {% #footer_card %}
    <a href="{% url 'about' %}"></a><li>About Company</li>
    {% /footer_card %}
</div>

Not compatible with Django 4.1

Poetry will not accept to install Django 4.1 with slippers (any version).

$ poetry lock
Updating dependencies
Resolving dependencies... (1.5s)

  SolverProblemError

  Because no versions of slippers match <0.1.0 || >0.1.0,<0.1.1 || >0.1.1,<0.1.2 || >0.1.2,<0.1.3 || >0.1.3,<0.1.4 || >0.1.4,<0.2.0 || >0.2.0,<0.3.0 || >0.3.0,<0.3.1 || >0.3.1
   and slippers (0.1.0) depends on Django (>=3.0,<4.0), slippers (<0.1.1 || >0.1.1,<0.1.2 || >0.1.2,<0.1.3 || >0.1.3,<0.1.4 || >0.1.4,<0.2.0 || >0.2.0,<0.3.0 || >0.3.0,<0.3.1 || >0.3.1) requires Django (>=3.0,<4.0).
  And because slippers (0.1.1) depends on Django (>=2.2,<4.0), slippers (<0.1.2 || >0.1.2,<0.1.3 || >0.1.3,<0.1.4 || >0.1.4,<0.2.0 || >0.2.0,<0.3.0 || >0.3.0,<0.3.1 || >0.3.1) requires Django (>=2.2,<4.0).
  And because slippers (0.1.2) depends on Django (>=2.2,<4.0)
   and slippers (0.1.3) depends on Django (>=2.2,<4.0), slippers (<0.1.4 || >0.1.4,<0.2.0 || >0.2.0,<0.3.0 || >0.3.0,<0.3.1 || >0.3.1) requires Django (>=2.2,<4.0).
  And because slippers (0.1.4) depends on Django (>=2.2,<4.0)
   and slippers (0.2.0) depends on Django (>=2.2,<4.0), slippers (<0.3.0 || >0.3.0,<0.3.1 || >0.3.1) requires Django (>=2.2,<4.0).
  And because slippers (0.3.0) depends on Django (>=2.2,<4.0)
   and slippers (0.3.1) depends on Django (>=2.2,<4.1), every version of slippers requires Django (>=2.2,<4.1).
  So, because mango depends on both Django (4.1.*) and slippers (*), version solving failed.

Component autodiscovery

Maintaining a yaml file of components is a little annoying. There should be a built-in way to auto-discover components.

I have a few ideas but if you have suggestions, please share. 🙂

bug: Props drilling coerces None values to empty strings

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.

Consider this example:

page.html

<div class="layout">
	Layout!

	{% outer %}
</div>

outer.html

---
props.types = {
    'my_prop': Optional[str],
}
props.defaults = {}

print('OUTER: ', props['my_prop'])
print('OUTER_TYPE: ', type(props['my_prop']))
---

<div>
	outer!
	{% inner the_prop=my_prop %}
</div>

inner.html

---
props.types = {
    'the_prop': Optional[str],
}
props.defaults = {
	'the_prop': 'bamboozle',
}

print('INNER: ', props['the_prop'])
print('INNER_TYPE: ', type(props['the_prop']))
---

<h3>
    inner!
	{{ the_prop }}
</h3>

Since I didn't set the my_prop for outer, then I would expect that inner would render bamboozle, and hence the whole output would be:

<div class="layout">
	Layout!

	<div>
	  outer!
	  <h3>
	    inner!
	    bamboozle
	  </h3>
	</div>
</div>

However, instead the the_prop is an empty string, so I get:

<div class="layout">
	Layout!

	<div>
	  outer!
	  <h3>
	    inner!
	    
	  </h3>
	</div>
</div>

When rendered, the above prints:

OUTER:  None
OUTER_TYPE:  <class 'NoneType'>
INNER:  
INNER_TYPE:  <class 'str'>

EDIT: Added print statements.

Cannot pass current URL as parameter to the component

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.

{% disqus url="{{ request.build_absolute_uri }}" identifier='{{ request.resolver_match.url_name }}' %}

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>

Encapsulate `slippers` component with web-components?

Hi there

Is it possible to encapsulate all ( or selectively convert some ) slippers component into their standalone web-component module?

Because this way style and scripts are contained into one component thus reducing the chance of style collision ..

Thanks for reading.. Massive thanks for creating this library.

Upgrade typeguard

The current typeguard version is 4.x while it is 2.x in this package. Can typeguard be upgraded or is there a reason it is pinned to v2?

Feature request: Named Slots (Named child)

It would be great if we could have named slots. For slippers consistency, I think the best option would be something like:

<!-- card.html -->
<div class="card">
  <div class="card-title"> {{ children.title }} </div>
  <div class="card-action"> {{ children.action }} </div>
</div>
<!-- index.html -->
{% #card %}
  {% child title %}
    <span>My Title</span>
  {% endchild %}

 {% child action %}
    <button>My action</button>
  {% endchild %}
{% /card %}

It would also be possible to use only children:

<!-- card.html -->
<div class="card">
  {{ children }}
</div>

If approved, I can create a pull request for it.

Template error "Tag name expected"

Everything is working fine normally even components are working but this error is triggering me I wanted to know if this is expected behaviour

image

allow `{% attrs %}` to render hyphenated attributes ( `aria-label`, `data-script` etc)

from htmx.org

htmx gives you access to AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext

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

Happy to contribute to implementation

Add named slots

It would be really cool if you could have both named and unnamed slots, rather than children - Similar to Astro and Svelte.

Change suggested component name format to TitleCase

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 %}
  {% QuotePhoto src="/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 %}

Open to feedback on this.

[SETTINGS] builtins causing problems in /admin route.

settings.py

TEMPLATES = [
	{
		"BACKEND": "django.template.backends.django.DjangoTemplates",
		"DIRS": [BASE_DIR / "templates"],
		"APP_DIRS": True,
		"OPTIONS": {
			"context_processors": [
				"django.template.context_processors.debug",
				"django.template.context_processors.request",
				"django.contrib.auth.context_processors.auth",
				"django.contrib.messages.context_processors.messages",
				"apps.properties.context_processors.cart",
			],
			"builtins": ["slippers.templatetags.slippers"],
		},
	},
]

error

image

description

Environment:


Request Method: GET
Request URL: http://127.0.0.1:8000/admin/

Django Version: 5.0.6
Python Version: 3.12.1
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django.contrib.sites',
 'apps.core',
 'apps.accounts',
 'apps.properties',
 'slippers',
 'django_filters',
 'django_browser_reload',
 'django_extensions',
 'django_htmx',
 'anymail']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'django_browser_reload.middleware.BrowserReloadMiddleware',
 'django_htmx.middleware.HtmxMiddleware']


Template error:
In template C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\contrib\admin\templates\admin\index.html, error at line 41
   Invalid block tag on line 41: 'endfilter', expected 'elif', 'else' or 'endif'. Did you forget to register or load this tag?
   31 :             {% for entry in admin_log %}
   32 :             <li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">
   33 :                 <span class="visually-hidden">{% if entry.is_addition %}{% translate 'Added:' %}{% elif entry.is_change %}{% translate 'Changed:' %}{% elif entry.is_deletion %}{% translate 'Deleted:' %}{% endif %}</span>
   34 :                 {% if entry.is_deletion or not entry.get_admin_url %}
   35 :                     {{ entry.object_repr }}
   36 :                 {% else %}
   37 :                     <a href="{{ entry.get_admin_url }}">{{ entry.object_repr }}</a>
   38 :                 {% endif %}
   39 :                 <br>
   40 :                 {% if entry.content_type %}
   41 :                     <span class="mini quiet">{% filter capfirst %}{{ entry.content_type.name }} {% endfilter %} </span>
   42 :                 {% else %}
   43 :                     <span class="mini quiet">{% translate 'Unknown content' %}</span>
   44 :                 {% endif %}
   45 :             </li>
   46 :             {% endfor %}
   47 :             </ul>
   48 :             {% endif %}
   49 :     </div>
   50 : </div>
   51 : {% endblock %}


Traceback (most recent call last):
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\base.py", line 502, in parse
    compile_func = self.tags[command]
                   ^^^^^^^^^^^^^^^^^^

During handling of the above exception ('endfilter'), another exception occurred:
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\core\handlers\exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\core\handlers\base.py", line 220, in _get_response
    response = response.render()
               ^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\response.py", line 114, in render
    self.content = self.rendered_content
                   ^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\response.py", line 90, in rendered_content
    template = self.resolve_template(self.template_name)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\response.py", line 74, in resolve_template
    return get_template(template, using=self.using)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\loader.py", line 15, in get_template
    return engine.get_template(template_name)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\backends\django.py", line 33, in get_template
    return Template(self.engine.get_template(template_name), self)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\engine.py", line 177, in get_template
    template, origin = self.find_template(template_name)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\engine.py", line 159, in find_template
    template = loader.get_template(name, skip=skip)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\loaders\cached.py", line 57, in get_template
    template = super().get_template(template_name, skip)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\loaders\base.py", line 28, in get_template
    return Template(
           
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\base.py", line 154, in __init__
    self.nodelist = self.compile_nodelist()
                    ^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\base.py", line 196, in compile_nodelist
    return parser.parse()
           ^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\base.py", line 510, in parse
    raise self.error(token, e)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\base.py", line 508, in parse
    compiled_result = compile_func(self, token)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\loader_tags.py", line 295, in do_extends
    nodelist = parser.parse()
               ^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\base.py", line 510, in parse
    raise self.error(token, e)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\base.py", line 508, in parse
    compiled_result = compile_func(self, token)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\loader_tags.py", line 234, in do_block
    nodelist = parser.parse(("endblock",))
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\base.py", line 510, in parse
    raise self.error(token, e)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\base.py", line 508, in parse
    compiled_result = compile_func(self, token)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\defaulttags.py", line 975, in do_if
    nodelist = parser.parse(("endif",))
               ^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\base.py", line 510, in parse
    raise self.error(token, e)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\base.py", line 508, in parse
    compiled_result = compile_func(self, token)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\defaulttags.py", line 861, in do_for
    nodelist_loop = parser.parse(
                    
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\base.py", line 510, in parse
    raise self.error(token, e)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\base.py", line 508, in parse
    compiled_result = compile_func(self, token)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\defaulttags.py", line 961, in do_if
    nodelist = parser.parse(("elif", "else", "endif"))
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\base.py", line 504, in parse
    self.invalid_block_tag(token, command, parse_until)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Igor\Desktop\propertie.dev\.venv\Lib\site-packages\django\template\base.py", line 555, in invalid_block_tag
    raise self.error(
    ^

Exception Type: TemplateSyntaxError at /admin/
Exception Value: Invalid block tag on line 41: 'endfilter', expected 'elif', 'else' or 'endif'. Did you forget to register or load this tag?

pass arbitrary attributes to a component that knows how to 'spread' them into attributes?

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>

Detect whether children are defined

I wanted to build a component where the children are optional and that depending on the presence of children the output is different.

I tried to detect it via children|length but since it also contains whitespace (see example below) that did not work.

    {% #menu title="Test" %}
    {% /menu %}

In this example children|length returns a number > 0 since there is whitespace.

    {% #menu title="Test" %}{% /menu %}

Here children|length returns 0.

Is this possible at all to detect this?

{% attrs %} does not work with attributes containing the dot character `.` .

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.

Thanks.

Feature proposal: Class-based components

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
Screenshot 2023-12-01 at 19 11 53

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.

---
from myapp.templates.components.example_child import ExampleChild

ExampleChild(props)
---

<ul class="{{ class }}" {{ attrs|safe }}>
  {% for item, menu_items in render_data %}
    <li>
      {{ item }}
      <ul style="padding-left: 40px;">
        {% for menu_item in menu_items %}
          <li {{ menu_item.item_attrs|default:""|safe }}>
            {{ menu_item.value }}
          <li>
        {% endfor %}
      </ul>
    </li>
  {% endfor %}
</ul>

2. Child component logic (example_child.py)

This logic is imported into example_child.html.

Notice that:

  1. 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.
  2. The events used by ExampleChild are defined on ExampleChildEvents class.
  3. 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.
class ExampleChildEvents():
    def on_item_delete(self, item) -> str:
        return ''

  
class ExampleChild(ComponentABC[ExampleChildEvents]):
    types = {
        'items': list,
        'class': Optional[str],
        'attrs': Optional[str],
    }
    defaults = {
        'class': '',
        'attrs': '',
    }
    events = ExampleChildEvents

    def setup(self, props, events):
        items = props['items']
        menu_items_per_item = [
            [
                MenuItem(value="Edit", link="#"),
                MenuItem(value="Duplicate"),
                MenuItem(value="Delete", item_attrs=events.on_item_delete(item)),
            ]
            for item in items
        ]

        props['render_data'] = list(zip(items, menu_items_per_item))

3. Parent component (example_parent.html)

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_child items=items events=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
from myapp.templates.components.example_child import ExampleChildEvents

class ChildEvents(ExampleChildEvents):
    def on_item_delete(self, row_id):
        print('Hello from parent component!')
        return f"""
        onClick="(() => {{ 
            alert('Deleting item {row_id}!');
            return false;
        }})();"
        """


class ExampleParent(ComponentABC):
    def setup(self, props, events):
        props['items'] = ['Hello', 1, 'automaschine']
        props['child_events'] = ChildEvents()

5. components.yml

Register our components as Slippers components.

components:
  example_parent: "components/example_parent/example_parent.html"
  example_child: "components/example_child/example_child.html"

6. ComponentABC class

from typing import Any, TypeVar, Generic
from abc import ABC, abstractmethod

from slippers.props import Props

class BaseEventHandler(ABC):
  """Base class for the component event handlers"""
  pass


T = TypeVar('T', bound="BaseEventHandler")


def apply_component_defaults(props_obj: Props):
  """Apply defaults also for empty strings, not just None"""
  for key, val in props_obj.items():
      # Ignore if no default is defined for this key
      if key not in props_obj.defaults:
          continue
      if val is None or val == "":
          props_obj[key] = props_obj.defaults[key]


class ComponentABC(ABC, Generic[T]):
  types: dict[str, type[Any]] = None
  defaults: dict[str, Any] = None
  events: type[T] | None = None

  def __init__(self, props: Props[T]) -> None:
      super().__init__()
  	# Apply defaults
      self.types = self.types or {}
      self.defaults = self.defaults or {}

      # Populate props object
      props.types = self.types
      props.defaults = self.defaults

      props.types['events'] = self.events
      props.defaults['events'] = self.events() if self.events else None

      apply_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)

  @abstractmethod
  def setup(self, props: Props[T], events: T) -> None:
      ...

Knowing how the ComponentABC looks like, we can now go back to ExampleChild component class. Notice that:

  1. events is first defined as a class instead of instance, so that we can pass the class to props.types
  2. At instantiation, the Events class is instiantiated too, and assigned to self.events
  3. 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:

def setup_component(comp_class: type[ComponentABC]):
    comp_class(props)

Docs: How does slippers solve the motivating example?

In the docs I see this:

... 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?

`{% attrs %}` do not render attributes starting with a `@`

Hi,

Trying to marry Slippers with AlpineJS I found that attributes starting with @ do not render.

Component definition:

<button {% attrs type @click %}>{{ children }}</button>

Usage:

{% #button type="button" @click="open = true" %}Open something{% /button %}

Render:

<button>Open something</button>

It's not even rendering the type attribute

Non-boolean {% attrs %} which appear after boolean attributes don't get parsed/rendered correctly

This is with slippers 0.4.0.

Consider the following simple component, named test_component:

<input type="text" {% attrs required maxlength %}>

If you use it in a template like this:

{% test_component required maxlength=10 %}

...then it outputs this (note that the maxlength attribute is missing):

<input type="text" required>

But if you reverse the order of the attributes in the template:

{% test_component maxlength=10 required %}

Or if you pass an explicit value:

{% test_component required=True maxlength=10 %}

...then it works as expected:

<input type="text" required maxlength="10">

Possibility of changing the opening/closing symbols for template tags

Hello creator(s) of Slippers!

I have heard about your project from this guy on YouTube, BugBytes:
https://www.youtube.com/watch?v=oC1K8IKK3Vo

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 :)

Pass context variables down to components

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.

Passing component parameters to subcomponents and content

Consider this component (named tester):

<div {% attrs id %} aria-labelledby="{{ labelid }}">
{{ children }}
</div>

Instantiated like this:

{% #tester id="foo" labelid="bar" %}<h1 id="{{ labelid }}">My label</h1>{% /tester %}

Results in

<div id="foo" aria-labelledby="bar">
<h1 id="">My label</h1>
</div>

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.

Question: Can I use a single fragment in multiple blocks?

Firstly, thank you for your work on Slippers!

I've looked through the documentation and can't find something directly answering this question.

Assume I have a base template similar to the following:

// base.html
<div class="mobile-menu">{% block menu_mobile %}{% endblock %}</div>
<div class="wrapper">
  <div class="desktop-menu">{% block menu_desktop %}{% endblock %}</div>
  <div class="content">{% block content %}{% endblock %}</div>
</div>

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><a href="#">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.

Thanks for your time.

Components with N children

Hi

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.