Skip to content

rg3915/django-htmx-tutorial

Repository files navigation

django-htmx-tutorial

Tutorial sobre como trabalhar com Django e htmx.

htmx.png

Este projeto foi feito com:

Como rodar o projeto?

  • Clone esse repositório.
  • Crie um virtualenv com Python 3.
  • Ative o virtualenv.
  • Instale as dependências.
  • Rode as migrações.
git clone https://github.com/rg3915/django-htmx-tutorial.git cd django-htmx-tutorial python -m venv .venv source .venv/bin/activate pip install -r requirements.txt python contrib/env_gen.py python manage.py migrate python manage.py createsuperuser --username="admin" --email="" python manage.py runserver 

Exemplos

Passo a passo

Clonando o projeto base

git clone https://github.com/rg3915/django-htmx-tutorial.git cd django-htmx-tutorial git checkout passo-a-passo python -m venv .venv source .venv/bin/activate pip install -U pip pip install -r requirements.txt pip install ipdb python contrib/env_gen.py python manage.py migrate python manage.py createsuperuser --username="admin" --email="" 

Vamos editar:

  • base.html
  • nav.html
  • index.html

Em base.html escreva

<!-- HTMX --><scriptsrc="https://unpkg.com/[email protected]"></script><scriptsrc="https://unpkg.com/[email protected]/dist/ext/client-side-templates.js"></script><scriptsrc="https://cdn.jsdelivr.net/npm/[email protected]/mustache.min.js"></script>

Em nav.html escreva

<liclass="nav-item"><aclass="nav-link" href="{% url 'state:state_list' %}">Estados</a></li><liclass="nav-item"><aclass="nav-link" href="{url 'expense:expense_list' %}">Despesas</a></li><liclass="nav-item"><aclass="nav-link" href="{url 'expense:expense_client' %}">Despesas (Client side)</a></li>

Corrija o link em index.html

<ahref="{% url 'state:state_list' %}">Estados</a>

Exemplos

Filtrar várias tabelas com um clique

a03_tabela.png

Considere a app state.

Vamos editar:

  • views.py
  • urls.py
  • state_list.html
  • hx/state_hx.html

Em state/views.py escreva

# state/views.pyfromdjango.shortcutsimportrenderfrom .statesimportstatesdefstate_list(request): template_name='state/state_list.html'regions= ( ('n', 'Norte'), ('ne', 'Nordeste'), ('s', 'Sul'), ('se', 'Sudeste'), ('co', 'Centro-Oeste'), ) context={'regions': regions} returnrender(request, template_name, context)

Em state/urls.py escreva

# state/urls.pyfromdjango.urlsimportpathfrombackend.stateimportviewsasvapp_name='state'urlpatterns= [ path('', v.state_list, name='state_list'), path('result/', v.state_result, name='state_result'), ]

Crie as pastas

mkdir -p state/templates/state/hx 

Escreva o template

touch state/templates/state/state_list.html

<!-- state/templates/state/state_list.html -->{% extends "base.html" %}{% block content %} <h1>Regiões e Estados do Brasil</h1><h2style="color: #3465a4;">Filtrando várias tabelas com um clique</h2><divclass="row"><divclass="col"><tableclass="table table-hover"><thead><tr><th>Região</th></tr></thead><tbody>{% for region in regions %} <trhx-get="{% url 'state:state_result' %}?region={{region.0}}" hx-target="#states" ><td><a>{{region.1 }}</a></td></tr>{% endfor %} </tbody></table></div><divclass="col"><tableclass="table"><thead><tr><th>Estados</th></tr></thead><tbodyid="states"><!-- O novo conteúdo será inserido aqui. --></tbody></table></div></div>{% endblock content %}

Em state/views.py escreva

# state/views.pydefget_states(region): return [stateforstateinstates.get(region).items()] defstate_result(request): template_name='state/hx/state_hx.html'region=request.GET.get('region') ufs={'n': get_states('Norte'), 'ne': get_states('Nordeste'), 's': get_states('Sul'), 'se': get_states('Sudeste'), 'co': get_states('Centro-Oeste'), } context={'ufs': ufs[region]} returnrender(request, template_name, context)

Escreva o template

touch state/templates/state/hx/state_hx.html

<!-- state/templates/state/hx/state_hx.html -->{% for uf in ufs %} <tr><td>{{uf.1 }}</td></tr>{% endfor %}

Descomente em urls.py

path('state/', include('backend.state.urls', namespace='state')), 

Filtrar com dropdowns dependentes

a02_combobox.png

Vamos editar:

  • urls.py
  • views.py
  • hx/uf_hx.html
  • state_list.html

Edite state/urls.py

# state/urls.py ... path('uf/', v.uf_list, name='uf_list'), ...

Edite state/views.py

# state/views.pydefuf_list(request): template_name='state/hx/uf_hx.html'region=request.GET.get('region') ufs={'n': get_states('Norte'), 'ne': get_states('Nordeste'), 's': get_states('Sul'), 'se': get_states('Sudeste'), 'co': get_states('Centro-Oeste'), } context={'ufs': ufs[region]} returnrender(request, template_name, context)

Edite

touch state/templates/state/hx/uf_hx.html

<!-- state/templates/state/hx/uf_hx.html -->{% for uf in ufs %} <optionvalue="{{uf.0 }}">{{uf.1 }}</option>{% endfor %}

