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.Url
withComponent.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
Component
instances. Instead, callComponent.render()
orComponent.render_to_response()
directly. - Component caching can now consider slots (opt-in)
- And lot more...
BREAKING CHANGES ๐จ๐ขยค
Middleware
-
The middleware
ComponentDependencyMiddleware
was 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_STRATEGY
context 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_STRATEGY
context 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
EmptyTuple
andEmptyDict
types. Instead, there is now a singleEmpty
type.
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
args
andkwargs
parameters to 2nd and 3rd position.Next, the
render_dependencies
parameter 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_content
kwarg.Instead, slots are now always escaped.
To disable escaping, wrap the result of
slots
inmark_safe()
.Before:
After:
-
Component.template
no longer accepts a Template instance, only plain string.Before:
Instead, either:
-
Set
Component.template
to 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
None
values 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.template
After:
-
The
Component.Url
class was merged withComponent.View
.Instead of
Component.Url.public
, useComponent.View.public
.If you imported
ComponentUrl
fromdjango_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
name
kwarg can now be used as a regular input.
Slots
-
If you instantiated
Slot
class with kwargs, you should now usecontents
instead ofcontent_func
.Before:
After:
Alternatively, pass the function / content as first positional argument:
-
The undocumented
Slot.escaped
attribute was removed.Instead, slots are now always escaped.
To disable escaping, wrap the result of
slots
inmark_safe()
. -
Slot functions behavior has changed. See the new Slots docs for more info.
-
Function signature:
-
All parameters are now passed under a single
ctx
argument.You can still access all the same parameters via
ctx.context
,ctx.data
, andctx.fallback
. -
context
andfallback
now may beNone
if 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
Slot
instances. -
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 nowstrategy
instead 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_file
orComponent.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
type
kwarg inComponent.render()
andComponent.render_to_response()
is now deprecated. Usedeps_strategy
instead. Thetype
kwarg will be removed in v1.Before:
After:
-
The
render_dependencies
kwarg inComponent.render()
andComponent.render_to_response()
is now deprecated. Usedeps_strategy="ignore"
instead. Therender_dependencies
kwarg will be removed in v1.Before:
After:
-
Support for
Component
constructor kwargsregistered_name
,outer_context
, andregistry
is 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. Theinput
property 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 == True
After:
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_after
was updated to receive alsoerror
field.For backwards compatibility, the
error
field 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.component
will 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_data
field of the context object was superseded bytemplate_data
.The
context_data
field 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.ExtensionClass
attribute 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): pass
After:
-
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
-
SlotContent
was renamed toSlotInput
. The old name is deprecated and will be removed in v1. -
SlotRef
was renamed toSlotFallback
. The old name is deprecated and will be removed in v1. -
The
default
kwarg 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_filled
automatically escaped slot names, so that even slot names that are not valid python identifiers could be set as slot names.component_vars.slots
no longer does that. -
Component attribute
Component.is_filled
is now deprecated. Will be removed in v1. UseComponent.slots
instead.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_filled
automatically escaped slot names, so that even slot names that are not valid python identifiers could be set as slot names.Component.slots
no longer does that.
Miscellaneous
-
Template caching with
cached_template()
helper andtemplate_cache_size
setting 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_components
anddebug_highlight_slots
settings 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_defaults
in yoursettings.py
file.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_components
toTrue
:
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
,slots
arguments 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_strategy
kwarg (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
fragment
strategy 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.slots
attributes 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
,Slots
classes if defined, or plain lists / dictionaries otherwise. -
4 attributes that were previously available only under the
Component.input
attribute are now available directly on the Component instance:Component.raw_args
Component.raw_kwargs
Component.raw_slots
Component.deps_strategy
The first 3 attributes are the same as the deprecated
Component.input.args
,Component.input.kwargs
,Component.input.slots
properties.Compared to the
Component.args
/Component.kwargs
/Component.slots
attributes, these "raw" attributes are not typed and will remain as plain lists / dictionaries even if you define theArgs
,Kwargs
,Slots
classes.The
Component.deps_strategy
attribute is the same as the deprecatedComponent.input.deps_strategy
property. -
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
,Slots
classes 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 acceptsquery
andfragment
arguments. -
The
BaseNode
class has a newcontents
attribute, which contains the raw contents (string) of the tag body.This is relevant when you define custom template tags with
@template_tag
decorator orBaseNode
class.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
contents
attribute of theBaseNode
instance will contain the string"Hello, world!"
. -
The
BaseNode
class 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.
-
Slot
class now has 3 new metadata fields:-
Slot.contents
attribute contains the original contents:- If
Slot
was created from{% fill %}
tag,Slot.contents
will contain the body of the{% fill %}
tag. - If
Slot
was created from string viaSlot("...")
,Slot.contents
will contain that string. - If
Slot
was created from a function,Slot.contents
will contain that function.
- If
-
Slot.extra
attribute where you can put arbitrary metadata about the slot. -
Slot.fill_node
attribute tells where the slot comes from:FillNode
instance if the slot was created from{% fill %}
tag.ComponentNode
instance if the slot was created as a default slot from a{% component %}
tag.None
if the slot was created from a string, function, orSlot
instance.
See Slot metadata.
-
-
{% fill %}
tag now acceptsbody
kwarg to pass a Slot instance to fill.First pass a
Slot
instance 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 (ComponentNode
instance) from which a Component was created. UseComponent.node
to 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.node
isNone
if the component is created byComponent.render()
(but you can pass in thenode
kwarg yourself). -
Node classes
ComponentNode
,FillNode
,ProvideNode
, andSlotNode
are 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_slots
toTrue
.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.ExtensionClass
was 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! pass
Instead, you can import
ExtensionComponentConfig
directly:After:
Refactorยค
-
When a component is being rendered, a proper
Component
instance is now created.Previously, the
Component
state 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
Slot
instance toSlot
constructor raises an error. -
Extension hook
on_component_rendered
now receiveserror
field.on_component_rendered
now 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: str
After:
- 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_cache
when slots are rendered outside of the component's render context. (#1189) -
Component classes now have
do_not_call_in_templates=True
to prevent them from being called as functions in templates.