diff --git a/CHANGELOG.md b/CHANGELOG.md index 23310079..89b23fa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,10 @@ Summary: `get_context_data()` is now deprecated but will remain until v2. - Slots API polished and prepared for v1. - Merged `Component.Url` with `Component.View` -- Added `Component.args`, `Component.kwargs`, `Component.slots` +- 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, call `Component.render()` or `Component.render_to_response()` directly. +- Component caching can now consider slots (opt-in) - And lot more... #### 🚨📢 BREAKING CHANGES @@ -867,6 +869,8 @@ Summary: 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). +- Passing `Slot` instance to `Slot` constructor raises an error. + #### 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](https://github.com/django-components/django-components/issues/1165)). diff --git a/docs/concepts/fundamentals/slots.md b/docs/concepts/fundamentals/slots.md index b7841be4..d7b37131 100644 --- a/docs/concepts/fundamentals/slots.md +++ b/docs/concepts/fundamentals/slots.md @@ -640,15 +640,25 @@ Table.render( ) ``` -Slot class can be instantiated with a function, a string, or from another -[`Slot`](../../../reference/api#django_components.Slot) instance: +Slot class can be instantiated with a function or a string: ```py slot1 = Slot(lambda ctx: f"Hello, {ctx.data['name']}!") slot2 = Slot("Hello, world!") -slot3 = Slot(slot1) ``` +!!! warning + + Passing a [`Slot`](../../../reference/api#django_components.Slot) instance to the `Slot` + constructor results in an error: + + ```py + slot = Slot("Hello") + + # Raises an error + slot2 = Slot(slot) + ``` + ### Rendering slots **Python** @@ -713,12 +723,13 @@ html = slot() When accessing slots from within [`Component`](../../../reference/api#django_components.Component) methods, the [`Slot`](../../../reference/api#django_components.Slot) instances are populated -with extra metadata [`component_name`](../../../reference/api#django_components.Slot.component_name) -and [`slot_name`](../../../reference/api#django_components.Slot.slot_name). +with extra metadata [`component_name`](../../../reference/api#django_components.Slot.component_name), +[`slot_name`](../../../reference/api#django_components.Slot.slot_name), and +[`nodelist`](../../../reference/api#django_components.Slot.nodelist). -These are used solely for debugging. +These are used for debugging, such as printing the path to the slot in the component tree. -In fact, you can set these fields too when creating new slots: +When you create a slot, you can set these fields too: ```py # Either at slot creation @@ -753,19 +764,6 @@ slot = Slot("Hello!") print(slot.nodelist) # ``` -!!! info - - If you pass a [`Slot`](../../../reference/api#django_components.Slot) instance to the constructor, - the inner slot will be "unwrapped" and its `Slot.contents` will be used instead. - - ```py - slot = Slot("Hello") - print(slot.contents) # "Hello" - - slot2 = Slot(slot) - print(slot2.contents) # "Hello" - ``` - ### Escaping slots content Slots content are automatically escaped by default to prevent XSS attacks. diff --git a/docs/reference/api.md b/docs/reference/api.md index 8844d1e3..c4ac905c 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -87,6 +87,14 @@ options: show_if_no_docstring: true +::: django_components.SlotContext + options: + show_if_no_docstring: true + +::: django_components.SlotFallback + options: + show_if_no_docstring: true + ::: django_components.SlotFunc options: show_if_no_docstring: true diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 240770e2..8ea93d65 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -2,7 +2,7 @@ # Commands -These are all the [Django management commands](https://docs.djangoproject.com/en/5.1/ref/django-admin) +These are all the [Django management commands](https://docs.djangoproject.com/en/5.2/ref/django-admin) that will be added by installing `django_components`: @@ -54,9 +54,7 @@ python manage.py components ext run ## `components create` ```txt -usage: python manage.py components create [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] - [--dry-run] - name +usage: python manage.py components create [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run] name ``` @@ -238,7 +236,7 @@ List all extensions. - `--columns COLUMNS` - Comma-separated list of columns to show. Available columns: name. Defaults to `--columns name`. - `-s`, `--simple` - - Only show table data, without headers. Use this option for generating machine- readable output. + - Only show table data, without headers. Use this option for generating machine-readable output. @@ -386,7 +384,7 @@ usage: python manage.py components list [-h] [--all] [--columns COLUMNS] [-s] -See source code +See source code @@ -401,7 +399,7 @@ List all components created in this project. - `--columns COLUMNS` - Comma-separated list of columns to show. Available columns: name, full_name, path. Defaults to `--columns full_name,path`. - `-s`, `--simple` - - Only show table data, without headers. Use this option for generating machine- readable output. + - Only show table data, without headers. Use this option for generating machine-readable output. @@ -463,9 +461,8 @@ ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAc ## `upgradecomponent` ```txt -usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS] - [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] - [--skip-checks] +usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] + [--force-color] [--skip-checks] ``` @@ -509,10 +506,8 @@ Deprecated. Use `components upgrade` instead. ## `startcomponent` ```txt -usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] - [--verbose] [--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS] - [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] - [--skip-checks] +usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run] [--version] [-v {0,1,2,3}] + [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks] name ``` diff --git a/docs/reference/extension_hooks.md b/docs/reference/extension_hooks.md index 39a49bd9..f4e7fdfb 100644 --- a/docs/reference/extension_hooks.md +++ b/docs/reference/extension_hooks.md @@ -85,9 +85,9 @@ name | type | description `component` | [`Component`](../api#django_components.Component) | The Component instance that received the input and is being rendered `component_cls` | [`Type[Component]`](../api#django_components.Component) | The Component class `component_id` | `str` | The unique identifier for this component instance -`context` | [`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context) | The Django template Context object +`context` | [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context) | The Django template Context object `kwargs` | `Dict` | Dictionary of keyword arguments passed to the component -`slots` | `Dict` | Dictionary of slot definitions +`slots` | `Dict[str, Slot]` | Dictionary of slot definitions ::: django_components.extension.ComponentExtension.on_component_registered options: @@ -181,6 +181,30 @@ name | type | description --|--|-- `registry` | [`ComponentRegistry`](../api#django_components.ComponentRegistry) | The to-be-deleted ComponentRegistry instance +::: django_components.extension.ComponentExtension.on_slot_rendered + options: + heading_level: 3 + show_root_heading: true + show_signature: true + separate_signature: true + show_symbol_type_heading: false + show_symbol_type_toc: false + show_if_no_docstring: true + show_labels: false + +**Available data:** + +name | type | description +--|--|-- +`component` | [`Component`](../api#django_components.Component) | The Component instance that contains the `{% slot %}` tag +`component_cls` | [`Type[Component]`](../api#django_components.Component) | The Component class that contains the `{% slot %}` tag +`component_id` | `str` | The unique identifier for this component instance +`result` | `SlotResult` | The rendered result of the slot +`slot` | `Slot` | The Slot instance that was rendered +`slot_is_default` | `bool` | Whether the slot is default +`slot_is_required` | `bool` | Whether the slot is required +`slot_name` | `str` | The name of the `{% slot %}` tag + ## Objects ::: django_components.extension.OnComponentClassCreatedContext diff --git a/docs/reference/signals.md b/docs/reference/signals.md index eab706c6..c03b50be 100644 --- a/docs/reference/signals.md +++ b/docs/reference/signals.md @@ -6,7 +6,7 @@ Below are the signals that are sent by or during the use of django-components. ## template_rendered -Django's [`template_rendered`](https://docs.djangoproject.com/en/5.1/ref/signals/#template-rendered) signal. +Django's [`template_rendered`](https://docs.djangoproject.com/en/5.2/ref/signals/#template-rendered) signal. This signal is sent when a template is rendered. Django-components triggers this signal when a component is rendered. If there are nested components, diff --git a/docs/reference/template_tags.md b/docs/reference/template_tags.md index 10330341..93a0acbf 100644 --- a/docs/reference/template_tags.md +++ b/docs/reference/template_tags.md @@ -20,7 +20,7 @@ Import as -See source code +See source code @@ -43,7 +43,7 @@ If you insert this tag multiple times, ALL CSS links will be duplicately inserte -See source code +See source code @@ -67,7 +67,7 @@ If you insert this tag multiple times, ALL JS scripts will be duplicately insert -See source code +See source code @@ -120,7 +120,7 @@ and other tags: ### Isolating components By default, components behave similarly to Django's -[`{% include %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#include), +[`{% include %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#include), and the template inside the component has access to the variables defined in the outer template. You can selectively isolate a component, using the `only` flag, so that the inner template @@ -163,34 +163,32 @@ COMPONENTS = { ## fill ```django -{% fill name: str, *, data: Optional[str] = None, default: Optional[str] = None %} +{% fill name: str, *, data: Optional[str] = None, fallback: Optional[str] = None, body: Union[str, django.utils.safestring.SafeString, django_components.slots.SlotFunc[~TSlotData], django_components.slots.Slot[~TSlotData], NoneType] = None, default: Optional[str] = None %} {% endfill %} ``` -See source code +See source code Use this tag to insert content into component's slots. -`{% fill %}` tag may be used only within a `{% component %}..{% endcomponent %}` block. -Runtime checks should prohibit other usages. +`{% fill %}` tag may be used only within a `{% component %}..{% endcomponent %}` block, +and raises a `TemplateSyntaxError` if used outside of a component. **Args:** - `name` (str, required): Name of the slot to insert this content into. Use `"default"` for the default slot. -- `default` (str, optional): This argument allows you to access the original content of the slot - under the specified variable name. See - [Accessing original content of slots](../../concepts/fundamentals/slots#accessing-original-content-of-slots) - `data` (str, optional): This argument allows you to access the data passed to the slot - under the specified variable name. See [Scoped slots](../../concepts/fundamentals/slots#scoped-slots) + under the specified variable name. See [Slot data](../../concepts/fundamentals/slots#slot-data). +- `fallback` (str, optional): This argument allows you to access the original content of the slot + under the specified variable name. See [Slot fallback](../../concepts/fundamentals/slots#slot-fallback). -**Examples:** +**Example:** -Basic usage: ```django {% component "my_table" %} {% fill "pagination" %} @@ -199,7 +197,15 @@ Basic usage: {% endcomponent %} ``` -### Accessing slot's default content with the `default` kwarg +### Access slot fallback + +Use the `fallback` kwarg to access the original content of the slot. + +The `fallback` kwarg defines the name of the variable that will contain the slot's fallback content. + +Read more about [Slot fallback](../../concepts/fundamentals/slots#slot-fallback). + +Component template: ```django {# my_table.html #} @@ -211,17 +217,27 @@ Basic usage: ``` +Fill: + ```django {% component "my_table" %} - {% fill "pagination" default="default_pag" %} + {% fill "pagination" fallback="fallback" %}
- {{ default_pag }} + {{ fallback }}
{% endfill %} {% endcomponent %} ``` -### Accessing slot's data with the `data` kwarg +### Access slot data + +Use the `data` kwarg to access the data passed to the slot. + +The `data` kwarg defines the name of the variable that will contain the slot's data. + +Read more about [Slot data](../../concepts/fundamentals/slots#slot-data). + +Component template: ```django {# my_table.html #} @@ -233,6 +249,8 @@ Basic usage: ``` +Fill: + ```django {% component "my_table" %} {% fill "pagination" data="slot_data" %} @@ -245,20 +263,60 @@ Basic usage: {% endcomponent %} ``` -### Accessing slot data and default content on the default slot +### Using default slot -To access slot data and the default slot content on the default slot, +To access slot data and the fallback slot content on the default slot, use `{% fill %}` with `name` set to `"default"`: ```django {% component "button" %} - {% fill name="default" data="slot_data" default="default_slot" %} + {% fill name="default" data="slot_data" fallback="slot_fallback" %} You clicked me {{ slot_data.count }} times! - {{ default_slot }} + {{ slot_fallback }} {% endfill %} {% endcomponent %} ``` +### Slot fills from Python + +You can pass a slot fill from Python to a component by setting the `body` kwarg +on the `{% fill %}` tag. + +First pass a [`Slot`](../api#django_components.Slot) instance to the template +with the [`get_template_data()`](../api#django_components.Component.get_template_data) +method: + +```python +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: + +```django +{% component "table" %} + {% fill "pagination" body=my_slot / %} +{% endcomponent %} +``` + +!!! warning + + If you define both the `body` kwarg and the `{% fill %}` tag's body, + an error will be raised. + + ```django + {% component "table" %} + {% fill "pagination" body=my_slot %} + ... + {% endfill %} + {% endcomponent %} + ``` + ## html_attrs ```django @@ -274,8 +332,7 @@ use `{% fill %}` with `name` set to `"default"`: Generate HTML attributes (`key="value"`), combining data from multiple sources, whether its template variables or static text. -It is designed to easily merge HTML attributes passed from outside with the internal. -See how to in [Passing HTML attributes to components](../../guides/howto/passing_html_attrs/). +It is designed to easily merge HTML attributes passed from outside as well as inside the component. **Args:** @@ -318,8 +375,8 @@ renders
``` -**See more usage examples in -[HTML attributes](../../concepts/fundamentals/html_attributes#examples-for-html_attrs).** +See more usage examples in +[HTML attributes](../../concepts/fundamentals/html_attributes#examples-for-html_attrs). ## provide @@ -352,7 +409,7 @@ or Vue's [`provide()`](https://vuejs.org/guide/components/provide-inject). Provide the "user_data" in parent component: -```python +```djc_py @register("parent") class Parent(Component): template = """ @@ -372,7 +429,7 @@ class Parent(Component): Since the "child" component is used within the `{% provide %} / {% endprovide %}` tags, we can request the "user_data" using `Component.inject("user_data")`: -```python +```djc_py @register("child") class Child(Component): template = """ @@ -410,7 +467,7 @@ user = self.inject("user_data")["user"] -See source code +See source code @@ -435,7 +492,7 @@ or [React's `children`](https://react.dev/learn/passing-props-to-a-component#pas **Example:** -```python +```djc_py @register("child") class Child(Component): template = """ @@ -450,7 +507,7 @@ class Child(Component): """ ``` -```python +```djc_py @register("parent") class Parent(Component): template = """ @@ -468,12 +525,14 @@ class Parent(Component): """ ``` -### Passing data to slots +### Slot data Any extra kwargs will be considered as slot data, and will be accessible in the [`{% fill %}`](#fill) tag via fill's `data` kwarg: -```python +Read more about [Slot data](../../concepts/fundamentals/slots#slot-data). + +```djc_py @register("child") class Child(Component): template = """ @@ -486,7 +545,7 @@ class Child(Component): """ ``` -```python +```djc_py @register("parent") class Parent(Component): template = """ @@ -501,35 +560,35 @@ class Parent(Component): """ ``` -### Accessing default slot content +### Slot fallback -The content between the `{% slot %}..{% endslot %}` tags is the default content that +The content between the `{% slot %}..{% endslot %}` tags is the fallback content that will be rendered if no fill is given for the slot. -This default content can then be accessed from within the [`{% fill %}`](#fill) tag using -the fill's `default` kwarg. +This fallback content can then be accessed from within the [`{% fill %}`](#fill) tag using +the fill's `fallback` kwarg. This is useful if you need to wrap / prepend / append the original slot's content. -```python +```djc_py @register("child") class Child(Component): template = """
{% slot "content" %} - This is default content! + This is fallback content! {% endslot %}
""" ``` -```python +```djc_py @register("parent") class Parent(Component): template = """ - {# Parent can access the slot's default content #} + {# Parent can access the slot's fallback content #} {% component "child" %} - {% fill "content" default="default" %} - {{ default }} + {% fill "content" fallback="fallback" %} + {{ fallback }} {% endfill %} {% endcomponent %} """ diff --git a/docs/reference/template_vars.md b/docs/reference/template_vars.md index efbb4fc5..1188eaf0 100644 --- a/docs/reference/template_vars.md +++ b/docs/reference/template_vars.md @@ -7,5 +7,11 @@ template and in [`on_render_before` / `on_render_after`](../concepts/advanced/ho hooks. +::: django_components.component.ComponentVars.args + +::: django_components.component.ComponentVars.kwargs + +::: django_components.component.ComponentVars.slots + ::: django_components.component.ComponentVars.is_filled diff --git a/src/django_components/component.py b/src/django_components/component.py index bd4e5251..4162e8b4 100644 --- a/src/django_components/component.py +++ b/src/django_components/component.py @@ -56,7 +56,7 @@ from django_components.extension import ( extensions, ) from django_components.extensions.cache import ComponentCache -from django_components.extensions.defaults import ComponentDefaults, apply_defaults, defaults_by_component +from django_components.extensions.defaults import ComponentDefaults from django_components.extensions.view import ComponentView, ViewFn from django_components.node import BaseNode from django_components.perfutil.component import ComponentRenderer, component_context_cache, component_post_render @@ -2798,22 +2798,11 @@ class Component(metaclass=ComponentMeta): render_id = _gen_component_id() - # Apply defaults to missing or `None` values in `kwargs` - defaults = defaults_by_component.get(comp_cls, None) - if defaults is not None: - apply_defaults(kwargs_dict, defaults) - - # If user doesn't specify `Args`, `Kwargs`, `Slots` types, then we pass them in as plain - # dicts / lists. - args_inst = comp_cls.Args(*args_list) if comp_cls.Args is not None else args_list - kwargs_inst = comp_cls.Kwargs(**kwargs_dict) if comp_cls.Kwargs is not None else kwargs_dict - slots_inst = comp_cls.Slots(**slots_dict) if comp_cls.Slots is not None else slots_dict - component = comp_cls( id=render_id, - args=args_inst, - kwargs=kwargs_inst, - slots=slots_inst, + args=args_list, + kwargs=kwargs_dict, + slots=slots_dict, context=context, request=request, deps_strategy=deps_strategy, @@ -2841,6 +2830,12 @@ class Component(metaclass=ComponentMeta): if result_override is not None: return result_override + # If user doesn't specify `Args`, `Kwargs`, `Slots` types, then we pass them in as plain + # dicts / lists. + component.args = comp_cls.Args(*args_list) if comp_cls.Args is not None else args_list + component.kwargs = comp_cls.Kwargs(**kwargs_dict) if comp_cls.Kwargs is not None else kwargs_dict + component.slots = comp_cls.Slots(**slots_dict) if comp_cls.Slots is not None else slots_dict + ###################################### # 2. Prepare component state ###################################### diff --git a/src/django_components/extension.py b/src/django_components/extension.py index 337dbbbf..3a951255 100644 --- a/src/django_components/extension.py +++ b/src/django_components/extension.py @@ -92,7 +92,7 @@ class OnComponentInputContext(NamedTuple): """List of positional arguments passed to the component""" kwargs: Dict """Dictionary of keyword arguments passed to the component""" - slots: Dict + slots: Dict[str, "Slot"] """Dictionary of slot definitions""" context: Context """The Django template Context object""" @@ -498,6 +498,19 @@ class ComponentExtension: # Add extra kwarg to all components when they are rendered ctx.kwargs["my_input"] = "my_value" ``` + + !!! warning + + In this hook, the components' inputs are still mutable. + + As such, if a component defines [`Args`](../api#django_components.Component.Args), + [`Kwargs`](../api#django_components.Component.Kwargs), + [`Slots`](../api#django_components.Component.Slots) types, these types are NOT yet instantiated. + + Instead, component fields like [`Component.args`](../api#django_components.Component.args), + [`Component.kwargs`](../api#django_components.Component.kwargs), + [`Component.slots`](../api#django_components.Component.slots) + are plain `list` / `dict` objects. """ pass diff --git a/src/django_components/extensions/defaults.py b/src/django_components/extensions/defaults.py index c3ed8172..2a597f3f 100644 --- a/src/django_components/extensions/defaults.py +++ b/src/django_components/extensions/defaults.py @@ -3,7 +3,7 @@ from dataclasses import MISSING, Field, dataclass from typing import TYPE_CHECKING, Any, Callable, Dict, List, NamedTuple, Optional, Type from weakref import WeakKeyDictionary -from django_components.extension import ComponentExtension, OnComponentClassCreatedContext +from django_components.extension import ComponentExtension, OnComponentClassCreatedContext, OnComponentInputContext if TYPE_CHECKING: from django_components.component import Component @@ -99,7 +99,7 @@ def _extract_defaults(defaults: Optional[Type]) -> List[ComponentDefaultField]: return defaults_fields -def apply_defaults(kwargs: Dict, defaults: List[ComponentDefaultField]) -> None: +def _apply_defaults(kwargs: Dict, defaults: List[ComponentDefaultField]) -> None: """ Apply the defaults from `Component.Defaults` to the given `kwargs`. @@ -171,3 +171,11 @@ class DefaultsExtension(ComponentExtension): def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None: defaults_cls = getattr(ctx.component_cls, "Defaults", None) defaults_by_component[ctx.component_cls] = _extract_defaults(defaults_cls) + + # Apply defaults to missing or `None` values in `kwargs` + def on_component_input(self, ctx: OnComponentInputContext) -> None: + defaults = defaults_by_component.get(ctx.component_cls, None) + if defaults is None: + return + + _apply_defaults(ctx.kwargs, defaults) diff --git a/src/django_components/provide.py b/src/django_components/provide.py index 9c5e9bf7..16cb5909 100644 --- a/src/django_components/provide.py +++ b/src/django_components/provide.py @@ -30,7 +30,7 @@ class ProvideNode(BaseNode): Provide the "user_data" in parent component: - ```python + ```djc_py @register("parent") class Parent(Component): template = \"\"\" @@ -50,7 +50,7 @@ class ProvideNode(BaseNode): Since the "child" component is used within the `{% provide %} / {% endprovide %}` tags, we can request the "user_data" using `Component.inject("user_data")`: - ```python + ```djc_py @register("child") class Child(Component): template = \"\"\" diff --git a/src/django_components/slots.py b/src/django_components/slots.py index 69bd2a80..8fb2a2b1 100644 --- a/src/django_components/slots.py +++ b/src/django_components/slots.py @@ -52,7 +52,23 @@ FILL_BODY_KWARG = "body" # Public types SlotResult = Union[str, SafeString] -"""Type representing the result of a slot render function.""" +""" +Type representing the result of a slot render function. + +**Example:** + +```python +from django_components import SlotContext, SlotResult + +def my_slot_fn(ctx: SlotContext) -> SlotResult: + return "Hello, world!" + +my_slot = Slot(my_slot_fn) +html = my_slot() # Output: Hello, world! +``` + +Read more about [Slot functions](../../concepts/fundamentals/slots#slot-functions). +""" @dataclass(frozen=True) @@ -65,9 +81,9 @@ class SlotContext(Generic[TSlotData]): **Example:** ```python - from django_components import SlotContext + from django_components import SlotContext, SlotResult - def my_slot(ctx: SlotContext): + def my_slot(ctx: SlotContext) -> SlotResult: return f"Hello, {ctx.data['name']}!" ``` @@ -216,8 +232,6 @@ class Slot(Generic[TSlotData]): the body (string) of that `{% fill %}` tag. - If Slot was created from string as `Slot("...")`, `Slot.contents` will contain that string. - If Slot was created from a function, `Slot.contents` will contain that function. - - If Slot was created from another `Slot` as `Slot(slot)`, `Slot.contents` will contain the inner - slot's `Slot.contents`. Read more about [Slot contents](../../concepts/fundamentals/slots#slot-contents). """ @@ -232,28 +246,30 @@ class Slot(Generic[TSlotData]): # Following fields are only for debugging component_name: Optional[str] = None - """Name of the component that originally received this slot fill.""" + """ + Name of the component that originally received this slot fill. + + See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata). + """ slot_name: Optional[str] = None - """Slot name to which this Slot was initially assigned.""" + """ + Slot name to which this Slot was initially assigned. + + See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata). + """ nodelist: Optional[NodeList] = None """ If the slot was defined with [`{% fill %}`](../template_tags#fill) tag, this will be the Nodelist of the fill's content. + + See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata). """ def __post_init__(self) -> None: - # Since the `Slot` instance is treated as a function, it may be passed as `contents` - # to the `Slot()` constructor. In that case we need to unwrap to the original value - # if `Slot()` constructor got another Slot instance. - # NOTE: If `Slot` was passed as `contents`, we do NOT take the metadata from the inner Slot instance. - # Instead we treat is simply as a function. - # NOTE: Try to avoid infinite loop if `Slot.contents` points to itself. - seen_contents = set() - while isinstance(self.contents, Slot) and self.contents not in seen_contents: - seen_contents.add(id(self.contents)) - self.contents = self.contents.contents - if id(self.contents) in seen_contents: - raise ValueError("Detected infinite loop in `Slot.contents` pointing to itself") + # Raise if Slot received another Slot instance as `contents`, + # because this leads to ambiguity about how to handle the metadata. + if isinstance(self.contents, Slot): + raise ValueError("Slot received another Slot instance as `contents`") if self.content_func is None: self.contents, new_nodelist, self.content_func = self._resolve_contents(self.contents) @@ -261,7 +277,7 @@ class Slot(Generic[TSlotData]): self.nodelist = new_nodelist if not callable(self.content_func): - raise ValueError(f"Slot content must be a callable, got: {self.content_func}") + raise ValueError(f"Slot 'content_func' must be a callable, got: {self.content_func}") # Allow to treat the instances as functions def __call__( @@ -374,20 +390,30 @@ SlotName = str class SlotFallback: """ - SlotFallback allows to treat a slot fallback as a variable. The slot is rendered only once - the instance is coerced to string. + The content between the `{% slot %}..{% endslot %}` tags is the *fallback* content that + will be rendered if no fill is given for the slot. - This is used to access slots as variables inside the templates. When a `SlotFallback` - is rendered in the template with `{{ my_lazy_slot }}`, it will output the contents - of the slot. + ```django + {% slot "name" %} + Hello, my name is {{ name }} + {% endslot %} + ``` - Usage in slot functions: + Because the fallback is defined as a piece of the template + ([`NodeList`](https://github.com/django/django/blob/ddb85294159185c5bd5cae34c9ef735ff8409bfe/django/template/base.py#L1017)), + we want to lazily render it only when needed. + + `SlotFallback` type allows to pass around the slot fallback as a variable. + + To force the fallback to render, coerce it to string to trigger the `__str__()` method. + + **Example:** ```py def slot_function(self, ctx: SlotContext): return f"Hello, {ctx.fallback}!" ``` - """ + """ # noqa: E501 def __init__(self, slot: "SlotNode", context: Context): self._slot = slot @@ -400,6 +426,9 @@ class SlotFallback: # TODO_v1 - REMOVE - superseded by SlotFallback SlotRef = SlotFallback +""" +DEPRECATED: Use [`SlotFallback`](../api#django_components.SlotFallback) instead. Will be removed in v1. +""" name_escape_re = re.compile(r"[^\w]") @@ -457,7 +486,7 @@ class SlotNode(BaseNode): **Example:** - ```python + ```djc_py @register("child") class Child(Component): template = \"\"\" @@ -472,7 +501,7 @@ class SlotNode(BaseNode): \"\"\" ``` - ```python + ```djc_py @register("parent") class Parent(Component): template = \"\"\" @@ -490,12 +519,14 @@ class SlotNode(BaseNode): \"\"\" ``` - ### Passing data to slots + ### Slot data Any extra kwargs will be considered as slot data, and will be accessible in the [`{% fill %}`](#fill) tag via fill's `data` kwarg: - ```python + Read more about [Slot data](../../concepts/fundamentals/slots#slot-data). + + ```djc_py @register("child") class Child(Component): template = \"\"\" @@ -508,7 +539,7 @@ class SlotNode(BaseNode): \"\"\" ``` - ```python + ```djc_py @register("parent") class Parent(Component): template = \"\"\" @@ -523,7 +554,7 @@ class SlotNode(BaseNode): \"\"\" ``` - ### Accessing fallback slot content + ### Slot fallback The content between the `{% slot %}..{% endslot %}` tags is the fallback content that will be rendered if no fill is given for the slot. @@ -532,7 +563,7 @@ class SlotNode(BaseNode): the fill's `fallback` kwarg. This is useful if you need to wrap / prepend / append the original slot's content. - ```python + ```djc_py @register("child") class Child(Component): template = \"\"\" @@ -544,7 +575,7 @@ class SlotNode(BaseNode): \"\"\" ``` - ```python + ```djc_py @register("parent") class Parent(Component): template = \"\"\" @@ -910,21 +941,20 @@ class FillNode(BaseNode): """ Use this tag to insert content into component's slots. - `{% fill %}` tag may be used only within a `{% component %}..{% endcomponent %}` block. - Runtime checks should prohibit other usages. + `{% fill %}` tag may be used only within a `{% component %}..{% endcomponent %}` block, + and raises a `TemplateSyntaxError` if used outside of a component. **Args:** - `name` (str, required): Name of the slot to insert this content into. Use `"default"` for the default slot. - - `fallback` (str, optional): This argument allows you to access the original content of the slot - under the specified variable name. See [Slot fallback](../../concepts/fundamentals/slots#slot-fallback). - `data` (str, optional): This argument allows you to access the data passed to the slot under the specified variable name. See [Slot data](../../concepts/fundamentals/slots#slot-data). + - `fallback` (str, optional): This argument allows you to access the original content of the slot + under the specified variable name. See [Slot fallback](../../concepts/fundamentals/slots#slot-fallback). - **Examples:** + **Example:** - Basic usage: ```django {% component "my_table" %} {% fill "pagination" %} @@ -933,7 +963,15 @@ class FillNode(BaseNode): {% endcomponent %} ``` - ### Accessing slot's fallback content with the `fallback` kwarg + ### Access slot fallback + + Use the `fallback` kwarg to access the original content of the slot. + + The `fallback` kwarg defines the name of the variable that will contain the slot's fallback content. + + Read more about [Slot fallback](../../concepts/fundamentals/slots#slot-fallback). + + Component template: ```django {# my_table.html #} @@ -945,6 +983,8 @@ class FillNode(BaseNode): ``` + Fill: + ```django {% component "my_table" %} {% fill "pagination" fallback="fallback" %} @@ -955,7 +995,15 @@ class FillNode(BaseNode): {% endcomponent %} ``` - ### Accessing slot's data with the `data` kwarg + ### Access slot data + + Use the `data` kwarg to access the data passed to the slot. + + The `data` kwarg defines the name of the variable that will contain the slot's data. + + Read more about [Slot data](../../concepts/fundamentals/slots#slot-data). + + Component template: ```django {# my_table.html #} @@ -967,6 +1015,8 @@ class FillNode(BaseNode): ``` + Fill: + ```django {% component "my_table" %} {% fill "pagination" data="slot_data" %} @@ -979,7 +1029,7 @@ class FillNode(BaseNode): {% endcomponent %} ``` - ### Accessing slot data and fallback content on the default slot + ### Using default slot To access slot data and the fallback slot content on the default slot, use `{% fill %}` with `name` set to `"default"`: @@ -993,7 +1043,7 @@ class FillNode(BaseNode): {% endcomponent %} ``` - ### Passing slot fill from Python + ### Slot fills from Python You can pass a slot fill from Python to a component by setting the `body` kwarg on the `{% fill %}` tag. @@ -1189,6 +1239,7 @@ class FillNode(BaseNode): ####################################### # EXTRACTING {% fill %} FROM TEMPLATES +# (internal) ####################################### diff --git a/tests/test_slots.py b/tests/test_slots.py index 2a044980..534c39af 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -116,6 +116,15 @@ class TestSlot: slots={"first": "SLOT_FN"}, ) + def test_render_raises_on_slot_instance_in_slot_constructor(self): + slot: Slot = Slot(lambda ctx: "SLOT_FN") + + with pytest.raises( + ValueError, + match=re.escape("Slot received another Slot instance as `contents`"), + ): + Slot(slot) + def test_render_slot_in_python__minimal(self): def slot_fn(ctx: SlotContext): assert ctx.context is None