Edite state/templates/state/state_list.html

<h2style="color: #3465a4;">Filtro com dropdowns dependentes</h2><divclass="row"><divclass="col"><label>Região</label><selectname="region" class="form-control" hx-get="{% url 'state:uf_list' %}" hx-target="#uf" ><optionvalue="">-----</option>{% for region in regions %} <optionvalue="{{region.0 }}">{{region.1 }}</option>{% endfor %} </select></div><divclass="col"><label>UF</label><selectid="uf" class="form-control" ><optionvalue="">-----</option><!-- O novo conteúdo será inserido aqui. --></select></div></div><hr> ...

A base para despesas

Considere o desenho a seguir:

expense_base.png

expense_hx.html será inserido em expense_table.hmtl, que por sua vez será inserido em expense_list.html.

Sendo que expense_hx.html será repetido várias vezes por causa do laço de repetição em expense_table.hmtl.


Adicionar itens

01_expense_add.png

Vamos editar:

  • models.py
  • admin.py
  • forms.py
  • views.py
  • urls.py
  • expense_list.html
  • expense_table.html
  • hx/expense_hx.html
  • nav.html
  • index.html

Escreva o expense/models.py

# expense/models.pyfromdjango.dbimportmodelsfrombackend.core.modelsimportTimeStampedModelclassExpense(TimeStampedModel): description=models.CharField('descrição', max_length=30) value=models.DecimalField('valor', max_digits=7, decimal_places=2) paid=models.BooleanField('pago', default=False) classMeta: ordering= ('description',) verbose_name='despesa'verbose_name_plural='despesas'def__str__(self): returnself.description

Escreva o expense/admin.py

# expense/admin.pyfromdjango.contribimportadminfrom .modelsimportExpense@admin.register(Expense)classExpenseAdmin(admin.ModelAdmin): list_display= ('__str__', 'value', 'paid') search_fields= ('description',) list_filter= ('paid',)

Escreva

touch expense/forms.py

# expense/forms.pyfromdjangoimportformsfrom .modelsimportExpenseclassExpenseForm(forms.ModelForm): required_css_class='required'classMeta: model=Expensefields= ('description', 'value') widgets={'description': forms.TextInput(attrs={'placeholder': 'Descrição', 'autofocus': True}), 'value': forms.NumberInput(attrs={'placeholder': 'Valor'}), } def__init__(self, *args, **kwargs): super(ExpenseForm, self).__init__(*args, **kwargs) forfield_name, fieldinself.fields.items(): field.widget.attrs['class'] ='form-control'

Escreva o expense/views.py

# expense/views.pyfromdjango.httpimportJsonResponsefromdjango.shortcutsimportrenderfromdjango.views.decorators.httpimportrequire_http_methodsfrom .formsimportExpenseFormfrom .modelsimportExpensedefexpense_list(request): template_name='expense/expense_list.html'form=ExpenseForm(request.POSTorNone) expenses=Expense.objects.all() context={'object_list': expenses, 'form': form} returnrender(request, template_name, context) @require_http_methods(['POST'])defexpense_create(request): form=ExpenseForm(request.POSTorNone) ifform.is_valid(): expense=form.save() context={'object': expense} returnrender(request, 'expense/hx/expense_hx.html', context)

Escreva o expense/urls.py

# expense/urls.pyfromdjango.urlsimportpathfrombackend.expenseimportviewsasvapp_name='expense'urlpatterns= [ path('', v.expense_list, name='expense_list'), path('create/', v.expense_create, name='expense_create'), ]

Crie as pastas

mkdir -p expense/templates/expense 

Escreva

touch expense/templates/expense/expense_list.html

<!-- expense_list.html -->{% extends "base.html" %}{% block content %} <h1>Lista de Despesas</h1><divclass="row"><divclass="col"><formclass="form-inline p-3" hx-post="{% url 'expense:expense_create' %}" hx-target="#expenseTbody" hx-indicator=".htmx-indicator" hx-swap="afterbegin" >{% csrf_token %}{% for field in form %} <divclass="form-group p-2">{{field }}{{field.errors }}{% if field.help_text %} <smallclass="text-muted">{{field.help_text|safe }}</small>{% endif %} </div>{% endfor %} <divclass="form-group"><buttontype="submit" class="btn btn-primary ml-2" >Adicionar</button></div></form></div></div><divid="checkedExpenses" class="col pt-2" ><form><tableclass="table"><thead><tr><th></th><th>Descrição</th><th>Valor</th><thclass="text-center">Pago</th><thclass="text-center">Ações</th></tr></thead><tbodyid="expenseTbody">{% include "./expense_table.html" %} </tbody></table></form></div>{% endblock content %}{% block js %} <script>document.body.addEventListener('htmx:configRequest',(event)=>{event.detail.headers['X-CSRFToken']='{{csrf_token }}';});</script>{% endblock js %}

Escreva

touch expense/templates/expense/expense_table.html

<!-- expense_table.html -->{% for object in object_list %}{% include "./hx/expense_hx.html" %}{% endfor %}

Escreva

touch expense/templates/expense/hx/expense_hx.html

<!-- hx/expense_hx.html --><trhx-target="this" hx-swap="outerHTML" class="person{% if object.paid %}activate{% else %}deactivate{% endif %}" ><td><inputtype="checkbox" name="ids" value="{{object.pk }}" ></td><td>{{object.description }}</td><td>{{object.value }}</td><tdclass="text-center">{% if object.paid %} <span><iclass="fa fa-check-circle ok"></i></span>{% else %} <span><iclass="fa fa-times-circle no"></i></span>{% endif %} </td></tr>

Edite nav.html

<aclass="nav-link" href="{% url 'expense:expense_list' %}">Despesas</a>

Edite index.html

<p><ahref="{% url 'expense:expense_list' %}">Despesas</a> CRUD com SPA</p>

Descomente urls.py

path('expense/', include('backend.expense.urls', namespace='expense')),

Pagar (editar) vários itens (Bulk Update)

02_expense_bulk_update.png

Vamos editar:

  • views.py
  • urls.py
  • expense_list.html

Escreva o expense/views.py

@require_http_methods(['POST'])defexpense_paid(request): ids=request.POST.getlist('ids') # Edita as despesas selecionadas.Expense.objects.filter(id__in=ids).update(paid=True) # Retorna todas as despesas novamente.expenses=Expense.objects.all() context={'object_list': expenses} returnrender(request, 'expense/expense_table.html', context) @require_http_methods(['POST'])defexpense_no_paid(request): ids=request.POST.getlist('ids') # Edita as despesas selecionadas.Expense.objects.filter(id__in=ids).update(paid=False) # Retorna todas as despesas novamente.expenses=Expense.objects.all() context={'object_list': expenses} returnrender(request, 'expense/expense_table.html', context)

Escreva o expense/urls.py

path('expense/paid/', v.expense_paid, name='expense_paid'), path('expense/no-paid/', v.expense_no_paid, name='expense_no_paid'),

Escreva o expense/expense_list.html

<!-- expense_list.html --><divclass="col" hx-include="#checkedExpenses" hx-target="#expenseTbody" ><aclass="btn btn-outline-success" hx-post="{% url 'expense:expense_paid' %}" >Pago</a><aclass="btn btn-outline-danger" hx-post="{% url 'expense:expense_no_paid' %}" >Não Pago</a><spanclass="lead"><strong>Bulk update</strong></span></div>

Editar um item

03_expense_update.png

Vamos editar:

  • views.py
  • urls.py
  • hx/expense_hx.html
  • hx/expense_detail.html

Escreva o expense/views.py

# expense/views.pydefexpense_detail(request, pk): template_name='expense/hx/expense_detail.html'obj=Expense.objects.get(pk=pk) form=ExpenseForm(request.POSTorNone, instance=obj) context={'object': obj, 'form': form} returnrender(request, template_name, context) defexpense_update(request, pk): template_name='expense/hx/expense_hx.html'obj=Expense.objects.get(pk=pk) form=ExpenseForm(request.POSTorNone, instance=obj) context={'object': obj} ifrequest.method=='POST': ifform.is_valid(): form.save() returnrender(request, template_name, context)

Escreva o expense/urls.py

# expense/urls.pypath('<int:pk>/', v.expense_detail, name='expense_detail'), path('<int:pk>/update/', v.expense_update, name='expense_update'),

Escreva o expense/hx/expense_hx.html

<!-- expense/hx/expense_hx.html --><tdclass="text-center"><spanhx-get="{% url 'expense:expense_detail' object.pk %}"><iclass="fa fa-pencil-square-o link span-is-link"></i></span></td>

Escreva

touch expense/templates/expense/hx/expense_detail.html

<!-- expense_detail.html --><trid="trExpense"><td></td><td>{{form.description }}</td><td>{{form.value }}</td><td></td><tdclass="text-center"><buttontype="submit" class="btn btn-success" hx-post="{% url 'expense:expense_update' object.pk %}" hx-target="#trExpense" hx-swap="outerHTML" > OK </button><buttonclass="btn btn-danger" hx-get="{% url 'expense:expense_update' object.pk %}" hx-target="#trExpense" hx-swap="outerHTML" ><iclass="fa fa-close"></i></button></td></tr>

Deletar um item

04_expense_delete.png

Vamos editar:

  • views.py
  • urls.py
  • hx/expense_hx.html

Escreva o expense/views.py

# expense/views.py@require_http_methods(['DELETE'])defexpense_delete(request, pk): obj=Expense.objects.get(pk=pk) obj.delete() returnrender(request, 'expense/expense_table.html')

Escreva o expense/urls.py

# expense/urls.pypath('<int:pk>/delete/', v.expense_delete, name='expense_delete'),

Escreva o expense/hx/expense_hx.html

<!-- expense/hx/expense_hx.html --><tdclass="text-center"> ... <spanhx-delete="{% url 'expense:expense_delete' object.pk %}" hx-confirm="Deseja mesmo deletar?" hx-target="closest tr" hx-swap="outerHTML swap:500ms" ><iclass="fa fa-trash no span-is-link pl-2"></i></span></td>

client-side-templates

https://htmx.org/extensions/client-side-templates/

Vamos editar:

# expense/models.pyclassExpense(TimeStampedModel): ... defto_dict(self): return{'id': self.id, 'description': self.description, 'value': self.value, 'paid': self.paid, }

Escreva o expense/views.py

# expense/views.pydefexpense_json(self): expenses=Expense.objects.all() data= [expense.to_dict() forexpenseinexpenses] returnJsonResponse({'data': data}) defexpense_client(request): template_name='expense/expense_client.html'returnrender(request, template_name)

Escreva o expense/urls.py

# expense/urls.py ... path('json/', v.expense_json, name='expense_json'), path('client/', v.expense_client, name='expense_client'),

Escreva

touch expense/templates/expense/expense_client.html

<!-- expense_client.html -->{% extends "base.html" %}{% block content %} <h1>Lista de Despesas (Client side)</h1><h3>Consumindo API Rest</h3><divhx-ext="client-side-templates"><buttonclass="btn btn-primary" hx-get="{% url 'expense:expense_json' %}" hx-swap="innerHTML" hx-target="#content" mustache-template="foo" > Clique para carregar os dados </button><tableclass="table"><thead><tr><th>Descrição</th><th>Valor</th><thclass="text-center">Pago</th></tr></thead><tbodyid="content"></tbody><templateid="foo"><!-- Mustache looping -->{#data } <tr><td>{description }</td><td>{value }</td><tdclass="text-center"><!-- Mustache conditional --><!-- http://mustache.github.io/mustache.5.html#Inverted-Sections -->{#paid } <iclass="fa fa-check-circle ok"></i>{/paid }{^paid } <iclass="fa fa-times-circle no"></i>{/paid } </td></tr>{/data } </template></table></div>{% endblock content %}{% block js %} <script>// https://github.com/janl/mustache.js/#custom-delimitersMustache.tags=['{','}'];</script>{% endblock js %}

Edite nav.html

<aclass="nav-link" href="{% url 'expense:expense_client' %}">Despesas (Client side)</a>

Atenção: tentar resolver o problema de cors-headers.

Json Server

Instalação

npm install -g json-server 

Crie um db.json

{"expenses":{"data": [{"description": "Lanche", "value": 20, "paid": true },{"description": "Conta de luz", "value": 80, "paid": false },{"description": "Refrigerante", "value": 5.5, "paid": true } ] } } 

Server

json-server --watch db.json 

Na pasta principal, escreva

touch index.html

Mude o endpoint para http://localhost:3000/expenses

<!-- index.html --><!DOCTYPE html><htmllang="en"><head><metacharset="utf-8"><metahttp-equiv="X-UA-Compatible" content="IE=edge"><metaname="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"><linkrel="shortcut icon" href="https://www.djangoproject.com/favicon.ico"><title>htmx</title><!-- Bootstrap core CSS --><linkrel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"><!-- Font-awesome --><linkrel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"><!-- HTMX --><scriptsrc="https://unpkg.com/[email protected]"></script><scriptsrc="https://unpkg.com/[email protected]/dist/ext/client-side-templates.js"></script><scriptsrc="https://cdn.jsdelivr.net/npm/[email protected]/mustache.min.js"></script><style> .no{color: red} .ok{color: green} </style></head><body><h1>Lista de Despesas (Client side)</h1><h3>Consumindo API Rest</h3><divhx-ext="client-side-templates"><!-- http://localhost:8000/expense/json/ --><buttonclass="btn btn-primary" hx-get="http://localhost:3000/expenses" hx-swap="innerHTML" hx-target="#content" mustache-template="foo" > Clique para carregar os dados </button><tableclass="table"><thead><tr><th>Descrição</th><th>Valor</th><thclass="text-center">Pago</th></tr></thead><tbodyid="content"></tbody><templateid="foo"><!-- Mustache looping -->{{#data }} <tr><td>{{description }}</td><td>{{value }}</td><tdclass="text-center"><!-- Mustache conditional --><!-- http://mustache.github.io/mustache.5.html#Inverted-Sections -->{{#paid }} <iclass="fa fa-check-circle ok"></i>{{/paid }}{{^paid }} <iclass="fa fa-times-circle no"></i>{{/paid }} </td></tr>{{/data }} </template></table></div></body></html>

Bookstore

Veremos um exemplo do uso de Bootstrap modal com htmx.

cd backend python ../manage.py startapp bookstore 

Vamos editar:

  • nav.html
  • settings.py
  • urls.py
  • bookstore/apps.py
  • bookstore/views.py
  • bookstore/models.py
  • bookstore/admin.py
  • bookstore/urls.py
  • bookstore/templates/bookstore/book_list.html
  • bookstore/templates/bookstore/book_table.html
  • bookstore/templates/bookstore/hx/book_result_hx.html

Edite nav.html

<liclass="nav-item"><aclass="nav-link" href="{% url 'bookstore:book_list' %}">Livros (Modal)</a></li>

Edite settings.py

INSTALLED_APPS= [ ... 'backend.bookstore', ... ]

Edite urls.py

fromdjango.contribimportadminfromdjango.urlsimportinclude, pathurlpatterns= [ ... path('bookstore/', include('backend.bookstore.urls', namespace='bookstore')), ... ]

Edite bookstore/apps.py

fromdjango.appsimportAppConfigclassBookstoreConfig(AppConfig): default_auto_field='django.db.models.BigAutoField'name='backend.bookstore'

Edite bookstore/views.py

fromdjango.shortcutsimportrenderfromdjango.views.decorators.httpimportrequire_http_methodsfromdjango.views.genericimportListView# from .forms import BookFormfrom .modelsimportBookclassBookListView(ListView): model=Bookpaginate_by=10

Edite bookstore/models.py

fromdjango.dbimportmodelsfromdjango.urlsimportreverse_lazyclassBook(models.Model): title=models.CharField('título', max_length=100, unique=True) author=models.CharField('autor', max_length=100, null=True, blank=True) classMeta: ordering= ('title',) verbose_name='livro'verbose_name_plural='livros'def__str__(self): returnself.titledefget_absolute_url(self): returnreverse_lazy('bookstore:book_detail', kwargs={'pk': self.pk})

Edite bookstore/admin.py

fromdjango.contribimportadminfrom .modelsimportBook@admin.register(Book)classBookAdmin(admin.ModelAdmin): list_display= ('title', 'author') search_fields= ('title', 'author')

Edite bookstore/urls.py

fromdjango.urlsimportpathfrombackend.bookstoreimportviewsasvapp_name='bookstore'urlpatterns= [ path('', v.BookListView.as_view(), name='book_list'), ]
mkdir -p backend/bookstore/templates/bookstore/includes 
cat <<EOF> backend/bookstore/templates/bookstore/book_list.html <!-- book_list.html -->{% extends "base.html" %}{% block content %} <div><divclass="row"><divclass="col-auto"><h1>Lista de Livros</h1></div><divclass="col-auto"><ahref="" class="btn btn-primary">Adicionar</a></div></div><tableclass="table"><thead><tr><th>Título</th><th>Autor</th><th>Ações</th></tr></thead><tbodyid="bookTbody">{% include "./book_table.html" %} </tbody></table></div>{% endblock content %} EOF
cat <<EOF> backend/bookstore/templates/bookstore/book_table.html <!-- book_table.html -->{% for object in object_list %}{% include "./hx/book_result_hx.html" %}{% endfor %} EOF
cat <<EOF> backend/bookstore/templates/bookstore/hx/book_result_hx.html <!-- hx/book_result_hx.html --><trid="trBook{{object.pk }}"><td><ahref="">{{object.title }}</a></td><td>{{object.author|default:'---' }}</td><td></td></tr> EOF

Adicionar

Vamos editar:

  • bookstore/views.py
  • bookstore/urls.py
  • bookstore/forms.py
  • bookstore/templates/bookstore/book_list.html
  • bookstore/templates/bookstore/includes/add_modal.html

Edite bookstore/views.py

... from .formsimportBookFormfrom .modelsimportBookdefbook_create(request): template_name='bookstore/hx/book_form_hx.html'form=BookForm(request.POSTorNone) ifrequest.method=='POST': ifform.is_valid(): book=form.save() template_name='bookstore/hx/book_result_hx.html'context={'object': book} returnrender(request, template_name, context) context={'form': form} returnrender(request, template_name, context)

Edite bookstore/urls.py

... path('create/', v.book_create, name='book_create'), ...
cat<<EOF>backend/bookstore/forms.pyfromdjangoimportformsfrom .modelsimportBookclassBookForm(forms.ModelForm): required_css_class='required'classMeta: model=Bookfields='__all__'def__init__(self, *args, **kwargs): super(BookForm, self).__init__(*args, **kwargs) forfield_name, fieldinself.fields.items(): field.widget.attrs['class'] ='form-control'EOF

Edite backend/bookstore/templates/bookstore/book_list.html

<!-- book_list.html --><ahref="" class="btn btn-primary" data-toggle="modal" data-target="#addModal" hx-get="{% url 'bookstore:book_create' %}" hx-target="#addContent" hx-swap="innerHTML" >Adicionar</a> ...{% include "./includes/add_modal.html" %}{% endblock content %}
cat <<EOF> backend/bookstore/templates/bookstore/includes/add_modal.html <!-- addModal --><divclass="modal fade" id="addModal" tabindex="-1" role="dialog" aria-labelledby="addModalLabel"><divclass="modal-dialog" role="document"><divid="addContent" class="modal-content"><!-- O novo conteúdo será inserido aqui. --><divclass="modal-header"><h4class="modal-title" id="detailModalLabel">Modal title</h4><buttontype="button" class="close" data-dismiss="modal" aria-label="Close"><spanaria-hidden="true">&times;</span></button></div><divclass="modal-body"> ... </div><divclass="modal-footer"><buttontype="button" class="btn btn-default" data-dismiss="modal">Fechar</button><buttontype="submit" class="btn btn-primary">Salvar</button></div></div></div></div> EOF
cat <<EOF> backend/bookstore/templates/bookstore/hx/book_form_hx.html <!-- book_form_hx.html --><divclass="modal-header"><h4class="modal-title" id="addModalLabel">Adicionar Livro</h4><buttontype="button" class="close" data-dismiss="modal" aria-label="Close"><spanaria-hidden="true">&times;</span></button></div><formhx-post="{% url 'bookstore:book_create' %}" hx-target="#bookTbody" hx-indicator=".htmx-indicator" hx-swap="afterbegin" ><divclass="modal-body">{% csrf_token %}{% for field in form %} <divclass="form-group p-2">{{field.label_tag }}{{field }}{{field.errors }}{% if field.help_text %} <smallclass="text-muted">{{field.help_text|safe }}</small>{% endif %} </div>{% endfor %} </div><divclass="modal-footer"><buttontype="button" class="btn btn-default" data-dismiss="modal">Fechar</button><buttontype="submit" class="btn btn-primary">Salvar</button></div></form><script>$('form').on('submit',function(){$('#addModal').modal('toggle')});</script> EOF

Detalhes

Vamos editar:

  • bookstore/views.py
  • bookstore/urls.py
  • bookstore/templates/bookstore/book_detail.html
  • bookstore/templates/bookstore/includes/detail_modal.html
  • bookstore/templates/bookstore/hx/book_result_hx.html
  • bookstore/templates/bookstore/book_list.html

Edite bookstore/views.py

defbook_detail(request, pk): template_name='bookstore/book_detail.html'obj=Book.objects.get(pk=pk) context={'object': obj} returnrender(request, template_name, context)

Edite bookstore/urls.py

... path('<int:pk>/', v.book_detail, name='book_detail'), ...
cat <<EOF> backend/bookstore/templates/bookstore/book_detail.html <divclass="modal-header"><h4class="modal-title" id="detailModalLabel">{{object.title }}</h4><buttontype="button" class="close" data-dismiss="modal" aria-label="Close"><spanaria-hidden="true">&times;</span></button></div><divclass="modal-body"><p>Título <spanclass="float-right">{{object.title }}</span></p><p>Autor <spanclass="float-right">{{object.author|default:'---' }}</span></p></div><divclass="modal-footer"><buttontype="button" class="btn btn-default" data-dismiss="modal">Fechar</button></div> EOF
cat <<EOF> backend/bookstore/templates/bookstore/includes/detail_modal.html <!-- detailModal --><divclass="modal fade" id="detailModal" tabindex="-1" role="dialog" aria-labelledby="detailModalLabel"><divclass="modal-dialog" role="document"><divid="detailContent" class="modal-content"><!-- O novo conteúdo será inserido aqui. --><divclass="modal-header"><h4class="modal-title" id="detailModalLabel">Modal title</h4><buttontype="button" class="close" data-dismiss="modal" aria-label="Close"><spanaria-hidden="true">&times;</span></button></div><divclass="modal-body"> ... </div><divclass="modal-footer"><buttontype="button" class="btn btn-default" data-dismiss="modal">Fechar</button><buttontype="submit" class="btn btn-primary">Salvar</button></div></div></div></div> EOF

Edite backend/bookstore/templates/bookstore/hx/book_result_hx.html

<!-- book_result_hx.html --><trid="trBook{{object.pk }}"><td><ahref="" data-toggle="modal" data-target="#detailModal" hx-get="{{object.get_absolute_url }}" hx-target="#detailContent" hx-indicator=".htmx-indicator" hx-swap="innerHTML" >{{object.title }}</a></td><td>{{object.author|default:'---' }}</td><td></td></tr>

Edite backend/bookstore/templates/bookstore/book_list.html

...{% include "./includes/detail_modal.html" %} ...

Editar

Vamos editar:

  • bookstore/views.py
  • bookstore/urls.py
  • bookstore/templates/bookstore/book_list.html
  • bookstore/templates/bookstore/includes/update_modal.html
  • bookstore/templates/bookstore/hx/book_result_hx.html
  • bookstore/templates/bookstore/book_update_form.html

Edite bookstore/views.py

defbook_update(request, pk): template_name='bookstore/book_update_form.html'instance=Book.objects.get(pk=pk) form=BookForm(request.POSTorNone, instance=instance) ifrequest.method=='POST': ifform.is_valid(): book=form.save() template_name='bookstore/hx/book_result_hx.html'context={'object': book} returnrender(request, template_name, context) context={'form': form, 'object': instance} returnrender(request, template_name, context)

Edite bookstore/urls.py

... path('<int:pk>/update/', v.book_update, name='book_update'), ...

Edite backend/bookstore/templates/bookstore/book_list.html

{% include "./includes/update_modal.html" %}
cat <<EOF> backend/bookstore/templates/bookstore/includes/update_modal.html <!-- updateModal --><divclass="modal fade" id="updateModal" tabindex="-1" role="dialog" aria-labelledby="updateModalLabel"><divclass="modal-dialog" role="document"><divid="updateContent" class="modal-content"><!-- O novo conteúdo será inserido aqui. --><divclass="modal-header"><h4class="modal-title" id="updateModalLabel">Modal title</h4><buttontype="button" class="close" data-dismiss="modal" aria-label="Close"><spanaria-hidden="true">&times;</span></button></div><divclass="modal-body"> ... </div><divclass="modal-footer"><buttontype="button" class="btn btn-default" data-dismiss="modal">Fechar</button><buttontype="submit" class="btn btn-primary">Salvar</button></div></div></div></div> EOF

Editar backend/bookstore/templates/bookstore/hx/book_result_hx.html

<spandata-toggle="modal" data-target="#updateModal" hx-get="{% url 'bookstore:book_update' object.pk %}" hx-target="#updateContent" hx-swap="innerHTML" ><iclass="fa fa-edit link span-is-link"></i></span>
cat <<EOF> backend/bookstore/templates/bookstore/book_update_form.html <divclass="modal-header"><h4class="modal-title" id="updateModalLabel">Editar{{object }}</h4><buttontype="button" class="close" data-dismiss="modal" aria-label="Close"><spanaria-hidden="true">&times;</span></button></div><formhx-post="{% url 'bookstore:book_update' object.pk %}" hx-target="#trBook{{object.pk }}" hx-indicator=".htmx-indicator" hx-swap="outerHTML" ><divclass="modal-body">{% csrf_token %}{% for field in form %} <divclass="form-group p-2">{{field.label_tag }}{{field }}{{field.errors }}{% if field.help_text %} <smallclass="text-muted">{{field.help_text|safe }}</small>{% endif %} </div>{% endfor %} </div><divclass="modal-footer"><buttontype="button" class="btn btn-default" data-dismiss="modal">Fechar</button><buttontype="submit" class="btn btn-primary">Salvar</button></div></form><script>$('form').on('submit',function(){$('#updateModal').modal('toggle')});</script> EOF

Deletar

Vamos editar:

  • bookstore/views.py
  • bookstore/urls.py
  • bookstore/templates/bookstore/book_list.html
  • bookstore/templates/bookstore/hx/book_result_hx.html

Edite bookstore/views.py

@require_http_methods(['DELETE'])defbook_delete(request, pk): template_name='bookstore/book_table.html'obj=Book.objects.get(pk=pk) obj.delete() returnrender(request, template_name)

Edite bookstore/urls.py

... path('<int:pk>/delete/', v.book_delete, name='book_delete'), ...

Editar backend/bookstore/templates/bookstore/book_list.html

{% block js %} <script>// Necessário por causa do deletedocument.body.addEventListener('htmx:configRequest',(event)=>{event.detail.headers['X-CSRFToken']='{{csrf_token }}';});</script>{% endblock js %}

Editar backend/bookstore/templates/bookstore/hx/book_result_hx.html

Ao lado icone de editar.

... <spanhx-delete="{% url 'bookstore:book_delete' object.pk %}" hx-confirm="Deseja mesmo deletar?" hx-target="closest tr" hx-swap="outerHTML swap:500ms" ><iclass="fa fa-trash no span-is-link pl-2"></i></span> ...

Like e Unlike

Vamos editar:

  • bookstore/models.py
  • bookstore/admin.py
  • bookstore/forms.py
  • bookstore/book_list.html
  • bookstore/hx/book_result_hx.html
  • bookstore/urls.py
  • bookstore/views.py

Edite models.py

... like=models.BooleanField(null=True)

Edite admin.py

... list_display= ('title', 'author', 'like') ...

Edite forms.py

... fields= ('title', 'author') ...

Edite book_list.html

<th>Gostou?</th>

Edite hx/book_result_hx.html

<td><spanhx-post="{% url 'bookstore:book_like' object.pk %}" hx-target="#trBook{{object.pk }}" hx-swap="outerHTML" >{% if object.like %} <iclass="fa fa-thumbs-up text-primary"></i>{% else %} <iclass="fa fa-thumbs-o-up text-secondary"></i>{% endif %} </span><spanclass="ml-2" hx-post="{% url 'bookstore:book_unlike' object.pk %}" hx-target="#trBook{{object.pk }}" hx-swap="outerHTML" >{% if not object.like %} <iclass="fa fa-thumbs-down text-danger"></i>{% else %} <iclass="fa fa-thumbs-o-down text-secondary"></i>{% endif %} </span></td>

Edite urls.py

... path('<int:pk>/like/', v.book_like, name='book_like'), path('<int:pk>/unlike/', v.book_unlike, name='book_unlike'),

Edite views.py

@require_http_methods(['POST'])defbook_like(request, pk): template_name='bookstore/hx/book_result_hx.html'book=Book.objects.get(pk=pk) book.like=Truebook.save() context={'object': book} returnrender(request, template_name, context) @require_http_methods(['POST'])defbook_unlike(request, pk): template_name='bookstore/hx/book_result_hx.html'book=Book.objects.get(pk=pk) book.like=Falsebook.save() context={'object': book} returnrender(request, template_name, context)

Criar uma nova categoria

Vamos editar:

  • settings.py
  • urls.py
  • apps.py
  • models.py
  • admin.py
  • urls.py
  • views.py
  • nav.html
  • product_list.html
  • product_table.html
  • includes/add_modal.html
  • hx/product_result_hx.html
  • hx/category_modal_form_hx.html

Edite settings.py

'backend.product',

Edite urls.py

path('product/', include('backend.product.urls', namespace='product')),

Edite apps.py

name='backend.product'

Edite models.py

fromdjango.dbimportmodelsclassCategory(models.Model): title=models.CharField('título', max_length=100, unique=True) classMeta: ordering= ('title',) verbose_name='categoria'verbose_name_plural='categorias'def__str__(self): returnself.titleclassProduct(models.Model): title=models.CharField('título', max_length=100, unique=True) category=models.ForeignKey( Category, on_delete=models.SET_NULL, verbose_name='categoria', related_name='products', null=True, blank=True ) classMeta: ordering= ('title',) verbose_name='produto'verbose_name_plural='produtos'def__str__(self): returnself.title

Edite admin.py

fromdjango.contribimportadminfrom .modelsimportCategory, Product@admin.register(Category)classCategoryAdmin(admin.ModelAdmin): list_display= ('__str__',) search_fields= ('title',) @admin.register(Product)classProductAdmin(admin.ModelAdmin): list_display= ('__str__', 'category') search_fields= ('title', 'category__title')

Edite urls.py

fromdjango.urlsimportpathfrombackend.productimportviewsasvapp_name='product'urlpatterns= [ path('', v.product_list, name='product_list'), path('category/<int:pk>/create/', v.category_create, name='category_create'), ]

Edite views.py

fromdjango.shortcutsimportrenderfrom .modelsimportCategory, Productdefproduct_list(request): template_name='product/product_list.html'object_list=Product.objects.all() categories=Category.objects.all() context={'object_list': object_list, 'categories': categories, } returnrender(request, template_name, context) defcategory_create(request, pk): template_name='product/hx/category_modal_form_hx.html'product=Product.objects.get(pk=pk) ifrequest.method=='POST': title=request.POST.get('categoria') # Cria a nova categoriacategory=Category.objects.create(title=title) # Associa a nova categoria ao produto atual.product.category=categoryproduct.save() template_name='product/hx/product_result_hx.html'categories=Category.objects.all() context={'object': product, 'categories': categories, } returnrender(request, template_name, context) context={'object': product} returnrender(request, template_name, context)

Edite nav.html

<liclass="nav-item"><aclass="nav-link" href="{% url 'product:product_list' %}">Produtos</a></li>

Edite product_list.html

<!-- product_list.html -->{% extends "base.html" %}{% block content %} <div><divclass="row"><divclass="col-auto"><h1>Lista de Produtos</h1></div></div><tableclass="table"><thead><tr><th>Produto</th><th>Categoria</th></tr></thead><tbodyid="productTbody">{% include "./product_table.html" %} </tbody></table></div>{% include "./includes/add_modal.html" %}{% endblock content %}

Edite product_table.html

<!-- product_table.html -->{% for object in object_list %}{% include "./hx/product_result_hx.html" %}{% endfor %}

Edite hx/product_result_hx.html

<!-- hx/product_result_hx.html --><trid="trProduct{{object.pk }}"><td>{{object.title }}</td><td><divclass="row"><divclass="col"><selectid="id_category" name="category" class="form-control" ><optionvalue="">-----</option>{% for category in categories %} <optionvalue="{{category.id}}" {%ifobject.category == category%}selected{%endif%}>{{category.title }}</option>{% endfor %} </select></div><divclass="col-auto d-flex align-items-end"><spandata-toggle="modal" data-target="#addModal" hx-get="{% url 'product:category_create' object.pk %}" hx-target="#addContent" hx-swap="innerHTML" ><iclass="fa fa-plus-circle fa-2x ok"></i></span></div></div></td></tr>

Edite includes/add_modal.html

<!-- addModal --><divclass="modal fade" id="addModal" tabindex="-1" role="dialog" aria-labelledby="addModalLabel"><divclass="modal-dialog" role="document"><divid="addContent" class="modal-content"><!-- O novo conteúdo será adicionado aqui. --></div></div></div>

Edite hx/category_modal_form_hx.html

<!-- category_modal_form_hx.html --><divclass="modal-header"><h4id="detailModalLabel" class="modal-title">Adicionar Categoria para{{object.title }}</h4><buttontype="button" class="close" data-dismiss="modal" aria-label="Close"><spanaria-hidden="true">&times;</span></button></div><formhx-post="{% url 'product:category_create' object.pk %}" hx-target="#trProduct{{object.pk }}" hx-swap="outerHTML" >{% csrf_token %} <divclass="modal-body"><label>Categoria</label><inputid="id_categoria" name="categoria" class="form-control" type="text" /></div><divclass="modal-footer"><buttontype="button" class="btn btn-default" data-dismiss="modal">Fechar</button><buttontype="submit" class="btn btn-primary">Salvar</button></div></form><script>$('form').on('submit',function(){$('#addModal').modal('toggle')});</script>

./manage.py shell_plus

Category.objects.create(title='bebida') Category.objects.create(title='lanche') produtos= [ ('Água mineral', 'bebida'), ('Refrigerante 350ml', 'bebida'), ('Batata frita', 'bebida'), ('Casquinha', ''), ] forprodutoinprodutos: categoria_titulo=produto[1] category=Category.objects.filter(title=categoria_titulo).first() ifcategory: Product.objects.create(title=produto[0], category=category) else: Product.objects.create(title=produto[0])

Trocar a categoria

Vamos editar:

  • hx/product_result_hx.html
  • product_list.html
  • urls.py
  • views.py

Edite hx/product_result_hx.html

<selectid="id_category" name="category" class="form-control" hx-post="{% url 'product:category_update' object.pk %}" hx-swap="none" >

Edite product_list.html

{% block js %} <script>// Por causa do editar categoria.document.body.addEventListener('htmx:configRequest',(event)=>{event.detail.headers['X-CSRFToken']='{{csrf_token }}';});</script>{% endblock js %}

Edite urls.py

path('<int:product_pk>/category/update/', v.category_update, name='category_update'), # noqa E501

Edite views.py

fromdjango.httpimportHttpResponsefromdjango.shortcutsimportrenderfromdjango.views.decorators.httpimportrequire_http_methodsfrom .modelsimportCategory, Product ... @require_http_methods(['POST'])defcategory_update(request, product_pk): product=Product.objects.get(pk=product_pk) category_pk=request.POST.get('category') category=Category.objects.get(pk=category_pk) product.category=categoryproduct.save() returnHttpResponse('ok')

About

Django and htmx tutorial

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published