v0.140.0 ๐จ๐ขยค
โ ๏ธ Major release โ ๏ธ - Please test thoroughly before / after upgrading.
This is the biggest step towards v1. While this version introduces many small API changes, we don't expect to make further changes to the affected parts before v1.
For more details see #433.
Summary:
- Overhauled typing system
- Middleware removed, no longer needed
get_template_data()is the new canonical way to define template data.get_context_data()is now deprecated but will remain until v2.- Slots API polished and prepared for v1.
- Merged
Component.UrlwithComponent.View - Added
Component.args,Component.kwargs,Component.slots,Component.context - Added
{{ component_vars.args }},{{ component_vars.kwargs }},{{ component_vars.slots }} - You should no longer instantiate
Componentinstances. Instead, callComponent.render()orComponent.render_to_response()directly. - Component caching can now consider slots (opt-in)
- And lot more...
BREAKING CHANGES ๐จ๐ขยค
Middleware
-
The middleware
ComponentDependencyMiddlewarewas removed as it is no longer needed.The middleware served one purpose - to render the JS and CSS dependencies of components when you rendered templates with
Template.render()ordjango.shortcuts.render()and those templates contained{% component %}tags.- NOTE: If you rendered HTML with
Component.render()orComponent.render_to_response(), the JS and CSS were already rendered.
Now, the JS and CSS dependencies of components are automatically rendered, even when you render Templates with
Template.render()ordjango.shortcuts.render().To disable this behavior, set the
DJC_DEPS_STRATEGYcontext key to"ignore"when rendering the template:# With `Template.render()`: template = Template(template_str) rendered = template.render(Context({"DJC_DEPS_STRATEGY": "ignore"})) # Or with django.shortcuts.render(): from django.shortcuts import render rendered = render( request, "my_template.html", context={"DJC_DEPS_STRATEGY": "ignore"}, )In fact, you can set the
DJC_DEPS_STRATEGYcontext key to any of the strategies:"document""fragment""simple""prepend""append""ignore"
See Dependencies rendering for more info.
- NOTE: If you rendered HTML with
Typing
-
Component typing no longer uses generics. Instead, the types are now defined as class attributes of the component class.
Before:
After:
See Migrating from generics to class attributes for more info. - Removed
EmptyTupleandEmptyDicttypes. Instead, there is now a singleEmptytype.
Component API
-
The interface of the not-yet-released
get_js_data()andget_css_data()methods has changed to matchget_template_data().Before:
After:
-
Arguments in
Component.render_to_response()have changed to match that ofComponent.render().Please ensure that you pass the parameters as kwargs, not as positional arguments, to avoid breaking changes.
The signature changed, moving the
argsandkwargsparameters to 2nd and 3rd position.Next, the
render_dependenciesparameter was added to matchComponent.render().Lastly:
- Previously, any extra ARGS and KWARGS were passed to the
response_class. - Now, only extra KWARGS will be passed to the
response_class.
Before:
def render_to_response( cls, context: Optional[Union[Dict[str, Any], Context]] = None, slots: Optional[SlotsType] = None, escape_slots_content: bool = True, args: Optional[ArgsType] = None, kwargs: Optional[KwargsType] = None, deps_strategy: DependenciesStrategy = "document", request: Optional[HttpRequest] = None, *response_args: Any, **response_kwargs: Any, ) -> HttpResponse:After:
def render_to_response( context: Optional[Union[Dict[str, Any], Context]] = None, args: Optional[Any] = None, kwargs: Optional[Any] = None, slots: Optional[Any] = None, deps_strategy: DependenciesStrategy = "document", type: Optional[DependenciesStrategy] = None, # Deprecated, use `deps_strategy` render_dependencies: bool = True, # Deprecated, use `deps_strategy="ignore"` outer_context: Optional[Context] = None, request: Optional[HttpRequest] = None, registry: Optional[ComponentRegistry] = None, registered_name: Optional[str] = None, node: Optional[ComponentNode] = None, **response_kwargs: Any, ) -> HttpResponse: - Previously, any extra ARGS and KWARGS were passed to the
-
Component.render()andComponent.render_to_response()NO LONGER acceptescape_slots_contentkwarg.Instead, slots are now always escaped.
To disable escaping, wrap the result of
slotsinmark_safe().Before:
After:
-
Component.templateno longer accepts a Template instance, only plain string.Before:
Instead, either:
-
Set
Component.templateto a plain string. -
Move the template to it's own HTML file and set
Component.template_file. -
Or, if you dynamically created the template, render the template inside
Component.on_render().
-
-
Subclassing of components with
Nonevalues has changed:Previously, when a child component's template / JS / CSS attributes were set to
None, the child component still inherited the parent's template / JS / CSS.Now, the child component will not inherit the parent's template / JS / CSS if it sets the attribute to
None.Before:
class Parent(Component): template = "parent.html" class Child(Parent): template = None # Child still inherited parent's template assert Child.template == Parent.templateAfter:
-
The
Component.Urlclass was merged withComponent.View.Instead of
Component.Url.public, useComponent.View.public.If you imported
ComponentUrlfromdjango_components, you need to update your import toComponentView.Before:
class MyComponent(Component): class Url: public = True class View: def get(self, request): return self.render_to_response()After:
-
Caching - The function signatures of
Component.Cache.get_cache_key()andComponent.Cache.hash()have changed to enable passing slots.Args and kwargs are no longer spread, but passed as a list and a dict, respectively.
Before:
def get_cache_key(self, *args: Any, **kwargs: Any) -> str: def hash(self, *args: Any, **kwargs: Any) -> str:After:
Template tags
-
Component name in the
{% component %}tag can no longer be set as a kwarg.Instead, the component name MUST be the first POSITIONAL argument only.
Before, it was possible to set the component name as a kwarg and put it anywhere in the
{% component %}tag:Now, the component name MUST be the first POSITIONAL argument:
Thus, the
namekwarg can now be used as a regular input.
Slots
-
If you instantiated
Slotclass with kwargs, you should now usecontentsinstead ofcontent_func.Before:
After:
Alternatively, pass the function / content as first positional argument:
-
The undocumented
Slot.escapedattribute was removed.Instead, slots are now always escaped.
To disable escaping, wrap the result of
slotsinmark_safe(). -
Slot functions behavior has changed. See the new Slots docs for more info.
-
Function signature:
-
All parameters are now passed under a single
ctxargument.You can still access all the same parameters via
ctx.context,ctx.data, andctx.fallback. -
contextandfallbacknow may beNoneif the slot function was called outside of{% slot %}tag.
Before:
def slot_fn(context: Context, data: Dict, slot_ref: SlotRef): isinstance(context, Context) isinstance(data, Dict) isinstance(slot_ref, SlotRef) return "CONTENT"After:
-
-
Calling slot functions:
-
Rather than calling the slot functions directly, you should now call the
Slotinstances. -
All parameters are now optional.
-
The order of parameters has changed.
Before:
def slot_fn(context: Context, data: Dict, slot_ref: SlotRef): return "CONTENT" html = slot_fn(context, data, slot_ref)After:
-
-
Usage in components:
Before:
class MyComponent(Component): def get_context_data(self, *args, **kwargs): slots = self.input.slots slot_fn = slots["my_slot"] html = slot_fn(context, data, slot_ref) return { "html": html, }After:
-
Miscellaneous
-
The second argument to
render_dependencies()is nowstrategyinstead oftype.Before:
After:
Deprecation ๐จ๐ขยค
Component API
-
Component.get_context_data()is now deprecated. UseComponent.get_template_data()instead.get_template_data()behaves the same way, but has a different function signature to accept also slots and context.Since
get_context_data()is widely used, it will remain available until v2. -
Component.get_template_name()andComponent.get_template()are now deprecated. UseComponent.template,Component.template_fileorComponent.on_render()instead.Component.get_template_name()andComponent.get_template()will be removed in v1.In v1, each Component will have at most one static template. This is needed to enable support for Markdown, Pug, or other pre-processing of templates by extensions.
If you are using the deprecated methods to point to different templates, there's 2 ways to migrate:
-
Split the single Component into multiple Components, each with its own template. Then switch between them in
Component.on_render(): -
Alternatively, use
Component.on_render()with Django'sget_template()to dynamically render different templates:
Read more in django-components#1204.
-
-
The
typekwarg inComponent.render()andComponent.render_to_response()is now deprecated. Usedeps_strategyinstead. Thetypekwarg will be removed in v1.Before:
After:
-
The
render_dependencieskwarg inComponent.render()andComponent.render_to_response()is now deprecated. Usedeps_strategy="ignore"instead. Therender_dependencieskwarg will be removed in v1.Before:
After:
-
Support for
Componentconstructor kwargsregistered_name,outer_context, andregistryis deprecated, and will be removed in v1.Before, you could instantiate a standalone component, and then call
render()on the instance:comp = MyComponent( registered_name="my_component", outer_context=my_context, registry=my_registry, ) comp.render( args=[1, 2, 3], kwargs={"a": 1, "b": 2}, slots={"my_slot": "CONTENT"}, )Now you should instead pass all that data to
Component.render()/Component.render_to_response(): -
Component.input(and its typeComponentInput) is now deprecated. Theinputproperty will be removed in v1.Instead, use attributes directly on the Component instance.
Before:
class MyComponent(Component): def on_render(self, context, template): assert self.input.args == [1, 2, 3] assert self.input.kwargs == {"a": 1, "b": 2} assert self.input.slots == {"my_slot": "CONTENT"} assert self.input.context == {"my_slot": "CONTENT"} assert self.input.deps_strategy == "document" assert self.input.type == "document" assert self.input.render_dependencies == TrueAfter:
class MyComponent(Component): def on_render(self, context, template): assert self.args == [1, 2, 3] assert self.kwargs == {"a": 1, "b": 2} assert self.slots == {"my_slot": "CONTENT"} assert self.context == {"my_slot": "CONTENT"} assert self.deps_strategy == "document" assert (self.deps_strategy != "ignore") is True -
Component method
on_render_afterwas updated to receive alsoerrorfield.For backwards compatibility, the
errorfield can be omitted until v1.Before:
After:
-
If you are using the Components as views, the way to access the component class is now different.
Instead of
self.component, useself.component_cls.self.componentwill be removed in v1.Before:
class MyView(View): def get(self, request): return self.component.render_to_response(request=request)After:
Extensions
-
In the
on_component_data()extension hook, thecontext_datafield of the context object was superseded bytemplate_data.The
context_datafield will be removed in v1.0.Before:
class MyExtension(ComponentExtension): def on_component_data(self, ctx: OnComponentDataContext) -> None: ctx.context_data["my_template_var"] = "my_value"After:
-
When creating extensions, the
ComponentExtension.ExtensionClassattribute was renamed toComponentConfig.The old name is deprecated and will be removed in v1.
Before:
from django_components import ComponentExtension class MyExtension(ComponentExtension): class ExtensionClass(ComponentExtension.ExtensionClass): passAfter:
-
When creating extensions, to access the Component class from within the methods of the extension nested classes, use
component_cls.Previously this field was named
component_class. The old name is deprecated and will be removed in v1.
ComponentExtension.ExtensionClass attribute was renamed to ComponentConfig.
The old name is deprecated and will be removed in v1.
Before:
```py
from django_components import ComponentExtension, ExtensionComponentConfig
class LoggerExtension(ComponentExtension):
name = "logger"
class ComponentConfig(ExtensionComponentConfig):
def log(self, msg: str) -> None:
print(f"{self.component_class.__name__}: {msg}")
```
After:
```py
from django_components import ComponentExtension, ExtensionComponentConfig
class LoggerExtension(ComponentExtension):
name = "logger"
class ComponentConfig(ExtensionComponentConfig):
def log(self, msg: str) -> None:
print(f"{self.component_cls.__name__}: {msg}")
```
Slots
-
SlotContentwas renamed toSlotInput. The old name is deprecated and will be removed in v1. -
SlotRefwas renamed toSlotFallback. The old name is deprecated and will be removed in v1. -
The
defaultkwarg in{% fill %}tag was renamed tofallback. The old name is deprecated and will be removed in v1.Before:
After:
-
The template variable
{{ component_vars.is_filled }}is now deprecated. Will be removed in v1. Use{{ component_vars.slots }}instead.Before:
After:
NOTE:
component_vars.is_filledautomatically escaped slot names, so that even slot names that are not valid python identifiers could be set as slot names.component_vars.slotsno longer does that. -
Component attribute
Component.is_filledis now deprecated. Will be removed in v1. UseComponent.slotsinstead.Before:
class MyComponent(Component): def get_template_data(self, args, kwargs, slots, context): if self.is_filled.footer: color = "red" else: color = "blue" return { "color": color, }After:
class MyComponent(Component): def get_template_data(self, args, kwargs, slots, context): if "footer" in slots: color = "red" else: color = "blue" return { "color": color, }NOTE:
Component.is_filledautomatically escaped slot names, so that even slot names that are not valid python identifiers could be set as slot names.Component.slotsno longer does that.
Miscellaneous
-
Template caching with
cached_template()helper andtemplate_cache_sizesetting is deprecated. These will be removed in v1.This feature made sense if you were dynamically generating templates for components using
Component.get_template_string()andComponent.get_template().However, in v1, each Component will have at most one static template. This static template is cached internally per component class, and reused across renders.
This makes the template caching feature obsolete.
If you relied on
cached_template(), you should either:- Wrap the templates as Components.
- Manage the cache of Templates yourself.
-
The
debug_highlight_componentsanddebug_highlight_slotssettings are deprecated. These will be removed in v1.The debug highlighting feature was re-implemented as an extension. As such, the recommended way for enabling it has changed:
Before:
After:
Set
extensions_defaultsin yoursettings.pyfile.COMPONENTS = ComponentsSettings( extensions_defaults={ "debug_highlight": { "highlight_components": True, "highlight_slots": True, }, }, )Alternatively, you can enable highlighting for specific components by setting
Component.DebugHighlight.highlight_componentstoTrue:
Featยค
-
New method to render template variables -
get_template_data()get_template_data()behaves the same way asget_context_data(), but has a different function signature to accept also slots and context.class Button(Component): def get_template_data(self, args, kwargs, slots, context): return { "val1": args[0], "val2": kwargs["field"], }If you define
Component.Args,Component.Kwargs,Component.Slots, then theargs,kwargs,slotsarguments will be instances of these classes: -
Input validation is now part of the render process.
When you specify the input types (such as
Component.Args,Component.Kwargs, etc), the actual inputs to data methods (Component.get_template_data(), etc) will be instances of the types you specified.This practically brings back input validation, because the instantiation of the types will raise an error if the inputs are not valid.
Read more on Typing and validation
-
Render emails or other non-browser HTML with new "dependencies strategies"
When rendering a component with
Component.render()orComponent.render_to_response(), thedeps_strategykwarg (previouslytype) now accepts additional options:"simple""prepend""append""ignore"
Calendar.render_to_response( request=request, kwargs={ "date": request.GET.get("date", ""), }, deps_strategy="append", )Comparison of dependencies render strategies:
"document"- Smartly inserts JS / CSS into placeholders or into
<head>and<body>tags. - Inserts extra script to allow
fragmentstrategy to work. - Assumes the HTML will be rendered in a JS-enabled browser.
- Smartly inserts JS / CSS into placeholders or into
"fragment"- A lightweight HTML fragment to be inserted into a document with AJAX.
- Ignores placeholders and any
<head>/<body>tags. - No JS / CSS included.
"simple"- Smartly insert JS / CSS into placeholders or into
<head>and<body>tags. - No extra script loaded.
- Smartly insert JS / CSS into placeholders or into
"prepend"- Insert JS / CSS before the rendered HTML.
- Ignores placeholders and any
<head>/<body>tags. - No extra script loaded.
"append"- Insert JS / CSS after the rendered HTML.
- Ignores placeholders and any
<head>/<body>tags. - No extra script loaded.
"ignore"- Rendered HTML is left as-is. You can still process it with a different strategy later with
render_dependencies(). - Used for inserting rendered HTML into other components.
- Rendered HTML is left as-is. You can still process it with a different strategy later with
See Dependencies rendering for more info.
-
New
Component.args,Component.kwargs,Component.slotsattributes available on the component class itself.These attributes are the same as the ones available in
Component.get_template_data().You can use these in other methods like
Component.on_render_before()orComponent.on_render_after().from django_components import Component, SlotInput class Table(Component): class Args(NamedTuple): page: int class Kwargs(NamedTuple): per_page: int class Slots(NamedTuple): content: SlotInput def on_render_before(self, context: Context, template: Optional[Template]) -> None: assert self.args.page == 123 assert self.kwargs.per_page == 10 content_html = self.slots.content()Same as with the parameters in
Component.get_template_data(), they will be instances of theArgs,Kwargs,Slotsclasses if defined, or plain lists / dictionaries otherwise. -
4 attributes that were previously available only under the
Component.inputattribute are now available directly on the Component instance:Component.raw_argsComponent.raw_kwargsComponent.raw_slotsComponent.deps_strategy
The first 3 attributes are the same as the deprecated
Component.input.args,Component.input.kwargs,Component.input.slotsproperties.Compared to the
Component.args/Component.kwargs/Component.slotsattributes, these "raw" attributes are not typed and will remain as plain lists / dictionaries even if you define theArgs,Kwargs,Slotsclasses.The
Component.deps_strategyattribute is the same as the deprecatedComponent.input.deps_strategyproperty. -
New template variables
{{ component_vars.args }},{{ component_vars.kwargs }},{{ component_vars.slots }}These attributes are the same as the ones available in
Component.get_template_data().{# Typed #} {% if component_vars.args.page == 123 %} <div> {% slot "content" / %} </div> {% endif %} {# Untyped #} {% if component_vars.args.0 == 123 %} <div> {% slot "content" / %} </div> {% endif %}Same as with the parameters in
Component.get_template_data(), they will be instances of theArgs,Kwargs,Slotsclasses if defined, or plain lists / dictionaries otherwise. -
New component lifecycle hook
Component.on_render().This hook is called when the component is being rendered.
You can override this method to:
- Change what template gets rendered
- Modify the context
- Modify the rendered output after it has been rendered
- Handle errors
See on_render for more info.
-
get_component_url()now optionally acceptsqueryandfragmentarguments. -
The
BaseNodeclass has a newcontentsattribute, which contains the raw contents (string) of the tag body.This is relevant when you define custom template tags with
@template_tagdecorator orBaseNodeclass.When you define a custom template tag like so:
from django_components import BaseNode, template_tag @template_tag( library, tag="mytag", end_tag="endmytag", allowed_flags=["required"] ) def mytag(node: BaseNode, context: Context, name: str, **kwargs) -> str: print(node.contents) return f"Hello, {name}!"And render it like so:
Then, the
contentsattribute of theBaseNodeinstance will contain the string"Hello, world!". -
The
BaseNodeclass also has two new metadata attributes:template_name- the name of the template that rendered the node.template_component- the component class that the template belongs to.
This is useful for debugging purposes.
-
Slotclass now has 3 new metadata fields:-
Slot.contentsattribute contains the original contents:- If
Slotwas created from{% fill %}tag,Slot.contentswill contain the body of the{% fill %}tag. - If
Slotwas created from string viaSlot("..."),Slot.contentswill contain that string. - If
Slotwas created from a function,Slot.contentswill contain that function.
- If
-
Slot.extraattribute where you can put arbitrary metadata about the slot. -
Slot.fill_nodeattribute tells where the slot comes from:FillNodeinstance if the slot was created from{% fill %}tag.ComponentNodeinstance if the slot was created as a default slot from a{% component %}tag.Noneif the slot was created from a string, function, orSlotinstance.
See Slot metadata.
-
-
{% fill %}tag now acceptsbodykwarg to pass a Slot instance to fill.First pass a
Slotinstance to the template with theget_template_data()method:from django_components import component, Slot class Table(Component): def get_template_data(self, args, kwargs, slots, context): return { "my_slot": Slot(lambda ctx: "Hello, world!"), }Then pass the slot to the
{% fill %}tag: -
You can now access the
{% component %}tag (ComponentNodeinstance) from which a Component was created. UseComponent.nodeto access it.This is mostly useful for extensions, which can use this to detect if the given Component comes from a
{% component %}tag or from a different source (such asComponent.render()).Component.nodeisNoneif the component is created byComponent.render()(but you can pass in thenodekwarg yourself). -
Node classes
ComponentNode,FillNode,ProvideNode, andSlotNodeare part of the public API.These classes are what is instantiated when you use
{% component %},{% fill %},{% provide %}, and{% slot %}tags.You can for example use these for type hints:
from django_components import Component, ComponentNode class MyTable(Component): def get_template_data(self, args, kwargs, slots, context): if kwargs.get("show_owner"): node: Optional[ComponentNode] = self.node owner: Optional[Component] = self.node.template_component else: node = None owner = None return { "owner": owner, "node": node, } -
Component caching can now take slots into account, by setting
Component.Cache.include_slotstoTrue.In which case the following two calls will generate separate cache entries:
{% component "my_component" position="left" %} Hello, Alice {% endcomponent %} {% component "my_component" position="left" %} Hello, Bob {% endcomponent %}Same applies to
Component.render()with string slots:MyComponent.render( kwargs={"position": "left"}, slots={"content": "Hello, Alice"} ) MyComponent.render( kwargs={"position": "left"}, slots={"content": "Hello, Bob"} )Read more on Component caching.
-
New extension hook
on_slot_rendered()This hook is called when a slot is rendered, and allows you to access and/or modify the rendered result.
This is used by the "debug highlight" feature.
To modify the rendered result, return the new value:
class MyExtension(ComponentExtension): def on_slot_rendered(self, ctx: OnSlotRenderedContext) -> Optional[str]: return ctx.result + "<!-- Hello, world! -->"If you don't want to modify the rendered result, return
None.See all Extension hooks.
-
When creating extensions, the previous syntax with
ComponentExtension.ExtensionClasswas causing Mypy errors, because Mypy doesn't allow using class attributes as bases:Before:
from django_components import ComponentExtension class MyExtension(ComponentExtension): class ExtensionClass(ComponentExtension.ExtensionClass): # Error! passInstead, you can import
ExtensionComponentConfigdirectly:After:
Refactorยค
-
When a component is being rendered, a proper
Componentinstance is now created.Previously, the
Componentstate was managed as half-instance, half-stack. -
Component's "Render API" (args, kwargs, slots, context, inputs, request, context data, etc) can now be accessed also outside of the render call. So now its possible to take the component instance out of
get_template_data()(although this is not recommended). -
Components can now be defined without a template.
Previously, the following would raise an error:
"Template-less" components can be used together with
Component.on_render()to dynamically pick what to render:class TableNew(Component): template_file = "table_new.html" class TableOld(Component): template_file = "table_old.html" class Table(Component): def on_render(self, context, template): if self.kwargs.get("feat_table_new_ui"): return TableNew.render(args=self.args, kwargs=self.kwargs, slots=self.slots) else: return TableOld.render(args=self.args, kwargs=self.kwargs, slots=self.slots)"Template-less" components can be also used as a base class for other components, or as mixins.
-
Passing
Slotinstance toSlotconstructor raises an error. -
Extension hook
on_component_renderednow receiveserrorfield.on_component_renderednow behaves similar toComponent.on_render_after:- Raising error in this hook overrides what error will be returned from
Component.render(). - Returning new string overrides what will be returned from
Component.render().
Before:
class OnComponentRenderedContext(NamedTuple): component: "Component" component_cls: Type["Component"] component_id: str result: strAfter:
- Raising error in this hook overrides what error will be returned from
Fixยค
-
Fix bug: Context processors data was being generated anew for each component. Now the data is correctly created once and reused across components with the same request (#1165).
-
Fix KeyError on
component_context_cachewhen slots are rendered outside of the component's render context. (#1189) -
Component classes now have
do_not_call_in_templates=Trueto prevent them from being called as functions in templates.