Forms overview#
A Django form usually costs a URL entry, a view, manual CSRF handling, and a redirect-on-success per action before it accepts a single POST.
The forms subsystem removes that wiring.
Declaring a subclass of next.forms.Form or next.forms.ModelForm is enough to make that form reachable by name from every template its scope covers, with a POST endpoint, CSRF, and re-render-on-failure already attached.
No decorator, no manual registry call, and no URL wiring is required.
Auto-registration#
Every subclass of next.forms.BaseForm or next.forms.BaseModelForm registers itself through the __init_subclass__ hook the moment Python executes the class statement.
The framework derives the action name from the class name by converting CamelCase to snake_case.
ArticleEditForm becomes article_edit_form, ContactForm becomes contact_form.
The framework also records which file the class was declared in and uses that to decide its scope. Actions is the canonical reference for name derivation and scope.
article_edit_form#from django.http import HttpRequest
import next.forms
from next.forms import redirect_to_origin
class ArticleEditForm(next.forms.ModelForm):
class Meta:
model = Article
fields = ["title", "body"]
def on_valid(self, request: HttpRequest):
self.save()
return redirect_to_origin(request)
Why a stable URL#
The framework hashes the action’s scope key and name into a single POST endpoint at /_next/form/<uid>/.
The URL is derived deterministically from the class, so a form needs no URL wiring and no per-page route.
The same form can be embedded on any page that renders its tag and every copy submits to the same endpoint.
Scope#
A form declared in page.py or component.py is page-scoped and keyed to its file, so two pages may each declare an ArticleEditForm without collision.
A form declared in any other file is shared, carries a project-wide name, and is reachable from any template.
contact_form (shared)#from django.http import HttpRequest
import next.forms
from next.forms import redirect_to_origin
class ContactForm(next.forms.Form):
email = next.forms.EmailField()
def on_valid(self, request: HttpRequest):
send_email(self.cleaned_data["email"])
return redirect_to_origin(request)
Set Meta.scope to "page" or "shared" to pin the scope regardless of file name.
See Actions for the anchor-file rule, the Meta.scope override, and the next.E047 check.
Autodiscover#
NextFrameworkConfig.ready calls autodiscover_forms() once on startup.
It imports the forms submodule of every installed app so shared forms declared in app/forms.py register before the first request arrives.
Set NEXT_FRAMEWORK["FORM_AUTODISCOVER"] = False to disable the automatic import.
Handling submissions#
Override on_valid to run code after the form passes validation.
The framework injects the current request only into a parameter annotated HttpRequest, an unannotated request parameter resolves to None.
The method may declare any other parameter the dependency injector knows how to resolve.
from django.http import HttpRequest
def on_valid(self, request: HttpRequest):
self.save()
return redirect_to_origin(request)
The default implementation redirects to the origin page, and a ModelForm saves first. See Actions for the exact default behaviour and return contract.
Meta.success_url and Meta.success_message declare the redirect target and a flash message without writing on_valid by hand.
The default on_valid reads both, so a save-and-redirect form can skip the method entirely.
get_initial prepopulates the form before the first render.
Declare it as a classmethod with the same DI-friendly signature.
from django.http import HttpRequest
@classmethod
def get_initial(cls, request: HttpRequest, note_id: int | None = None):
if note_id is None:
return {}
return Note.objects.get(pk=note_id)
The framework calls get_initial through the dependency injector, never application code.
The framework supplies request to any parameter annotated HttpRequest, an unannotated request parameter resolves to None.
A parameter named after a URL segment is filled from the URL, and the rest resolve through providers.
See Actions for the full signature rules.
Form-less actions#
Use @action to register a plain function when no form fields are needed.
A logout button or a delete confirmation is a typical case.
The name is optional.
A bare @action registers the function under its own name.
from django.http import HttpRequest
from next import action
from next.forms import redirect_to_origin
from next.urls import DUrl
@action("delete_article")
def delete_article(article_id: DUrl["id", int], request: HttpRequest):
Article.objects.filter(pk=article_id).delete()
return redirect_to_origin(request)
The template tag works the same way, but form is None inside the block because there is no form class.
Template usage#
The {% form "name" %} block tag renders the <form> element, injects the CSRF token, and publishes form inside the block body.
{% form "article_edit_form" %}
{{ form.title }}
{{ form.body }}
<button type="submit">Save</button>
{% endform %}
See Form templates for the full tag reference.
See also#
See also
Actions for auto-registration details, name derivation, and system checks.
Form templates for the {% form %} tag.
Validation and re-render for the re-render pipeline.
Action backends for swapping the dispatch backend.