mirror of
https://github.com/django-components/django-components.git
synced 2025-08-09 16:57:59 +00:00
feat: dependency injection with inject/provide (#506)
This commit is contained in:
parent
9bfb50b8f2
commit
8ca2814ee3
10 changed files with 1032 additions and 7 deletions
130
README.md
130
README.md
|
@ -33,6 +33,7 @@ Read on to learn about the details!
|
||||||
- [Using slots in templates](#using-slots-in-templates)
|
- [Using slots in templates](#using-slots-in-templates)
|
||||||
- [Passing data to components](#passing-data-to-components)
|
- [Passing data to components](#passing-data-to-components)
|
||||||
- [Rendering HTML attributes](#rendering-html-attributes)
|
- [Rendering HTML attributes](#rendering-html-attributes)
|
||||||
|
- [Prop drilling and dependency injection (provide / inject)](#prop-drilling-and-dependency-injection-provide--inject)
|
||||||
- [Component context and scope](#component-context-and-scope)
|
- [Component context and scope](#component-context-and-scope)
|
||||||
- [Rendering JS and CSS dependencies](#rendering-js-and-css-dependencies)
|
- [Rendering JS and CSS dependencies](#rendering-js-and-css-dependencies)
|
||||||
- [Available settings](#available-settings)
|
- [Available settings](#available-settings)
|
||||||
|
@ -44,6 +45,8 @@ Read on to learn about the details!
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
|
||||||
|
**Version 0.80** introduces dependency injection with the `{% provide %}` tag and `inject()` method.
|
||||||
|
|
||||||
🚨📢 **Version 0.79**
|
🚨📢 **Version 0.79**
|
||||||
|
|
||||||
- BREAKING CHANGE: Default value for the `COMPONENTS.context_behavior` setting was changes from `"isolated"` to `"django"`. If you did not set this value explicitly before, this may be a breaking change. See the rationale for change [here](https://github.com/EmilStenstrom/django-components/issues/498).
|
- BREAKING CHANGE: Default value for the `COMPONENTS.context_behavior` setting was changes from `"isolated"` to `"django"`. If you did not set this value explicitly before, this may be a breaking change. See the rationale for change [here](https://github.com/EmilStenstrom/django-components/issues/498).
|
||||||
|
@ -1363,6 +1366,133 @@ attributes_to_string(attrs)
|
||||||
# 'class="my-class text-red pa-4" data-id="123" required'
|
# 'class="my-class text-red pa-4" data-id="123" required'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Prop drilling and dependency injection (provide / inject)
|
||||||
|
|
||||||
|
_New in version 0.80_:
|
||||||
|
|
||||||
|
Django components supports dependency injection with the combination of:
|
||||||
|
1. `{% provide %}` tag
|
||||||
|
1. `inject()` method of the `Component` class
|
||||||
|
|
||||||
|
### What is "dependency injection" and "prop drilling"?
|
||||||
|
|
||||||
|
Prop drilling refers to a scenario in UI development where you need to pass data through many layers of a component tree to reach the nested components that actually need the data.
|
||||||
|
|
||||||
|
Normally, you'd use props to send data from a parent component to its children. However, this straightforward method becomes cumbersome and inefficient if the data has to travel through many levels or if several components scattered at different depths all need the same piece of information.
|
||||||
|
|
||||||
|
This results in a situation where the intermediate components, which don't need the data for their own functioning, end up having to manage and pass along these props. This clutters the component tree and makes the code verbose and harder to manage.
|
||||||
|
|
||||||
|
A neat solution to avoid prop drilling is using the "provide and inject" technique, AKA dependency injection.
|
||||||
|
|
||||||
|
With dependency injection, a parent component acts like a data hub for all its descendants. This setup allows any component, no matter how deeply nested it is, to access the required data directly from this centralized provider without having to messily pass props down the chain. This approach significantly cleans up the code and makes it easier to maintain.
|
||||||
|
|
||||||
|
This feature is inspired by Vue's [Provide / Inject](https://vuejs.org/guide/components/provide-inject) and React's [Context / useContext](https://react.dev/learn/passing-data-deeply-with-context).
|
||||||
|
|
||||||
|
### How to use provide / inject
|
||||||
|
|
||||||
|
As the name suggest, using provide / inject consists of 2 steps
|
||||||
|
1. Providing data
|
||||||
|
2. Injecting provided data
|
||||||
|
|
||||||
|
For examples of advanced uses of provide / inject, [see this discussion](https://github.com/EmilStenstrom/django-components/pull/506#issuecomment-2132102584).
|
||||||
|
|
||||||
|
### Using `{% provide %}` tag
|
||||||
|
|
||||||
|
First we use the `{% provide %}` tag to define the data we want to "provide" (make available).
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% provide "my_data" key="hi" another=123 %}
|
||||||
|
{% component "child" %} <--- Can access "my_data"
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
|
||||||
|
{% component "child" %} <--- Cannot access "my_data"
|
||||||
|
{% endcomponent %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice that the `provide` tag REQUIRES a name as a first argument. This is the _key_ by which we can then access the data passed to this tag.
|
||||||
|
|
||||||
|
`provide` tag _key_, similarly to the _name_ argument in `component` or `slot` tags, has these requirements:
|
||||||
|
- The _key_ must be a string literal
|
||||||
|
- It must be a valid identifier (AKA a valid Python variable name)
|
||||||
|
|
||||||
|
Once you've set the name, you define the data you want to "provide" by passing it as keyword arguments. This is similar to how you pass data to the `{% with %}` tag.
|
||||||
|
|
||||||
|
> NOTE: Kwargs passed to `{% provide %}` are NOT added to the context.
|
||||||
|
> In the example below, the `{{ key }}` won't render anything:
|
||||||
|
> ```django
|
||||||
|
> {% provide "my_data" key="hi" another=123 %}
|
||||||
|
> {{ key }}
|
||||||
|
> {% endprovide %}
|
||||||
|
> ```
|
||||||
|
|
||||||
|
### Using `inject()` method
|
||||||
|
|
||||||
|
To "inject" (access) the data defined on the `provide` tag, you can use the `inject()` method inside of `get_context_data()`.
|
||||||
|
|
||||||
|
For a component to be able to "inject" some data, the component (`{% component %}` tag) must be nested inside the `{% provide %}` tag.
|
||||||
|
|
||||||
|
In the example from previous section, we've defined two kwargs: `key="hi" another=123`. That means that if we now inject `"my_data"`, we get an object with 2 attributes - `key` and `another`.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class ChildComponent(component.Component):
|
||||||
|
def get_context_data(self):
|
||||||
|
my_data = self.inject("my_data")
|
||||||
|
print(my_data.key) # hi
|
||||||
|
print(my_data.another) # 123
|
||||||
|
return {}
|
||||||
|
```
|
||||||
|
|
||||||
|
First argument to `inject` is the _key_ of the provided data. This
|
||||||
|
must match the string that you used in the `provide` tag. If no provider
|
||||||
|
with given key is found, `inject` raises a `KeyError`.
|
||||||
|
|
||||||
|
To avoid the error, you can pass a second argument to `inject` to which will act as a default value, similar to `dict.get(key, default)`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class ChildComponent(component.Component):
|
||||||
|
def get_context_data(self):
|
||||||
|
my_data = self.inject("invalid_key", DEFAULT_DATA)
|
||||||
|
assert my_data == DEFAUKT_DATA
|
||||||
|
return {}
|
||||||
|
```
|
||||||
|
|
||||||
|
The instance returned from `inject()` is a subclass of `NamedTuple`, so the instance is immutable. This ensures that the data returned from `inject` will always
|
||||||
|
have all the keys that were passed to the `provide` tag.
|
||||||
|
|
||||||
|
> NOTE: `inject()` works strictly only in `get_context_data`. If you try to call it from elsewhere, it will raise an error.
|
||||||
|
|
||||||
|
|
||||||
|
### Full example
|
||||||
|
|
||||||
|
```py
|
||||||
|
@component.register("child")
|
||||||
|
class ChildComponent(component.Component):
|
||||||
|
template = """
|
||||||
|
<div> {{ my_data.key }} </div>
|
||||||
|
<div> {{ my_data.another }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
my_data = self.inject("my_data", "default")
|
||||||
|
return {"my_data": my_data}
|
||||||
|
|
||||||
|
template_str = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "my_data" key="hi" another=123 %}
|
||||||
|
{% component "child" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
renders:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div> hi </div>
|
||||||
|
<div> 123 </div>
|
||||||
|
```
|
||||||
|
|
||||||
## Component context and scope
|
## Component context and scope
|
||||||
|
|
||||||
By default, context variables are passed down the template as in regular Django - deeper scopes can access the variables from the outer scopes. So if you have several nested forloops, then inside the deep-most loop you can access variables defined by all previous loops.
|
By default, context variables are passed down the template as in regular Django - deeper scopes can access the variables from the outer scopes. So if you have several nested forloops, then inside the deep-most loop you can access variables defined by all previous loops.
|
||||||
|
|
|
@ -30,6 +30,7 @@ from django_components.context import (
|
||||||
_FILLED_SLOTS_CONTENT_CONTEXT_KEY,
|
_FILLED_SLOTS_CONTENT_CONTEXT_KEY,
|
||||||
_PARENT_COMP_CONTEXT_KEY,
|
_PARENT_COMP_CONTEXT_KEY,
|
||||||
_ROOT_CTX_CONTEXT_KEY,
|
_ROOT_CTX_CONTEXT_KEY,
|
||||||
|
get_injected_context_var,
|
||||||
make_isolated_context_copy,
|
make_isolated_context_copy,
|
||||||
prepare_context,
|
prepare_context,
|
||||||
)
|
)
|
||||||
|
@ -201,6 +202,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
self.outer_context: Context = outer_context or Context()
|
self.outer_context: Context = outer_context or Context()
|
||||||
self.fill_content = fill_content or {}
|
self.fill_content = fill_content or {}
|
||||||
self.component_id = component_id or gen_id()
|
self.component_id = component_id or gen_id()
|
||||||
|
self._context: Optional[Context] = None
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||||
cls.class_hash = hash(inspect.getfile(cls) + cls.__name__)
|
cls.class_hash = hash(inspect.getfile(cls) + cls.__name__)
|
||||||
|
@ -260,13 +262,71 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
f"Note: this attribute is not required if you are overriding the class's `get_template*()` methods."
|
f"Note: this attribute is not required if you are overriding the class's `get_template*()` methods."
|
||||||
)
|
)
|
||||||
|
|
||||||
def render_from_input(self, context: Context, args: Union[List, Tuple], kwargs: Dict[str, Any]) -> str:
|
def inject(self, key: str, default: Optional[Any] = None) -> Any:
|
||||||
component_context: dict = self.get_context_data(*args, **kwargs)
|
"""
|
||||||
|
Use this method to retrieve the data that was passed to a `{% provide %}` tag
|
||||||
|
with the corresponding key.
|
||||||
|
|
||||||
with context.update(component_context):
|
To retrieve the data, `inject()` must be called inside a component that's
|
||||||
|
inside the `{% provide %}` tag.
|
||||||
|
|
||||||
|
You may also pass a default that will be used if the `provide` tag with given
|
||||||
|
key was NOT found.
|
||||||
|
|
||||||
|
This method mut be used inside the `get_context_data()` method and raises
|
||||||
|
an error if called elsewhere.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
Given this template:
|
||||||
|
```django
|
||||||
|
{% provide "provider" hello="world" %}
|
||||||
|
{% component "my_comp" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
```
|
||||||
|
|
||||||
|
And given this definition of "my_comp" component:
|
||||||
|
```py
|
||||||
|
@component.register("my_comp")
|
||||||
|
class MyComp(component.Component):
|
||||||
|
template = "hi {{ data.hello }}!"
|
||||||
|
def get_context_data(self):
|
||||||
|
data = self.inject("provider")
|
||||||
|
return {"data": data}
|
||||||
|
```
|
||||||
|
|
||||||
|
This renders into:
|
||||||
|
```
|
||||||
|
hi world!
|
||||||
|
```
|
||||||
|
|
||||||
|
As the `{{ data.hello }}` is taken from the "provider".
|
||||||
|
"""
|
||||||
|
comp_name = self.registered_name or self.__class__.__name__
|
||||||
|
if self._context is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Method 'inject()' of component '{comp_name}' was called outside of 'get_context_data()'"
|
||||||
|
)
|
||||||
|
|
||||||
|
return get_injected_context_var(comp_name, self._context, key, default)
|
||||||
|
|
||||||
|
def render_from_input(
|
||||||
|
self,
|
||||||
|
context: Context,
|
||||||
|
args: Union[List, Tuple],
|
||||||
|
kwargs: Dict[str, Any],
|
||||||
|
) -> str:
|
||||||
|
# Temporarily populate _context so user can call `self.inject()` from
|
||||||
|
# within `get_context_data()`
|
||||||
|
self._context = context
|
||||||
|
context_data = self.get_context_data(*args, **kwargs)
|
||||||
|
self._context = None
|
||||||
|
|
||||||
|
with context.update(context_data):
|
||||||
rendered_component = self.render(
|
rendered_component = self.render(
|
||||||
context=context,
|
context=context,
|
||||||
context_data=component_context,
|
context_data=context_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_dependency_middleware_active():
|
if is_dependency_middleware_active():
|
||||||
|
|
|
@ -5,7 +5,10 @@ pass data across components, nodes, slots, and contexts.
|
||||||
You can think of the Context as our storage system.
|
You can think of the Context as our storage system.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.template import Context
|
from collections import namedtuple
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from django.template import Context, TemplateSyntaxError
|
||||||
|
|
||||||
from django_components.utils import find_last_index
|
from django_components.utils import find_last_index
|
||||||
|
|
||||||
|
@ -13,6 +16,7 @@ _FILLED_SLOTS_CONTENT_CONTEXT_KEY = "_DJANGO_COMPONENTS_FILLED_SLOTS"
|
||||||
_ROOT_CTX_CONTEXT_KEY = "_DJANGO_COMPONENTS_ROOT_CTX"
|
_ROOT_CTX_CONTEXT_KEY = "_DJANGO_COMPONENTS_ROOT_CTX"
|
||||||
_PARENT_COMP_CONTEXT_KEY = "_DJANGO_COMPONENTS_PARENT_COMP"
|
_PARENT_COMP_CONTEXT_KEY = "_DJANGO_COMPONENTS_PARENT_COMP"
|
||||||
_CURRENT_COMP_CONTEXT_KEY = "_DJANGO_COMPONENTS_CURRENT_COMP"
|
_CURRENT_COMP_CONTEXT_KEY = "_DJANGO_COMPONENTS_CURRENT_COMP"
|
||||||
|
_INJECT_CONTEXT_KEY_PREFIX = "_DJANGO_COMPONENTS_INJECT__"
|
||||||
|
|
||||||
|
|
||||||
def prepare_context(
|
def prepare_context(
|
||||||
|
@ -35,7 +39,13 @@ def make_isolated_context_copy(context: Context) -> Context:
|
||||||
# Pass through our internal keys
|
# Pass through our internal keys
|
||||||
context_copy[_FILLED_SLOTS_CONTENT_CONTEXT_KEY] = context.get(_FILLED_SLOTS_CONTENT_CONTEXT_KEY, {})
|
context_copy[_FILLED_SLOTS_CONTENT_CONTEXT_KEY] = context.get(_FILLED_SLOTS_CONTENT_CONTEXT_KEY, {})
|
||||||
if _ROOT_CTX_CONTEXT_KEY in context:
|
if _ROOT_CTX_CONTEXT_KEY in context:
|
||||||
context_copy[_ROOT_CTX_CONTEXT_KEY] = context.get(_ROOT_CTX_CONTEXT_KEY, {})
|
context_copy[_ROOT_CTX_CONTEXT_KEY] = context[_ROOT_CTX_CONTEXT_KEY]
|
||||||
|
|
||||||
|
# Make inject/provide to work in isolated mode
|
||||||
|
context_keys = context.flatten().keys()
|
||||||
|
for key in context_keys:
|
||||||
|
if key.startswith(_INJECT_CONTEXT_KEY_PREFIX):
|
||||||
|
context_copy[key] = context[key]
|
||||||
|
|
||||||
return context_copy
|
return context_copy
|
||||||
|
|
||||||
|
@ -62,3 +72,65 @@ def copy_forloop_context(from_context: Context, to_context: Context) -> None:
|
||||||
if "forloop" in from_context:
|
if "forloop" in from_context:
|
||||||
forloop_dict_index = find_last_index(from_context.dicts, lambda d: "forloop" in d)
|
forloop_dict_index = find_last_index(from_context.dicts, lambda d: "forloop" in d)
|
||||||
to_context.update(from_context.dicts[forloop_dict_index])
|
to_context.update(from_context.dicts[forloop_dict_index])
|
||||||
|
|
||||||
|
|
||||||
|
def get_injected_context_var(
|
||||||
|
component_name: str,
|
||||||
|
context: Context,
|
||||||
|
key: str,
|
||||||
|
default: Optional[Any] = None,
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Retrieve a 'provided' field. The field MUST have been previously 'provided'
|
||||||
|
by the component's ancestors using the `{% provide %}` template tag.
|
||||||
|
"""
|
||||||
|
# NOTE: For simplicity, we keep the provided values directly on the context.
|
||||||
|
# This plays nicely with Django's Context, which behaves like a stack, so "newer"
|
||||||
|
# values overshadow the "older" ones.
|
||||||
|
internal_key = _INJECT_CONTEXT_KEY_PREFIX + key
|
||||||
|
|
||||||
|
# Return provided value if found
|
||||||
|
if internal_key in context:
|
||||||
|
return context[internal_key]
|
||||||
|
|
||||||
|
# If a default was given, return that
|
||||||
|
if default is not None:
|
||||||
|
return default
|
||||||
|
|
||||||
|
# Otherwise raise error
|
||||||
|
raise KeyError(
|
||||||
|
f"Component '{component_name}' tried to inject a variable '{key}' before it was provided."
|
||||||
|
f"To fix this, make sure that at least one ancestor of component '{component_name}' has"
|
||||||
|
f" the variable '{key}' in their 'provide' attribute."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def set_provided_context_var(
|
||||||
|
context: Context,
|
||||||
|
key: str,
|
||||||
|
provided_kwargs: Dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
'Provide' given data under given key. In other words, this data can be retrieved
|
||||||
|
using `self.inject(key)` inside of `get_context_data()` method of components that
|
||||||
|
are nested inside the `{% provide %}` tag.
|
||||||
|
"""
|
||||||
|
# NOTE: We raise TemplateSyntaxError since this func should be called only from
|
||||||
|
# within template.
|
||||||
|
if not key:
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
"Provide tag received an empty string. Key must be non-empty and a valid identifier."
|
||||||
|
)
|
||||||
|
if not key.isidentifier():
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
"Provide tag received a non-identifier string. Key must be non-empty and a valid identifier."
|
||||||
|
)
|
||||||
|
|
||||||
|
# We turn the kwargs into a NamedTuple so that the object that's "provided"
|
||||||
|
# is immutable. This ensures that the data returned from `inject` will always
|
||||||
|
# have all the keys that were passed to the `provide` tag.
|
||||||
|
tpl_cls = namedtuple("DepInject", provided_kwargs.keys()) # type: ignore[misc]
|
||||||
|
payload = tpl_cls(**provided_kwargs)
|
||||||
|
|
||||||
|
internal_key = _INJECT_CONTEXT_KEY_PREFIX + key
|
||||||
|
context[internal_key] = payload
|
||||||
|
|
|
@ -63,7 +63,7 @@ def trace(logger: logging.Logger, message: str, *args: Any, **kwargs: Any) -> No
|
||||||
|
|
||||||
def trace_msg(
|
def trace_msg(
|
||||||
action: Literal["PARSE", "ASSOC", "RENDR", "GET", "SET"],
|
action: Literal["PARSE", "ASSOC", "RENDR", "GET", "SET"],
|
||||||
node_type: Literal["COMP", "FILL", "SLOT", "N/A"],
|
node_type: Literal["COMP", "FILL", "SLOT", "PROVIDE", "N/A"],
|
||||||
node_name: str,
|
node_name: str,
|
||||||
node_id: str,
|
node_id: str,
|
||||||
msg: str = "",
|
msg: str = "",
|
||||||
|
|
52
src/django_components/provide.py
Normal file
52
src/django_components/provide.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from django.template import Context
|
||||||
|
from django.template.base import FilterExpression, Node, NodeList
|
||||||
|
from django.utils.safestring import SafeString
|
||||||
|
|
||||||
|
from django_components.context import set_provided_context_var
|
||||||
|
from django_components.expression import safe_resolve_dict
|
||||||
|
from django_components.logger import trace_msg
|
||||||
|
from django_components.template_parser import process_aggregate_kwargs
|
||||||
|
from django_components.utils import gen_id
|
||||||
|
|
||||||
|
|
||||||
|
class ProvideNode(Node):
|
||||||
|
"""
|
||||||
|
Implementation of the `{% provide %}` tag.
|
||||||
|
For more info see `Component.inject`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
nodelist: NodeList,
|
||||||
|
node_id: Optional[str] = None,
|
||||||
|
provide_kwargs: Optional[Dict[str, FilterExpression]] = None,
|
||||||
|
):
|
||||||
|
self.name = name
|
||||||
|
self.nodelist = nodelist
|
||||||
|
self.node_id = node_id or gen_id()
|
||||||
|
self.provide_kwargs = provide_kwargs or {}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<Provide Node: {self.name}. Contents: {repr(self.nodelist)}. Data: {self.provide_kwargs}>"
|
||||||
|
|
||||||
|
def render(self, context: Context) -> SafeString:
|
||||||
|
trace_msg("RENDR", "PROVIDE", self.name, self.node_id)
|
||||||
|
|
||||||
|
data = safe_resolve_dict(self.provide_kwargs, context)
|
||||||
|
# Allow user to use the var:key=value syntax
|
||||||
|
data = process_aggregate_kwargs(data)
|
||||||
|
|
||||||
|
# NOTE: The "provided" kwargs are meant to be shared privately, meaning that components
|
||||||
|
# have to explicitly opt in by using the `Component.inject()` method. That's why we don't
|
||||||
|
# add the provided kwargs into the Context.
|
||||||
|
with context.update({}):
|
||||||
|
# "Provide" the data to child nodes
|
||||||
|
set_provided_context_var(context, self.name, data)
|
||||||
|
|
||||||
|
output = self.nodelist.render(context)
|
||||||
|
|
||||||
|
trace_msg("RENDR", "PROVIDE", self.name, self.node_id, msg="...Done!")
|
||||||
|
return output
|
|
@ -17,6 +17,7 @@ from django_components.middleware import (
|
||||||
JS_DEPENDENCY_PLACEHOLDER,
|
JS_DEPENDENCY_PLACEHOLDER,
|
||||||
is_dependency_middleware_active,
|
is_dependency_middleware_active,
|
||||||
)
|
)
|
||||||
|
from django_components.provide import ProvideNode
|
||||||
from django_components.slots import FillNode, SlotNode, parse_slot_fill_nodes_from_component_nodelist
|
from django_components.slots import FillNode, SlotNode, parse_slot_fill_nodes_from_component_nodelist
|
||||||
from django_components.template_parser import parse_bits
|
from django_components.template_parser import parse_bits
|
||||||
from django_components.utils import gen_id
|
from django_components.utils import gen_id
|
||||||
|
@ -216,6 +217,30 @@ def do_component(parser: Parser, token: Token) -> ComponentNode:
|
||||||
return component_node
|
return component_node
|
||||||
|
|
||||||
|
|
||||||
|
@register.tag("provide")
|
||||||
|
def do_provide(parser: Parser, token: Token) -> SlotNode:
|
||||||
|
# e.g. {% provide <name> key=val key2=val2 %}
|
||||||
|
tag_name, *args = token.split_contents()
|
||||||
|
provide_key, kwargs = _parse_provide_args(parser, args, tag_name)
|
||||||
|
|
||||||
|
# Use a unique ID to be able to tie the fill nodes with components and slots
|
||||||
|
# NOTE: MUST be called BEFORE `parser.parse()` to ensure predictable numbering
|
||||||
|
slot_id = gen_id()
|
||||||
|
trace_msg("PARSE", "PROVIDE", provide_key, slot_id)
|
||||||
|
|
||||||
|
nodelist = parser.parse(parse_until=["endprovide"])
|
||||||
|
parser.delete_first_token()
|
||||||
|
slot_node = ProvideNode(
|
||||||
|
provide_key,
|
||||||
|
nodelist,
|
||||||
|
node_id=slot_id,
|
||||||
|
provide_kwargs=kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
trace_msg("PARSE", "PROVIDE", provide_key, slot_id, "...Done!")
|
||||||
|
return slot_node
|
||||||
|
|
||||||
|
|
||||||
@register.tag("html_attrs")
|
@register.tag("html_attrs")
|
||||||
def do_html_attrs(parser: Parser, token: Token) -> HtmlAttrsNode:
|
def do_html_attrs(parser: Parser, token: Token) -> HtmlAttrsNode:
|
||||||
"""
|
"""
|
||||||
|
@ -438,6 +463,32 @@ def _parse_fill_args(
|
||||||
return slot_name_fexp, slot_default_var_fexp, slot_data_var_fexp
|
return slot_name_fexp, slot_default_var_fexp, slot_data_var_fexp
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_provide_args(
|
||||||
|
parser: Parser,
|
||||||
|
bits: List[str],
|
||||||
|
tag_name: str,
|
||||||
|
) -> Tuple[str, Dict[str, FilterExpression]]:
|
||||||
|
if not len(bits):
|
||||||
|
raise TemplateSyntaxError("'provide' tag does not match pattern {% provide <key> [key=val, ...] %}. ")
|
||||||
|
|
||||||
|
provide_key, *options = bits
|
||||||
|
if not is_wrapped_in_quotes(provide_key):
|
||||||
|
raise TemplateSyntaxError(f"'{tag_name}' key must be a string 'literal'.")
|
||||||
|
|
||||||
|
provide_key = resolve_string(provide_key, parser)
|
||||||
|
|
||||||
|
# Parse kwargs that will be 'provided' under the given key
|
||||||
|
_, tag_kwarg_pairs = parse_bits(parser=parser, bits=options, params=[], name=tag_name)
|
||||||
|
tag_kwargs: Dict[str, FilterExpression] = {}
|
||||||
|
for key, val in tag_kwarg_pairs:
|
||||||
|
if key in tag_kwargs:
|
||||||
|
# The keyword argument has already been supplied once
|
||||||
|
raise TemplateSyntaxError(f"'{tag_name}' received multiple values for keyword argument '{key}'")
|
||||||
|
tag_kwargs[key] = val
|
||||||
|
|
||||||
|
return provide_key, tag_kwargs
|
||||||
|
|
||||||
|
|
||||||
def _get_positional_param(
|
def _get_positional_param(
|
||||||
tag_name: str,
|
tag_name: str,
|
||||||
param_name: str,
|
param_name: str,
|
||||||
|
|
15
tests/templates/block_in_component_provide.html
Normal file
15
tests/templates/block_in_component_provide.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% load component_tags %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<body>
|
||||||
|
{% provide "block_provide" hello="from_block" %}
|
||||||
|
{% component "slotted_component" %}
|
||||||
|
{% fill "header" %}{% endfill %}
|
||||||
|
{% fill "main" %}
|
||||||
|
{% block body %}
|
||||||
|
{% endblock %}
|
||||||
|
{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
</body>
|
||||||
|
</html>
|
5
tests/templates/inject.html
Normal file
5
tests/templates/inject.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{% load component_tags %}
|
||||||
|
<div>
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
</div>
|
|
@ -453,3 +453,42 @@ class BlockCompatTests(BaseTestCase):
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
self.assertHTMLEqual(rendered, expected)
|
self.assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_inject_inside_block(self):
|
||||||
|
component.registry.register("slotted_component", SlottedComponent)
|
||||||
|
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
var = self.inject("block_provide")
|
||||||
|
return {"var": var}
|
||||||
|
|
||||||
|
template: types.django_html = """
|
||||||
|
{% extends "block_in_component_provide.html" %}
|
||||||
|
{% load component_tags %}
|
||||||
|
{% block body %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endblock %}
|
||||||
|
"""
|
||||||
|
rendered = Template(template).render(Context())
|
||||||
|
expected = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<body>
|
||||||
|
<custom-template>
|
||||||
|
<header></header>
|
||||||
|
<main>
|
||||||
|
<div> injected: DepInject(hello='from_block') </div>
|
||||||
|
</main>
|
||||||
|
<footer>Default footer</footer>
|
||||||
|
</custom-template>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
self.assertHTMLEqual(rendered, expected)
|
||||||
|
|
601
tests/test_templatetags_provide.py
Normal file
601
tests/test_templatetags_provide.py
Normal file
|
@ -0,0 +1,601 @@
|
||||||
|
from django.template import Context, Template, TemplateSyntaxError
|
||||||
|
|
||||||
|
# isort: off
|
||||||
|
from .django_test_setup import * # NOQA
|
||||||
|
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||||
|
|
||||||
|
# isort: on
|
||||||
|
|
||||||
|
from django_components import component, types
|
||||||
|
|
||||||
|
|
||||||
|
class ProvideTemplateTagTest(BaseTestCase):
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_provide_basic(self):
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
var = self.inject("my_provide", "default")
|
||||||
|
return {"var": var}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "my_provide" key="hi" another=123 %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<div> injected: DepInject(key='hi', another=123) </div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_provide_access_keys_in_python(self):
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> key: {{ key }} </div>
|
||||||
|
<div> another: {{ another }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
my_provide = self.inject("my_provide")
|
||||||
|
return {
|
||||||
|
"key": my_provide.key,
|
||||||
|
"another": my_provide.another,
|
||||||
|
}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "my_provide" key="hi" another=123 %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<div> key: hi </div>
|
||||||
|
<div> another: 123 </div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_provide_access_keys_in_django(self):
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> key: {{ my_provide.key }} </div>
|
||||||
|
<div> another: {{ my_provide.another }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
my_provide = self.inject("my_provide")
|
||||||
|
return {
|
||||||
|
"my_provide": my_provide,
|
||||||
|
}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "my_provide" key="hi" another=123 %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<div> key: hi </div>
|
||||||
|
<div> another: 123 </div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_provide_does_not_leak(self):
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
var = self.inject("my_provide", "default")
|
||||||
|
return {"var": var}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "my_provide" key="hi" another=123 %}
|
||||||
|
{% endprovide %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<div> injected: default </div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_provide_empty(self):
|
||||||
|
"""Check provide tag with no kwargs"""
|
||||||
|
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
var = self.inject("my_provide", "default")
|
||||||
|
return {"var": var}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "my_provide" %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<div> injected: DepInject() </div>
|
||||||
|
<div> injected: default </div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_provide_no_inject(self):
|
||||||
|
"""Check that nothing breaks if we do NOT inject even if some data is provided"""
|
||||||
|
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div></div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "my_provide" key="hi" another=123 %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_provide_key_single_quotes(self):
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
var = self.inject("my_provide", "default")
|
||||||
|
return {"var": var}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide 'my_provide' key="hi" another=123 %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<div> injected: DepInject(key='hi', another=123) </div>
|
||||||
|
<div> injected: default </div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_provide_no_key_raises(self):
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
var = self.inject("my_provide", "default")
|
||||||
|
return {"var": var}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide key="hi" another=123 %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
with self.assertRaises(TemplateSyntaxError):
|
||||||
|
Template(template_str)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_provide_key_must_be_string_literal(self):
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
var = self.inject("my_provide", "default")
|
||||||
|
return {"var": var}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide my_var key="hi" another=123 %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
with self.assertRaises(TemplateSyntaxError):
|
||||||
|
Template(template_str)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_provide_key_must_be_identifier(self):
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
var = self.inject("my_provide", "default")
|
||||||
|
return {"var": var}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "%heya%" key="hi" another=123 %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
with self.assertRaises(TemplateSyntaxError):
|
||||||
|
template.render(Context({}))
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_provide_aggregate_dics(self):
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
var = self.inject("my_provide", "default")
|
||||||
|
return {"var": var}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "my_provide" var1:key="hi" var1:another=123 var2:x="y" %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<div> injected: DepInject(var1={'key': 'hi', 'another': 123}, var2={'x': 'y'}) </div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_provide_does_not_expose_kwargs_to_context(self):
|
||||||
|
"""Check that `provide` tag doesn't assign the keys to the context like `with` tag does"""
|
||||||
|
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
var = self.inject("my_provide", "default")
|
||||||
|
return {"var": var}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
var_out: {{ var }}
|
||||||
|
key_out: {{ key }}
|
||||||
|
{% provide "my_provide" key="hi" another=123 %}
|
||||||
|
var_in: {{ var }}
|
||||||
|
key_in: {{ key }}
|
||||||
|
{% endprovide %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({"var": "123"}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
var_out: 123
|
||||||
|
key_out:
|
||||||
|
var_in: 123
|
||||||
|
key_in:
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_provide_nested_in_provide_same_key(self):
|
||||||
|
"""Check that inner `provide` with same key overshadows outer `provide`"""
|
||||||
|
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
var = self.inject("my_provide", "default")
|
||||||
|
return {"var": var}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "my_provide" key="hi" another=123 lost=0 %}
|
||||||
|
{% provide "my_provide" key="hi1" another=1231 new=3 %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<div> injected: DepInject(key='hi1', another=1231, new=3) </div>
|
||||||
|
<div> injected: DepInject(key='hi', another=123, lost=0) </div>
|
||||||
|
<div> injected: default </div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_provide_nested_in_provide_different_key(self):
|
||||||
|
"""Check that `provide` tag with different keys don't affect each other"""
|
||||||
|
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> first_provide: {{ first_provide|safe }} </div>
|
||||||
|
<div> second_provide: {{ second_provide|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
first_provide = self.inject("first_provide", "default")
|
||||||
|
second_provide = self.inject("second_provide", "default")
|
||||||
|
return {
|
||||||
|
"first_provide": first_provide,
|
||||||
|
"second_provide": second_provide,
|
||||||
|
}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "first_provide" key="hi" another=123 lost=0 %}
|
||||||
|
{% provide "second_provide" key="hi1" another=1231 new=3 %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
{% endprovide %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<div> first_provide: DepInject(key='hi', another=123, lost=0) </div>
|
||||||
|
<div> second_provide: DepInject(key='hi1', another=1231, new=3) </div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_provide_in_include(self):
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
var = self.inject("my_provide", "default")
|
||||||
|
return {"var": var}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "my_provide" key="hi" another=123 %}
|
||||||
|
{% include "inject.html" %}
|
||||||
|
{% endprovide %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<div>
|
||||||
|
<div> injected: DepInject(key='hi', another=123) </div>
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InjectTest(BaseTestCase):
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_inject_basic(self):
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
var = self.inject("my_provide")
|
||||||
|
return {"var": var}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "my_provide" key="hi" another=123 %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<div> injected: DepInject(key='hi', another=123) </div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_inject_missing_key_raises_without_default(self):
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
var = self.inject("abc")
|
||||||
|
return {"var": var}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
with self.assertRaises(KeyError):
|
||||||
|
template.render(Context({}))
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_inject_missing_key_ok_with_default(self):
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
var = self.inject("abc", "default")
|
||||||
|
return {"var": var}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<div> injected: default </div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_inject_empty_string(self):
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
var = self.inject("")
|
||||||
|
return {"var": var}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "my_provide" key="hi" another=123 %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endprovide %}
|
||||||
|
{% component "injectee" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
with self.assertRaises(KeyError):
|
||||||
|
template.render(Context({}))
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_inject_raises_on_called_outside_get_context_data(self):
|
||||||
|
@component.register("injectee")
|
||||||
|
class InjectComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
var = self.inject("abc", "default")
|
||||||
|
return {"var": var}
|
||||||
|
|
||||||
|
comp = InjectComponent("")
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
comp.inject("abc", "def")
|
Loading…
Add table
Add a link
Reference in a new issue