Package: django-web-components
django-web-componentsA simple way to create reusable template components in Django.ExampleYou have to first register your componentfromdjango_web_componentsimportcomponent@component.register("card")classCard(component.Component):template_name="components/card.html"The component's template:# components/card.html

{% load components %}<divclass="card"><divclass="card-header">{% render_slot slots.header %}</div><divclass="card-body"><h5class="card-title">{% render_slot slots.title %}</h5>{% render_slot slots.inner_block %}</div></div>You can now render this component with:{% load components %}

{% card %}
  {% slot header %} Featured {% endslot %}
  {% slot title %} Card title {% endslot %}<p>Some quick example text to build on the card title and make up the bulk of the card's content.</p><ahref="#"class="btn btn-primary">Go somewhere</a>{% endcard %}Which will result in the following HTML being rendered:<divclass="card"><divclass="card-header">Featured</div><divclass="card-body"><h5class="card-title">Card title</h5><p>Some quick example text to build on the card title and make up the bulk of the card's content.</p><ahref="#"class="btn btn-primary">Go somewhere</a></div></div>Installationpip install django-web-componentsThen adddjango_web_componentsto yourINSTALLED_APPS.INSTALLED_APPS=[...,"django_web_components",]OptionalTo avoid having to use{% load components %}in each template, you may add the tags to thebuiltinslist inside your
settings.TEMPLATES=[{...,"OPTIONS":{"context_processors":[...],"builtins":["django_web_components.templatetags.components",],},},]Python / Django compatibilityThe library supports Python 3.8+ and Django 3.2+.Python versionDjango version3.114.13.104.1,4.0,3.23.94.1,4.0,3.23.84.1,4.0,3.2ComponentsThere are two approaches to writing components: class based components and function based components.Class based componentsfromdjango_web_componentsimportcomponent@component.register("alert")classAlert(component.Component):# You may also override the get_template_name() method insteadtemplate_name="components/alert.html"# Extra context data will be passed to the template contextdefget_context_data(self,**kwargs)->dict:return{"dismissible":False,}The component will be rendered by calling therender(context)method, which by default will load the template file and render it.For tiny components, it may feel cumbersome to manage both the component class and the component's template. For this reason, you may define the template directly from therendermethod:fromdjango_web_componentsimportcomponentfromdjango_web_components.templateimportCachedTemplate@component.register("alert")classAlert(component.Component):defrender(self,context)->str:returnCachedTemplate("""<div class="alert alert-primary" role="alert">{% render_slot slots.inner_block %}</div>""",name="alert",).render(context)Function based componentsA component may also be defined as a single function that accepts acontextand returns a string:fromdjango_web_componentsimportcomponentfromdjango_web_components.templateimportCachedTemplate@component.registerdefalert(context):returnCachedTemplate("""<div class="alert alert-primary" role="alert">{% render_slot slots.inner_block %}</div>""",name="alert",).render(context)The examples in this guide will mostly use function based components, since it's easier to exemplify as the component code and template are in the same place, but you are free to choose whichever method you prefer.Template files vs template stringsThe library uses the regular Django templates, which allows you to eitherload templates from files, or createTemplate objectsdirectly using template strings. Both methods are supported, and both have advantages and disadvantages:Template filesYou get formatting support and syntax highlighting from your editorYou getcachingby defaultHarder to manage / reason about since your code is split from the templateTemplate stringsEasier to manage / reason about since your component's code and template are in the same placeYou lose formatting support and syntax highlighting since the template is just a stringYou lose cachingRegarding caching, the library provides aCachedTemplate, which will cache and reuse theTemplateobject as long as you provide anamefor it, which will be used as the cache key:fromdjango_web_componentsimportcomponentfromdjango_web_components.templateimportCachedTemplate@component.registerdefalert(context):returnCachedTemplate("""<div class="alert alert-primary" role="alert">{% render_slot slots.inner_block %}</div>""",name="alert",).render(context)So in reality, the caching should not be an issue when using template strings, sinceCachedTemplateis just as fast as using the cached loader with template files.Regarding formatting support and syntax highlighting, there is no good solution for template strings. PyCharm supportslanguage injectionwhich allows you to add a# language=htmlcomment before the template string and get syntax highlighting, however, it only highlights HTML and not the Django tags, and you are still missing support for formatting. Maybe the editors will add better support for this in the future, but for the moment you will be missing syntax highlighting and formatting if you go this route. There is anopen conversationabout this on thedjango-componentsrepo, credits toEmilStenstromfor moving the conversation forward with the VSCode team.In the end, it's a tradeoff. Use the method that makes the most sense for you.Registering your componentsJust like signals, the components can live anywhere, but you need to make sure Django picks them up on startup. The easiest way to do this is to define your components in acomponents.pysubmodule of the application they relate to, and then connect them inside theready()method of your application configuration class.fromdjango.appsimportAppConfigfromdjango_web_componentsimportcomponentclassMyAppConfig(AppConfig):...defready(self):# Implicitly register components decorated with @component.registerfrom.importcomponents# noqa# OR explicitly register a componentcomponent.register("card",components.Card)You may alsounregisteran existing component, or get a component from the registry:fromdjango_web_componentsimportcomponent# Unregister a componentcomponent.registry.unregister("card")# Get a componentcomponent.registry.get("card")# Remove all componentscomponent.registry.clear()# Get all components as a dict of name: componentcomponent.registry.all()Rendering componentsEach registered component will have two tags available for use in your templates:A block tag, e.g.{% card %} ... {% endcard %}An inline tag, e.g.{% #user_profile %}. This can be useful for components that don't necessarily require a bodyComponent tag formatterBy default, components will be registered using the following tags:Block start tag:{% <component_name> %}Block end tag:{% end<component_name> %}Inline tag:{% #<component_name> %}This behavior may be changed by providing a custom tag formatter in your settings. For example, to change the block tags to{% #card %} ... {% /card %}, and the inline tag to{% card %}(similar toslippers), you can use the following formatter:classComponentTagFormatter:defformat_block_start_tag(self,name):returnf"#{name}"defformat_block_end_tag(self,name):returnf"/{name}"defformat_inline_tag(self,name):returnname# inside your settingsWEB_COMPONENTS={"DEFAULT_COMPONENT_TAG_FORMATTER":"path.to.your.ComponentTagFormatter",}Passing data to componentsYou may pass data to components using keyword arguments, which accept either hardcoded values or variables:{% with error_message="Something bad happened!" %}
    {% #alert type="error" message=error_message %}
{% endwith %}All attributes will be added in anattributesdict which will be available in the template context:{"attributes":{"type":"error","message":"Something bad happened!"}}You can then access it from your component's template:<divclass="alert alert-{{ attributes.type }}">{{ attributes.message }}</div>Rendering all attributesYou may also render all attributes directly using{{ attributes }}. For example, if you have the following component{% alert id="alerts" class="font-bold" %} ... {% endalert %}You may render all attributes using<div{{attributes}}><!-- Component content --></div>Which will result in the following HTML being rendered:<divid="alerts"class="font-bold"><!-- Component content --></div>Attributes with special charactersYou can also pass attributes with special characters ([@:_-.]), as well as attributes with no value:{% button @click="handleClick" data-id="123" required %} ... {% endbutton %}Which will result in the follow dict available in the context:{"attributes":{"@click":"handleClick","data-id":"123","required":True,}}And will be rendered by{{ attributes }}as@click="handleClick" data-id="123" required.Default / merged attributesSometimes you may need to specify default values for attributes, or merge additional values into some of the component's attributes. The library provides amerge_attrstag that helps with this:<div{%merge_attrsattributesclass="alert"role="alert"%}><!-- Component content --></div>If we assume this component is utilized like so:{% alert class="mb-4" %} ... {% endalert %}The final rendered HTML of the component will appear like the following:<divclass="alert mb-4"role="alert"><!-- Component content --></div>Non-class attribute mergingWhen merging attributes that are notclassattributes, the values provided to themerge_attrstag will be considered the "default" values of the attribute. However, unlike theclassattribute, these attributes will not be merged with injected attribute values. Instead, they will be overwritten. For example, abuttoncomponent's implementation may look like the following:<button{%merge_attrsattributestype="button"%}>{% render_slot slots.inner_block %}</button>To render the button component with a customtype, it may be specified when consuming the component. If no type is specified, thebuttontype will be used:{% button type="submit" %} Submit {% endbutton %}The rendered HTML of thebuttoncomponent in this example would be:<buttontype="submit">Submit</button>Appendable attributesYou may also treat other attributes as "appendable" by using the+=operator:<div{%merge_attrsattributesdata-value+="some-value"%}><!-- Component content --></div>If we assume this component is utilized like so:{% alert data-value="foo" %} ... {% endalert %}The rendered HTML will be:<divdata-value="foo some-value"><!-- Component content --></div>Manipulating the attributesBy default, all attributes are added to anattributesdict inside the context. However, this may not always be what we want. For example, imagine we want to have analertcomponent that can be dismissed, while at the same time being able to pass extra attributes to the root element, like anidorclass. Ideally we would want to be able to render a component like this:{% alert id="alerts" dismissible %} Something went wrong! {% endalert %}A naive way to implement this component would be something like the following:<div{{attributes}}>{% render_slot slots.inner_block %}

    {% if attributes.dismissible %}<buttontype="button"class="btn-close"data-bs-dismiss="alert"aria-label="Close"></button>{% endif %}</div>However, this would result in thedismissibleattribute being included in the root element, which is not what we want:<divid="alerts"dismissible>Something went wrong!<buttontype="button"class="btn-close"data-bs-dismiss="alert"aria-label="Close"></button></div>Ideally we would want thedismissibleattribute to be separated from theattributessince we only want to use it in logic, but not necessarily render it to the component.To achieve this, you can manipulate the context from your component in order to provide a better API for using the components. There are several ways to do this, choose the method that makes the most sense to you, for example:You can overrideget_context_dataand remove thedismissibleattribute fromattributesand return it in the context insteadfromdjango_web_componentsimportcomponent@component.register("alert")classAlert(component.Component):template_name="components/alert.html"defget_context_data(self,**kwargs):dismissible=self.attributes.pop("dismissible",False)return{"dismissible":dismissible,}You can override therendermethod and manipulate the context therefromdjango_web_componentsimportcomponent@component.register("alert")classAlert(component.Component):template_name="components/alert.html"defrender(self,context):context["dismissible"]=context["attributes"].pop("dismissible",False)returnsuper().render(context)Both of the above solutions will work, and you can do the same for function based components. The component's template can then look like this:<div{{attributes}}>{% render_slot slots.inner_block %}

    {% if dismissible %}<buttontype="button"class="btn-close"data-bs-dismiss="alert"aria-label="Close"></button>{% endif %}</div>Which should result in the correct HTML being rendered:<divid="alerts">Something went wrong!<buttontype="button"class="btn-close"data-bs-dismiss="alert"aria-label="Close"></button></div>SlotsYou will often need to pass additional content to your components via "slots". Aslotscontext variable is passed to your components, which consists of a dict with the slot name as the key and the slot as the value. You may then render the slots inside your components using therender_slottag.The default slotTo explore this concept, let's imagine we want to pass some content to analertcomponent:{% alert %}<strong>Whoops!</strong>Something went wrong!
{% endalert %}By default, that content will be made available to your component in the default slot which is calledinner_block. You can then render this slot using therender_slottag inside your component:{% load components %}<divclass="alert alert-danger">{% render_slot slots.inner_block %}</div>Which should result in the following HTML being rendered:<divclass="alert alert-danger"><strong>Whoops!</strong>Something went wrong!</div>You may also rename the default slot by specifying it in your settings:# inside your settingsWEB_COMPONENTS={"DEFAULT_SLOT_NAME":"inner_block",}Named slotsSometimes a component may need to render multiple different slots in different locations within the component. Let's modify our alert component to allow for the injection of a "title" slot:{% load components %}<divclass="alert alert-danger"><spanclass="alert-title">{% render_slot slots.title %}</span>{% render_slot slots.inner_block %}</div>You may define the content of the named slot using theslottag. Any content not within an explicitslottag will be added to the defaultinner_blockslot:{% load components %}
{% alert %}
    {% slot title %} Server error {% endslot %}<strong>Whoops!</strong>Something went wrong!
{% endalert %}The rendered HTML in this example would be:<divclass="alert alert-danger"><spanclass="alert-title">Server error</span><strong>Whoops!</strong>Something went wrong!</div>Duplicate named slotsYou may define the same named slot multiple times:{% unordered_list %}
  {% slot item %} First item {% endslot %}
  {% slot item %} Second item {% endslot %}
  {% slot item %} Third item {% endslot %}
{% endunordered_list %}You can then iterate over the slot inside your component:<ul>{% for item in slots.item %}<li>{% render_slot item %}</li>{% endfor %}</ul>Which will result in the following HTML:<ul><li>First item</li><li>Second item</li><li>Third item</li></ul>Scoped slotsThe slot content will also have access to the component's context. To explore this concept, imagine a list component that accepts anentriesattribute representing a list of things, which it will then iterate over and render theinner_blockslot for each entry.fromdjango_web_componentsimportcomponentfromdjango_web_components.templateimportCachedTemplate@component.registerdefunordered_list(context):context["entries"]=context["attributes"].pop("entries",[])returnCachedTemplate("""<ul>{% for entry in entries %}<li>{% render_slot slots.inner_block %}</li>{% endfor %}</ul>""",name="unordered_list",).render(context)We can then render the component as follows:{% unordered_list entries=entries %}
    I like {{ entry }}!
{% endunordered_list %}In this example, theentryvariable comes from the component's context. If we assume thatentries = ["apples", "bananas", "cherries"], the resulting HTML will be:<ul><li>I like apples!</li><li>I like bananas!</li><li>I like cherries!</li></ul>You may also explicitly pass a second argument torender_slot:<ul>{% for entry in entries %}<li><!-- We are passing the `entry` as the second argument to render_slot -->{% render_slot slots.inner_block entry %}</li>{% endfor %}</ul>When invoking the component, you can use the special attribute:letto take the value that was passed torender_slotand bind it to a variable:{% unordered_list :let="fruit" entries=entries %}
    I like {{ fruit }}!
{% endunordered_list %}This would render the same HTML as above.Slot attributesSimilar to the components, you may assign additional attributes to slots. Below is a table component illustrating multiple named slots with attributes:fromdjango_web_componentsimportcomponentfromdjango_web_components.templateimportCachedTemplate@component.registerdeftable(context):context["rows"]=context["attributes"].pop("rows",[])returnCachedTemplate("""<table><tr>{% for col in slots.column %}<th>{{ col.attributes.label }}</th>{% endfor %}</tr>{% for row in rows %}<tr>{% for col in slots.column %}<td>{% render_slot col row %}</td>{% endfor %}</tr>{% endfor %}</table>""",name="table",).render(context)You can invoke the component like so:{% table rows=rows %}
    {% slot column :let="user" label="Name" %}
        {{ user.name }}
    {% endslot %}
    {% slot column :let="user" label="Age" %}
        {{ user.age }}
    {% endslot %}
{% endtable %}If we assume thatrows = [{ "name": "Jane", "age": "34" }, { "name": "Bob", "age": "51" }], the following HTML will be rendered:<table><tr><th>Name</th><th>Age</th></tr><tr><td>Jane</td><td>34</td></tr><tr><td>Bob</td><td>51</td></tr></table>Nested componentsYou may also nest components to achieve more complicated elements. Here is an example of how you might implement anAccordion component using Bootstrap:fromdjango_web_componentsimportcomponentfromdjango_web_components.templateimportCachedTemplateimportuuid@component.registerdefaccordion(context):context["accordion_id"]=context["attributes"].pop("id",str(uuid.uuid4()))context["always_open"]=context["attributes"].pop("always_open",False)returnCachedTemplate("""<div class="accordion" id="{{ accordion_id }}">{% render_slot slots.inner_block %}</div>""",name="accordion",).render(context)@component.registerdefaccordion_item(context):context["id"]=context["attributes"].pop("id",str(uuid.uuid4()))context["open"]=context["attributes"].pop("open",False)returnCachedTemplate("""<div class="accordion-item" id="{{ id }}"><h2 class="accordion-header" id="{{ id }}-header"><buttonclass="accordion-button {% if not open %}collapsed{% endif %}"type="button"data-bs-toggle="collapse"data-bs-target="#{{ id }}-collapse"aria-expanded="{% if open %}true{% else %}false{% endif %}"aria-controls="{{ id }}-collapse">{% render_slot slots.title %}</button></h2><divid="{{ id }}-collapse"class="accordion-collapse collapse {% if open %}show{% endif %}"aria-labelledby="{{ id }}-header"{% if accordion_id and not always_open %}data-bs-parent="#{{ accordion_id }}"{% endif %}}><div class="accordion-body">{% render_slot slots.body %}</div></div></div>""",name="accordion_item",).render(context)You can then use them as follows:{% accordion %}

    {% accordion_item open %}
        {% slot title %} Accordion Item #1 {% endslot %}
        {% slot body %}<strong>This is the first item's accordion body.</strong>It is shown by default.
        {% endslot %}
    {% endaccordion_item %}

    {% accordion_item %}
        {% slot title %} Accordion Item #2 {% endslot %}
        {% slot body %}<strong>This is the second item's accordion body.</strong>It is hidden by default.
        {% endslot %}
    {% endaccordion_item %}

    {% accordion_item %}
        {% slot title %} Accordion Item #3 {% endslot %}
        {% slot body %}<strong>This is the third item's accordion body.</strong>It is hidden by default.
        {% endslot %}
    {% endaccordion_item %}

{% endaccordion %}Setup for development and running the testsThe project usespoetryto manage the dependencies. Check out the documentation on how to install poetry here:https://python-poetry.org/docs/#installationInstall the dependenciespoetryinstallActivate the environmentpoetryshellNow you can run the testspythonruntests.pyMotivation / Inspiration / ResourcesThe project came to be after seeing how other languages / frameworks deal with components, and wanting to bring some of those ideas back to Django.django-components- The existingdjango-componentslibrary is already great and supports most of the features that this project has, but I thought the syntax could be improved a bit to feel less verbose, and add a few extra things that seemed necessary, like support for function based components and template strings, and working with attributesPhoenix Components- I really liked the simplicity of Phoenix and how they deal with components, and this project is heavily inspired by it. In fact, some of the examples above are straight-up copied from there (like the table example).Laravel Blade Components- The initial implementation was actually very different and was relying on HTML parsing to turn the HTML into template Nodes, and was heavily inspired by Laravel. This had the benefit of having a nicer syntax (e.g. rendering the components looked a lot like normal HTML<x-alert type="error">Server Error</x-alert>), but the solution was a lot more complicated and I came to the conclusion that using a similar approach todjango-componentsmade a lot more sense in DjangoVue Componentsslippersdjango-widget-tweaksHow EEx Turns Your Template Into HTMLComponent librariesMany other languages / frameworks are using the same concepts for building components (slots, attributes), so a lot of the knowledge is transferable, and there is already a great deal of existing component libraries out there (e.g. using Bootstrap, Tailwind, Material design, etc.). I highly recommend looking at some of them to get inspired on how to build / structure your components. Here are some examples:https://bootstrap-vue.org/docs/components/alerthttps://coreui.io/bootstrap-vue/components/alert.htmlhttps://laravel-bootstrap-components.com/components/alertshttps://flowbite.com/docs/components/alerts/https://www.creative-tim.com/vuematerial/components/buttonhttps://phoenix-ui.fly.dev/components/alerthttps://storybook.phenixgroupe.com/components/messagehttps://surface-ui.org/samplecomponents/Buttonhttps://react-bootstrap.github.io/components/alerts/