Una forma simple de crear componentes de plantilla reutilizables en Django.
Primero tienes que registrar tu componente
from django_web_components import component
@ component . register ( "card" )
class Card ( component . Component ):
template_name = "components/card.html"La plantilla del componente:
# components/card.html
{% load components %}
< div class =" card " >
< div class =" card-header " >
{% render_slot slots.header %}
</ div >
< div class =" card-body " >
< h5 class =" card-title " >
{% render_slot slots.title %}
</ h5 >
{% render_slot slots.inner_block %}
</ div >
</ div >Ahora puede representar este componente con:
{% 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 >
< a href =" # " class =" btn btn-primary " > Go somewhere </ a >
{% endcard %}Que dará como resultado que se represente el siguiente HTML:
< div class =" card " >
< div class =" card-header " >
Featured
</ div >
< div class =" card-body " >
< h5 class =" 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 >
< a href =" # " class =" btn btn-primary " > Go somewhere </ a >
</ div >
</ div > pip install django-web-components
Luego agregue django_web_components a sus INSTALLED_APPS .
INSTALLED_APPS = [
...,
"django_web_components" ,
] Para evitar tener que usar {% load components %} en cada plantilla, puede agregar las etiquetas a la lista builtins dentro de su configuración.
TEMPLATES = [
{
...,
"OPTIONS" : {
"context_processors" : [
...
],
"builtins" : [
"django_web_components.templatetags.components" ,
],
},
},
]La biblioteca admite Python 3.8+ y Django 3.2+.
| Versión de Python | Versión django |
|---|---|
3.12 | 5.0 , 4.2 |
3.11 | 5.0 , 4.2 , 4.1 |
3.10 | 5.0 , 4.2 , 4.1 , 4.0 , 3.2 |
3.9 | 4.2 , 4.1 , 4.0 , 3.2 |
3.8 | 4.2 , 4.1 , 4.0 , 3.2 |
Hay dos enfoques para escribir componentes: componentes basados en clases y componentes basados en funciones.
from django_web_components import component
@ component . register ( "alert" )
class Alert ( component . Component ):
# You may also override the get_template_name() method instead
template_name = "components/alert.html"
# Extra context data will be passed to the template context
def get_context_data ( self , ** kwargs ) -> dict :
return {
"dismissible" : False ,
} El componente se representará llamando al método render(context) , que de forma predeterminada cargará el archivo de plantilla y lo representará.
Para los componentes pequeños, puede sentirse engorroso administrar tanto la clase de componentes como la plantilla del componente. Por esta razón, puede definir la plantilla directamente del método render :
from django_web_components import component
from django_web_components . template import CachedTemplate
@ component . register ( "alert" )
class Alert ( component . Component ):
def render ( self , context ) -> str :
return CachedTemplate (
"""
<div class="alert alert-primary" role="alert">
{% render_slot slots.inner_block %}
</div>
""" ,
name = "alert" ,
). render ( context ) Un componente también puede definirse como una función única que acepta un context y devuelve una cadena:
from django_web_components import component
from django_web_components . template import CachedTemplate
@ component . register
def alert ( context ):
return CachedTemplate (
"""
<div class="alert alert-primary" role="alert">
{% render_slot slots.inner_block %}
</div>
""" ,
name = "alert" ,
). render ( context )Los ejemplos en esta guía utilizarán principalmente componentes basados en funciones, ya que es más fácil ejemplificarlo ya que el código y la plantilla de componentes están en el mismo lugar, pero puede elegir el método que prefiera.
La biblioteca utiliza las plantillas regulares de Django, que le permite cargar plantillas de archivos o crear objetos de plantilla directamente utilizando cadenas de plantilla. Ambos métodos son compatibles y ambos tienen ventajas y desventajas:
Con respecto al almacenamiento en caché, la biblioteca proporciona una CachedTemplate , que almacenará en caché y reutilizará el objeto Template siempre que lo proporcione un name , que se utilizará como la tecla de caché:
from django_web_components import component
from django_web_components . template import CachedTemplate
@ component . register
def alert ( context ):
return CachedTemplate (
"""
<div class="alert alert-primary" role="alert">
{% render_slot slots.inner_block %}
</div>
""" ,
name = "alert" ,
). render ( context ) Entonces, en realidad, el almacenamiento en caché no debe ser un problema al usar cadenas de plantilla, ya que CachedTemplate es tan rápido como usar el cargador en caché con archivos de plantilla.
Con respecto al formato de soporte y resaltado de sintaxis, no hay buena solución para las cadenas de plantilla. PyCharm admite la inyección de lenguaje que le permite agregar un comentario # language=html antes de la cadena de plantilla y obtener resaltar de sintaxis, sin embargo, solo resalta HTML y no las etiquetas Django, y todavía le falta soporte para el formato. Tal vez los editores agregan un mejor soporte para esto en el futuro, pero por el momento le faltará la sintaxis resaltando y el formateo si sigue esta ruta. Hay una conversación abierta sobre esto en el repositorio django-components , créditos a Emilstenstrom por avanzar con la conversación con el equipo VScode.
Al final, es una compensación. Use el método que tenga más sentido para usted.
Al igual que las señales, los componentes pueden vivir en cualquier lugar, pero debe asegurarse de que Django las recoja al inicio. La forma más fácil de hacerlo es definir sus componentes en un submódulo components.py de la aplicación con la que se relacionan y luego conectarlos dentro del método ready() de la clase de configuración de su aplicación.
from django . apps import AppConfig
from django_web_components import component
class MyAppConfig ( AppConfig ):
...
def ready ( self ):
# Implicitly register components decorated with @component.register
from . import components # noqa
# OR explicitly register a component
component . register ( "card" , components . Card ) También puede unregister un componente existente o obtener un componente del registro:
from django_web_components import component
# Unregister a component
component . registry . unregister ( "card" )
# Get a component
component . registry . get ( "card" )
# Remove all components
component . registry . clear ()
# Get all components as a dict of name: component
component . registry . all ()Cada componente registrado tendrá dos etiquetas disponibles para su uso en sus plantillas:
{% card %} ... {% endcard %}{% #user_profile %} . Esto puede ser útil para componentes que no necesariamente requieren un cuerpoPor defecto, los componentes se registrarán utilizando las siguientes etiquetas:
{% <component_name> %}{% end<component_name> %}{% #<component_name> %} Este comportamiento puede cambiarse proporcionando un formato de etiqueta personalizado en su configuración. Por ejemplo, para cambiar las etiquetas de bloque a {% #card %} ... {% /card %} , y la etiqueta en línea a {% card %} (similar a las zapatillas), puede usar el siguiente formato:
class ComponentTagFormatter :
def format_block_start_tag ( self , name ):
return f"# { name } "
def format_block_end_tag ( self , name ):
return f"/ { name } "
def format_inline_tag ( self , name ):
return name
# inside your settings
WEB_COMPONENTS = {
"DEFAULT_COMPONENT_TAG_FORMATTER" : "path.to.your.ComponentTagFormatter" ,
}Puede pasar datos a componentes utilizando argumentos de palabras clave, que aceptan valores o variables codificados:
{% with error_message="Something bad happened!" %}
{% #alert type="error" message=error_message %}
{% endwith %} Todos los atributos se agregarán en un dict de attributes que estará disponible en el contexto de la plantilla:
{
"attributes" : {
"type" : " error " ,
"message" : " Something bad happened! "
}
}Luego puede acceder a él desde la plantilla de su componente:
< div class =" alert alert-{{ attributes.type }} " >
{{ attributes.message }}
</ div > También puede representar todos los atributos directamente usando {{ attributes }} . Por ejemplo, si tiene el siguiente componente
{% alert id="alerts" class="font-bold" %} ... {% endalert %}Puede renderizar todos los atributos usando
< div {{ attributes }} >
<!-- Component content -->
</ div >Que dará como resultado que se represente el siguiente HTML:
< div id =" alerts " class =" font-bold " >
<!-- Component content -->
</ div > También puede pasar atributos con caracteres especiales ( [@:_-.] ), Así como atributos sin valor:
{% button @click="handleClick" data-id="123" required %} ... {% endbutton %}Que dará como resultado el siguiente dict disponible en el contexto:
{
"attributes" : {
"@click" : "handleClick" ,
"data-id" : "123" ,
"required" : True ,
}
} Y será presentado por {{ attributes }} como @click="handleClick" data-id="123" required .
A veces es posible que deba especificar valores predeterminados para atributos, o fusionar valores adicionales en algunos de los atributos del componente. La biblioteca proporciona una etiqueta merge_attrs que ayuda con esto:
< div {% merge_attrs attributes class =" alert " role =" alert " %} >
<!-- Component content -->
</ div >Si asumimos que este componente se utiliza así:
{% alert class="mb-4" %} ... {% endalert %}El HTML renderizado final del componente aparecerá como lo siguiente:
< div class =" alert mb-4 " role =" alert " >
<!-- Component content -->
</ div > Al fusionar atributos que no son atributos class , los valores proporcionados a la etiqueta merge_attrs se considerarán los valores "predeterminados" del atributo. Sin embargo, a diferencia del atributo class , estos atributos no se fusionarán con valores de atributos inyectados. En cambio, serán sobrescritos. Por ejemplo, la implementación de un componente button puede parecerse a la siguiente:
< button {% merge_attrs attributes type =" button " %} >
{% render_slot slots.inner_block %}
</ button > Para representar el componente del botón con un type personalizado, se puede especificar al consumir el componente. Si no se especifica ningún tipo, se utilizará el tipo button :
{% button type="submit" %} Submit {% endbutton %} El HTML renderizado del componente button en este ejemplo sería:
< button type =" submit " >
Submit
</ button > También puede tratar otros atributos como "apropiables" utilizando el operador += :
< div {% merge_attrs attributes data-value+ =" some-value " %} >
<!-- Component content -->
</ div >Si asumimos que este componente se utiliza así:
{% alert data-value="foo" %} ... {% endalert %}El HTML renderizado será:
< div data-value =" foo some-value " >
<!-- Component content -->
</ div > De manera predeterminada, todos los atributos se agregan a un DICT attributes dentro del contexto. Sin embargo, esto puede no ser siempre lo que queremos. Por ejemplo, imagine que queremos tener un componente alert que se pueda descartar, al mismo tiempo que puede pasar atributos adicionales al elemento raíz, como una id o class . Idealmente, nos gustaría poder representar un componente como este:
{% alert id="alerts" dismissible %} Something went wrong! {% endalert %}Una forma ingenua de implementar este componente sería algo como lo siguiente:
< div {{ attributes }} >
{% render_slot slots.inner_block %}
{% if attributes.dismissible %}
< button type =" button " class =" btn-close " data-bs-dismiss =" alert " aria-label =" Close " > </ button >
{% endif %}
</ div > Sin embargo, esto resultaría en que el atributo dismissible se incluya en el elemento raíz, que no es lo que queremos:
< div id =" alerts " dismissible >
Something went wrong!
< button type =" button " class =" btn-close " data-bs-dismiss =" alert " aria-label =" Close " > </ button >
</ div > Idealmente, queremos que el atributo dismissible se separe de los attributes ya que solo queremos usarlo en lógica, pero no necesariamente lo representa al componente.
Para lograr esto, puede manipular el contexto de su componente para proporcionar una mejor API para usar los componentes. Hay varias formas de hacer esto, elija el método que tenga más sentido para usted, por ejemplo:
get_context_data y eliminar el atributo dismissible de attributes y devolverlo en el contexto en su lugar from django_web_components import component
@ component . register ( "alert" )
class Alert ( component . Component ):
template_name = "components/alert.html"
def get_context_data ( self , ** kwargs ):
dismissible = self . attributes . pop ( "dismissible" , False )
return {
"dismissible" : dismissible ,
}render y manipular el contexto allí from django_web_components import component
@ component . register ( "alert" )
class Alert ( component . Component ):
template_name = "components/alert.html"
def render ( self , context ):
context [ "dismissible" ] = context [ "attributes" ]. pop ( "dismissible" , False )
return super (). render ( context )Ambas soluciones anteriores funcionarán, y puede hacer lo mismo para los componentes basados en funciones. La plantilla del componente puede verse así:
< div {{ attributes }} >
{% render_slot slots.inner_block %}
{% if dismissible %}
< button type =" button " class =" btn-close " data-bs-dismiss =" alert " aria-label =" Close " > </ button >
{% endif %}
</ div >Que debería dar como resultado que se represente el HTML correcto:
< div id =" alerts " >
Something went wrong!
< button type =" button " class =" btn-close " data-bs-dismiss =" alert " aria-label =" Close " > </ button >
</ div > A menudo deberá pasar contenido adicional a sus componentes a través de "ranuras". Se pasa una variable de contexto slots a sus componentes, que consiste en un dict con el nombre de la ranura como la clave y la ranura como valor. Luego puede representar las ranuras dentro de sus componentes usando la etiqueta render_slot .
Para explorar este concepto, imaginemos que queremos pasar un poco de contenido a un componente alert :
{% alert %}
< strong > Whoops! </ strong > Something went wrong!
{% endalert %} Por defecto, ese contenido estará disponible para su componente en la ranura predeterminada que se llama inner_block . Luego puede representar esta ranura usando la etiqueta render_slot dentro de su componente:
{% load components %}
< div class =" alert alert-danger " >
{% render_slot slots.inner_block %}
</ div >Que debería dar como resultado que se represente el siguiente HTML:
< div class =" alert alert-danger " >
< strong > Whoops! </ strong > Something went wrong!
</ div >También puede cambiar el nombre de la ranura predeterminada especificando en su configuración:
# inside your settings
WEB_COMPONENTS = {
"DEFAULT_SLOT_NAME" : "inner_block" ,
}A veces, es posible que un componente deba representar múltiples ranuras diferentes en diferentes ubicaciones dentro del componente. Modificemos nuestro componente de alerta para permitir la inyección de una ranura de "título":
{% load components %}
< div class =" alert alert-danger " >
< span class =" alert-title " >
{% render_slot slots.title %}
</ span >
{% render_slot slots.inner_block %}
</ div > Puede definir el contenido de la ranura nombrada usando la etiqueta slot . Cualquier contenido que no esté dentro de una etiqueta slot explícita se agregará a la ranura inner_block predeterminada:
{% load components %}
{% alert %}
{% slot title %} Server error {% endslot %}
< strong > Whoops! </ strong > Something went wrong!
{% endalert %}El HTML renderizado en este ejemplo sería:
< div class =" alert alert-danger " >
< span class =" alert-title " >
Server error
</ span >
< strong > Whoops! </ strong > Something went wrong!
</ div >Puede definir la misma ranura con nombre varias veces:
{% unordered_list %}
{% slot item %} First item {% endslot %}
{% slot item %} Second item {% endslot %}
{% slot item %} Third item {% endslot %}
{% endunordered_list %}Luego puede iterar sobre la ranura dentro de su componente:
< ul >
{% for item in slots.item %}
< li > {% render_slot item %} </ li >
{% endfor %}
</ ul >Que dará como resultado el siguiente HTML:
< ul >
< li > First item </ li >
< li > Second item </ li >
< li > Third item </ li >
</ ul > El contenido de la ranura también tendrá acceso al contexto del componente. Para explorar este concepto, imagine un componente de la lista que acepta un atributo entries que representa una lista de cosas, que luego iterará y representará la ranura inner_block para cada entrada.
from django_web_components import component
from django_web_components . template import CachedTemplate
@ component . register
def unordered_list ( context ):
context [ "entries" ] = context [ "attributes" ]. pop ( "entries" , [])
return CachedTemplate (
"""
<ul>
{% for entry in entries %}
<li>
{% render_slot slots.inner_block %}
</li>
{% endfor %}
</ul>
""" ,
name = "unordered_list" ,
). render ( context )Luego podemos representar el componente de la siguiente manera:
{% unordered_list entries=entries %}
I like {{ entry }}!
{% endunordered_list %} En este ejemplo, la variable entry proviene del contexto del componente. Si asumimos que entries = ["apples", "bananas", "cherries"] , el HTML resultante será:
< ul >
< li > I like apples! </ li >
< li > I like bananas! </ li >
< li > I like cherries! </ li >
</ ul > También puede pasar explícitamente un segundo argumento para render_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 > Al invocar el componente, puede usar el atributo especial :let tomar el valor que se pasó a render_slot y vincularlo a una variable:
{% unordered_list :let="fruit" entries=entries %}
I like {{ fruit }}!
{% endunordered_list %}Esto representaría el mismo HTML que el anterior.
Similar a los componentes, puede asignar atributos adicionales a las ranuras. A continuación se muestra un componente de tabla que ilustra múltiples ranuras con nombre con atributos:
from django_web_components import component
from django_web_components . template import CachedTemplate
@ component . register
def table ( context ):
context [ "rows" ] = context [ "attributes" ]. pop ( "rows" , [])
return CachedTemplate (
"""
<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 )Puede invocar el componente así:
{% table rows=rows %}
{% slot column :let="user" label="Name" %}
{{ user.name }}
{% endslot %}
{% slot column :let="user" label="Age" %}
{{ user.age }}
{% endslot %}
{% endtable %} Si suponemos que rows = [{ "name": "Jane", "age": "34" }, { "name": "Bob", "age": "51" }] , se representará el siguiente html:
< 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 >También puede anidar componentes para lograr elementos más complicados. Aquí hay un ejemplo de cómo puede implementar un componente de acordeón usando Bootstrap:
from django_web_components import component
from django_web_components . template import CachedTemplate
import uuid
@ component . register
def accordion ( context ):
context [ "accordion_id" ] = context [ "attributes" ]. pop ( "id" , str ( uuid . uuid4 ()))
context [ "always_open" ] = context [ "attributes" ]. pop ( "always_open" , False )
return CachedTemplate (
"""
<div class="accordion" id="{{ accordion_id }}">
{% render_slot slots.inner_block %}
</div>
""" ,
name = "accordion" ,
). render ( context )
@ component . register
def accordion_item ( context ):
context [ "id" ] = context [ "attributes" ]. pop ( "id" , str ( uuid . uuid4 ()))
context [ "open" ] = context [ "attributes" ]. pop ( "open" , False )
return CachedTemplate (
"""
<div class="accordion-item" id="{{ id }}">
<h2 class="accordion-header" id="{{ id }}-header">
<button
class="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>
<div
id="{{ 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 )Luego puedes usarlos de la siguiente manera:
{% 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 %} El proyecto utiliza poetry para administrar las dependencias. Consulte la documentación sobre cómo instalar poesía aquí: https://python-poetry.org/docs/#installation
Instalar las dependencias
poetry installActivar el entorno
poetry shellAhora puedes ejecutar las pruebas
python runtests.pyEl proyecto llegó a ser después de ver cómo otros idiomas / marcos tratan con los componentes y querer devolver algunas de esas ideas a Django.
django-components ya es excelente y admite la mayoría de las características que tiene este proyecto, pero pensé que la sintaxis podría mejorarse un poco para sentirse menos detallada y agregar algunas cosas adicionales que parecían necesarias, como el soporte para componentes basados en funciones y las tormentas de plantilla, y trabajar con atributos<x-alert type="error">Server Error</x-alert> ), pero la solución era mucho más complicada y llegué a la conclusión de que el uso de un enfoque similar a django-components tenía más sentido en Django en DjangoMuchos otros idiomas / marcos están utilizando los mismos conceptos para construir componentes (ranuras, atributos), por lo que gran parte del conocimiento es transferible, y ya existe una gran cantidad de bibliotecas de componentes existentes (por ejemplo, usando bootstrap, viento de cola, diseño de material, etc.). Recomiendo ver a algunos de ellos para inspirarme sobre cómo construir / estructurar sus componentes. Aquí hay algunos ejemplos: