Uma maneira simples de criar componentes de modelo reutilizáveis no Django.
Você tem que primeiro registrar seu componente
from django_web_components import component
@ component . register ( "card" )
class Card ( component . Component ):
template_name = "components/card.html"O modelo do 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 >Agora você pode renderizar este componente com:
{% 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 resultará no renderizado o seguinte 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
Em seguida, adicione django_web_components ao seu INSTALLED_APPS .
INSTALLED_APPS = [
...,
"django_web_components" ,
] Para evitar ter que usar {% load components %} em cada modelo, você pode adicionar as tags à lista builtins dentro de suas configurações.
TEMPLATES = [
{
...,
"OPTIONS" : {
"context_processors" : [
...
],
"builtins" : [
"django_web_components.templatetags.components" ,
],
},
},
]A biblioteca suporta Python 3.8+ e Django 3.2+.
| Versão Python | Versão 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 |
Existem duas abordagens para escrever componentes: componentes baseados em classe e componentes baseados em funções.
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 ,
} O componente será renderizado chamando o método render(context) , que por padrão carregará o arquivo de modelo e renderá -lo.
Para pequenos componentes, pode parecer pesado gerenciar a classe de componentes e o modelo do componente. Por esse motivo, você pode definir o modelo diretamente do 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 ) Um componente também pode ser definido como uma única função que aceita um context e retorna uma string:
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 )Os exemplos deste guia usarão principalmente componentes baseados em funções, pois é mais fácil exemplificar como o código e o modelo do componente estão no mesmo local, mas você é livre para escolher o método que preferir.
A biblioteca usa os modelos de Django regulares, que permitem carregar modelos de arquivos ou criar objetos de modelo diretamente usando strings de modelo. Ambos os métodos são suportados e ambos têm vantagens e desvantagens:
Em relação ao armazenamento em cache, a biblioteca fornece uma CachedTemplate , que abrigará e reutilizará o objeto Template , desde que você forneça um name para ele, que será usado como chave de cache:
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 ) Portanto, na realidade, o cache não deve ser um problema ao usar strings de modelo, pois CachedTemplate é tão rápido quanto o uso do carregador em cache com arquivos de modelo.
Em relação à formatação de suporte e destaque da sintaxe, não há uma boa solução para seqüências de modelos. O PyCharm suporta injeção de linguagem que permite adicionar um comentário # language=html antes da sequência de modelos e obter destaque da sintaxe; no entanto, ele destaca apenas o HTML e não as tags do Django, e você ainda está faltando suporte para formatação. Talvez os editores adicionem melhor suporte a isso no futuro, mas, no momento, você estará faltando a sintaxe destacando e a formatação se seguir esse caminho. Há uma conversa aberta sobre isso no repositório django-components , Créditos a Emilstenstrom por levar a conversa adiante com a equipe do VSCODE.
No final, é uma troca. Use o método que faz mais sentido para você.
Assim como os sinais, os componentes podem morar em qualquer lugar, mas você precisa garantir que o Django os pegue na startup. A maneira mais fácil de fazer isso ready() definir seus componentes em um submodule de components.py .
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 ) Você também pode unregister um componente existente ou obter um componente do 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 terá duas tags disponíveis para uso em seus modelos:
{% card %} ... {% endcard %}{% #user_profile %} . Isso pode ser útil para componentes que não exigem necessariamente um corpoPor padrão, os componentes serão registrados usando as seguintes tags:
{% <component_name> %}{% end<component_name> %}{% #<component_name> %} Esse comportamento pode ser alterado fornecendo um formatador de tag personalizado em suas configurações. Por exemplo, para alterar as tags de bloco para {% #card %} ... {% /card %} e a tag embutida para {% card %} (semelhante aos chinelos), você pode usar o seguinte formatador:
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" ,
}Você pode passar dados para componentes usando argumentos de palavras -chave, que aceitam valores ou variáveis codificadas:
{% with error_message="Something bad happened!" %}
{% #alert type="error" message=error_message %}
{% endwith %} Todos os atributos serão adicionados em um ditado de attributes que estarão disponíveis no contexto do modelo:
{
"attributes" : {
"type" : " error " ,
"message" : " Something bad happened! "
}
}Você pode acessá -lo do modelo do seu componente:
< div class =" alert alert-{{ attributes.type }} " >
{{ attributes.message }}
</ div > Você também pode renderizar todos os atributos diretamente usando {{ attributes }} . Por exemplo, se você tiver o seguinte componente
{% alert id="alerts" class="font-bold" %} ... {% endalert %}Você pode renderizar todos os atributos usando
< div {{ attributes }} >
<!-- Component content -->
</ div >Que resultará no renderizado o seguinte HTML:
< div id =" alerts " class =" font-bold " >
<!-- Component content -->
</ div > Você também pode passar atributos com caracteres especiais ( [@:_-.] ), Bem como atributos sem valor:
{% button @click="handleClick" data-id="123" required %} ... {% endbutton %}Que resultará no seguinte ditado disponível no contexto:
{
"attributes" : {
"@click" : "handleClick" ,
"data-id" : "123" ,
"required" : True ,
}
} E será renderizado por {{ attributes }} como @click="handleClick" data-id="123" required .
Às vezes, pode ser necessário especificar valores padrão para atributos ou mesclar valores adicionais em alguns dos atributos do componente. A biblioteca fornece uma tag merge_attrs que ajuda com isso:
< div {% merge_attrs attributes class =" alert " role =" alert " %} >
<!-- Component content -->
</ div >Se assumirmos que este componente é utilizado como assim:
{% alert class="mb-4" %} ... {% endalert %}O HTML final renderizado do componente aparecerá como o seguinte:
< div class =" alert mb-4 " role =" alert " >
<!-- Component content -->
</ div > Ao mesclar atributos que não são atributos class , os valores fornecidos à tag merge_attrs serão considerados os valores "padrão" do atributo. No entanto, diferentemente do atributo class , esses atributos não serão mesclados com valores de atributo injetado. Em vez disso, eles serão substituídos. Por exemplo, a implementação de um componente button pode parecer o seguinte:
< button {% merge_attrs attributes type =" button " %} >
{% render_slot slots.inner_block %}
</ button > Para renderizar o componente do botão com um type personalizado, ele pode ser especificado ao consumir o componente. Se nenhum tipo for especificado, o tipo button será usado:
{% button type="submit" %} Submit {% endbutton %} O HTML renderizado do componente button neste exemplo seria:
< button type =" submit " >
Submit
</ button > Você também pode tratar outros atributos como "anexos" usando o += operador:
< div {% merge_attrs attributes data-value+ =" some-value " %} >
<!-- Component content -->
</ div >Se assumirmos que este componente é utilizado como assim:
{% alert data-value="foo" %} ... {% endalert %}O HTML renderizado será:
< div data-value =" foo some-value " >
<!-- Component content -->
</ div > Por padrão, todos os atributos são adicionados a um attributes ditado dentro do contexto. No entanto, isso nem sempre é o que queremos. Por exemplo, imagine que queremos ter um componente alert que possa ser descartado, ao mesmo tempo em que podemos passar atributos extras para o elemento raiz, como um id ou class . Idealmente, gostaríamos de poder renderizar um componente como este:
{% alert id="alerts" dismissible %} Something went wrong! {% endalert %}Uma maneira ingênua de implementar esse componente seria algo como o seguinte:
< 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 > No entanto, isso resultaria no fato de o atributo dismissible estar incluído no elemento raiz, que não é o 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, gostaríamos que o atributo dismissible seja separado dos attributes pois queremos usá -lo apenas na lógica, mas não necessariamente o renderiza ao componente.
Para conseguir isso, você pode manipular o contexto do seu componente para fornecer uma API melhor para o uso dos componentes. Existem várias maneiras de fazer isso, escolha o método que faz mais sentido para você, por exemplo:
get_context_data e remover o atributo dismissible dos attributes e devolvê -lo no contexto em vez 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 e manipular o contexto lá 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 as soluções acima funcionarão e você pode fazer o mesmo para componentes baseados em funções. O modelo do componente pode então ficar assim:
< 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 >O que deve resultar no renderizado o HTML correto:
< div id =" alerts " >
Something went wrong!
< button type =" button " class =" btn-close " data-bs-dismiss =" alert " aria-label =" Close " > </ button >
</ div > Muitas vezes, você precisará passar conteúdo adicional para seus componentes por meio de "slots". Uma variável de contexto slots é passada para seus componentes, que consiste em um ditado com o nome do slot como a chave e o slot como o valor. Você pode renderizar os slots dentro de seus componentes usando a tag render_slot .
Para explorar esse conceito, vamos imaginar que queremos passar algum conteúdo para um componente alert :
{% alert %}
< strong > Whoops! </ strong > Something went wrong!
{% endalert %} Por padrão, esse conteúdo será disponibilizado ao seu componente no slot padrão chamado inner_block . Você pode renderizar esse slot usando a tag render_slot dentro do seu componente:
{% load components %}
< div class =" alert alert-danger " >
{% render_slot slots.inner_block %}
</ div >O que deve resultar na renderização do HTML a seguir:
< div class =" alert alert-danger " >
< strong > Whoops! </ strong > Something went wrong!
</ div >Você também pode renomear o slot padrão especificando -o em suas configurações:
# inside your settings
WEB_COMPONENTS = {
"DEFAULT_SLOT_NAME" : "inner_block" ,
}Às vezes, um componente pode precisar renderizar vários slots diferentes em locais diferentes no componente. Vamos modificar nosso componente de alerta para permitir a injeção de um slot "título":
{% load components %}
< div class =" alert alert-danger " >
< span class =" alert-title " >
{% render_slot slots.title %}
</ span >
{% render_slot slots.inner_block %}
</ div > Você pode definir o conteúdo do slot nomeado usando a tag slot . Qualquer conteúdo que não esteja dentro de uma etiqueta slot explícito será adicionado ao slot inner_block padrão:
{% load components %}
{% alert %}
{% slot title %} Server error {% endslot %}
< strong > Whoops! </ strong > Something went wrong!
{% endalert %}O HTML renderizado neste exemplo seria:
< div class =" alert alert-danger " >
< span class =" alert-title " >
Server error
</ span >
< strong > Whoops! </ strong > Something went wrong!
</ div >Você pode definir o mesmo slot nomeado várias vezes:
{% unordered_list %}
{% slot item %} First item {% endslot %}
{% slot item %} Second item {% endslot %}
{% slot item %} Third item {% endslot %}
{% endunordered_list %}Você pode iterar sobre o slot dentro do seu componente:
< ul >
{% for item in slots.item %}
< li > {% render_slot item %} </ li >
{% endfor %}
</ ul >Que resultará no seguinte HTML:
< ul >
< li > First item </ li >
< li > Second item </ li >
< li > Third item </ li >
</ ul > O conteúdo do slot também terá acesso ao contexto do componente. Para explorar esse conceito, imagine um componente de lista que aceite um atributo entries representando uma lista de coisas, que ele irá iterar e renderizar o slot 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 )Podemos então renderizar o componente da seguinte forma:
{% unordered_list entries=entries %}
I like {{ entry }}!
{% endunordered_list %} Neste exemplo, a variável entry vem do contexto do componente. Se assumirmos que entries = ["apples", "bananas", "cherries"] , o HTML resultante será:
< ul >
< li > I like apples! </ li >
< li > I like bananas! </ li >
< li > I like cherries! </ li >
</ ul > Você também pode passar explicitamente um 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 > Ao invocar o componente, você pode usar o atributo especial :let tomar o valor que foi passado para render_slot e ligá -lo a uma variável:
{% unordered_list :let="fruit" entries=entries %}
I like {{ fruit }}!
{% endunordered_list %}Isso renderizaria o mesmo HTML que acima.
Semelhante aos componentes, você pode atribuir atributos adicionais aos slots. Abaixo está um componente de tabela que ilustra vários slots nomeados com 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 )Você pode invocar o componente assim:
{% table rows=rows %}
{% slot column :let="user" label="Name" %}
{{ user.name }}
{% endslot %}
{% slot column :let="user" label="Age" %}
{{ user.age }}
{% endslot %}
{% endtable %} Se assumirmos que rows = [{ "name": "Jane", "age": "34" }, { "name": "Bob", "age": "51" }] , o seguinte HTML será renderizado:
< 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 >Você também pode aninhar componentes para obter elementos mais complicados. Aqui está um exemplo de como você pode implementar um componente de acordeão usando o 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 )Você pode usá -los da seguinte maneira:
{% 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 %} O projeto usa poetry para gerenciar as dependências. Confira a documentação sobre como instalar a poesia aqui: https://python-poetry.org/docs/#installation
Instalar as dependências
poetry installAtive o ambiente
poetry shellAgora você pode executar os testes
python runtests.pyO projeto passou a ser depois de ver como outros idiomas / estruturas lidam com os componentes e querendo trazer algumas dessas idéias de volta ao Django.
django-components existente já é ótima e suporta a maioria dos recursos que esse projeto possui, mas achei que a sintaxe poderia ser um pouco melhorada para se sentir menos detalhada e adicionar algumas coisas extras que pareciam necessárias, como suporte para componentes e modelos baseados em funções, e trabalhar com atributos com atributos<x-alert type="error">Server Error</x-alert> ), mas a solução foi muito mais complicada e cheguei à conclusão de que o uso de uma abordagem semelhante aos django-components fez muito mais sentido no DJangoMuitos outros idiomas / estruturas estão usando os mesmos conceitos para construir componentes (slots, atributos), então muito do conhecimento é transferível, e já existe uma grande quantidade de bibliotecas de componentes existentes (por exemplo, usando bootstrap, Tailwind, design de material, etc.). Eu recomendo olhar para alguns deles para se inspirar em como criar / estruturar seus componentes. Aqui estão alguns exemplos: