mirror of
https://github.com/django-components/django-components.git
synced 2025-07-07 17:34:59 +00:00
feat: add BaseNode.contents (#1177)
This commit is contained in:
parent
661413d4a9
commit
5f4fbe76e5
6 changed files with 184 additions and 10 deletions
30
CHANGELOG.md
30
CHANGELOG.md
|
@ -406,6 +406,36 @@
|
|||
# /components/ext/view/components/c1ab2c3?foo=bar#baz
|
||||
```
|
||||
|
||||
- The `BaseNode` class has a new `contents` attribute, which contains the raw contents (string) of the tag body.
|
||||
|
||||
This is relevant when you define custom template tags with `@template_tag` decorator or `BaseNode` class.
|
||||
|
||||
When you define a custom template tag like so:
|
||||
|
||||
```py
|
||||
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:
|
||||
|
||||
```django
|
||||
{% mytag name="John" %}
|
||||
Hello, world!
|
||||
{% endmytag %}
|
||||
```
|
||||
|
||||
Then, the `contents` attribute of the `BaseNode` instance will contain the string `"Hello, world!"`.
|
||||
|
||||
#### 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)).
|
||||
|
|
|
@ -52,7 +52,7 @@ This will allow you to use the tag in your templates like this:
|
|||
|
||||
### Parameters
|
||||
|
||||
The `@template_tag` decorator accepts the following parameters:
|
||||
The [`@template_tag`](../../../reference/api#django_components.template_tag) decorator accepts the following parameters:
|
||||
|
||||
- `library`: The Django template library to register the tag with
|
||||
- `tag`: The name of the template tag (e.g. `"mytag"` for `{% mytag %}`)
|
||||
|
@ -61,7 +61,8 @@ The `@template_tag` decorator accepts the following parameters:
|
|||
|
||||
### Function signature
|
||||
|
||||
The function decorated with `@template_tag` must accept at least two arguments:
|
||||
The function decorated with [`@template_tag`](../../../reference/api#django_components.template_tag)
|
||||
must accept at least two arguments:
|
||||
|
||||
1. `node`: The node instance (we'll explain this in detail in the next section)
|
||||
2. `context`: The Django template context
|
||||
|
@ -150,15 +151,16 @@ GreetNode.register(library)
|
|||
|
||||
### Node properties
|
||||
|
||||
When using `BaseNode`, you have access to several useful properties:
|
||||
When using [`BaseNode`](../../../reference/api#django_components.BaseNode), you have access to several useful properties:
|
||||
|
||||
- `node_id`: A unique identifier for this node instance
|
||||
- `flags`: Dictionary of flag values (e.g. `{"required": True}`)
|
||||
- `params`: List of raw parameters passed to the tag
|
||||
- `nodelist`: The template nodes between the start and end tags
|
||||
- `contents`: The raw contents between the start and end tags
|
||||
- `active_flags`: List of flags that are currently set to True
|
||||
|
||||
This is what the `node` parameter in the `@template_tag` decorator gives you access to - it's the instance of the node class that was automatically created for your template tag.
|
||||
This is what the `node` parameter in the [`@template_tag`](../../../reference/api#django_components.template_tag) decorator gives you access to - it's the instance of the node class that was automatically created for your template tag.
|
||||
|
||||
### Rendering content between tags
|
||||
|
||||
|
|
|
@ -2888,8 +2888,9 @@ class ComponentNode(BaseNode):
|
|||
flags: Optional[Dict[str, bool]] = None,
|
||||
nodelist: Optional[NodeList] = None,
|
||||
node_id: Optional[str] = None,
|
||||
contents: Optional[str] = None,
|
||||
) -> None:
|
||||
super().__init__(params=params, flags=flags, nodelist=nodelist, node_id=node_id)
|
||||
super().__init__(params=params, flags=flags, nodelist=nodelist, node_id=node_id, contents=contents)
|
||||
|
||||
self.name = name
|
||||
self.registry = registry
|
||||
|
|
|
@ -313,11 +313,13 @@ class BaseNode(Node, metaclass=NodeMeta):
|
|||
flags: Optional[Dict[str, bool]] = None,
|
||||
nodelist: Optional[NodeList] = None,
|
||||
node_id: Optional[str] = None,
|
||||
contents: Optional[str] = None,
|
||||
):
|
||||
self.params = params
|
||||
self.flags = flags or {flag: False for flag in self.allowed_flags or []}
|
||||
self.nodelist = nodelist or NodeList()
|
||||
self.node_id = node_id or gen_id()
|
||||
self.contents = contents
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
|
@ -350,12 +352,13 @@ class BaseNode(Node, metaclass=NodeMeta):
|
|||
|
||||
trace_node_msg("PARSE", cls.tag, tag_id)
|
||||
|
||||
body = tag.parse_body()
|
||||
body, contents = tag.parse_body()
|
||||
node = cls(
|
||||
nodelist=body,
|
||||
node_id=tag_id,
|
||||
params=tag.params,
|
||||
flags=tag.flags,
|
||||
contents=contents,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
|
|
@ -104,7 +104,7 @@ def resolve_params(
|
|||
class ParsedTag(NamedTuple):
|
||||
flags: Dict[str, bool]
|
||||
params: List[TagAttr]
|
||||
parse_body: Callable[[], NodeList]
|
||||
parse_body: Callable[[], Tuple[NodeList, Optional[str]]]
|
||||
|
||||
|
||||
def parse_template_tag(
|
||||
|
@ -140,13 +140,15 @@ def parse_template_tag(
|
|||
|
||||
raw_params, flags = _extract_flags(tag_name, attrs, allowed_flags or [])
|
||||
|
||||
def _parse_tag_body(parser: Parser, end_tag: str, inline: bool) -> NodeList:
|
||||
def _parse_tag_body(parser: Parser, end_tag: str, inline: bool) -> Tuple[NodeList, Optional[str]]:
|
||||
if inline:
|
||||
body = NodeList()
|
||||
contents: Optional[str] = None
|
||||
else:
|
||||
contents = _extract_contents_until(parser, [end_tag])
|
||||
body = parser.parse(parse_until=[end_tag])
|
||||
parser.delete_first_token()
|
||||
return body
|
||||
return body, contents
|
||||
|
||||
return ParsedTag(
|
||||
params=raw_params,
|
||||
|
@ -155,10 +157,53 @@ def parse_template_tag(
|
|||
# loggers before the parsing. This is because, if the body contains any other
|
||||
# tags, it will trigger their tag handlers. So the code called AFTER
|
||||
# `parse_body()` is already after all the nested tags were processed.
|
||||
parse_body=lambda: _parse_tag_body(parser, end_tag, is_inline) if end_tag else NodeList(),
|
||||
parse_body=lambda: _parse_tag_body(parser, end_tag, is_inline) if end_tag else (NodeList(), None),
|
||||
)
|
||||
|
||||
|
||||
# Similar to `parser.parse(parse_until=[end_tag])`, except:
|
||||
# 1. Does not remove the token it goes over (unlike `parser.parse()`, which mutates the parser state)
|
||||
# 2. Returns a string, instead of a NodeList
|
||||
#
|
||||
# This is used so we can access the contents of the tag body as strings, for example
|
||||
# to be used for caching slots.
|
||||
#
|
||||
# See https://github.com/django/django/blob/1fb3f57e81239a75eb8f873b392e11534c041fdc/django/template/base.py#L471
|
||||
def _extract_contents_until(parser: Parser, until_blocks: List[str]) -> str:
|
||||
contents: List[str] = []
|
||||
for token in reversed(parser.tokens):
|
||||
# Use the raw values here for TokenType.* for a tiny performance boost.
|
||||
token_type = token.token_type.value
|
||||
if token_type == 0: # TokenType.TEXT
|
||||
contents.append(token.contents)
|
||||
elif token_type == 1: # TokenType.VAR
|
||||
contents.append("{{ " + token.contents + " }}")
|
||||
elif token_type == 2: # TokenType.BLOCK
|
||||
try:
|
||||
command = token.contents.split()[0]
|
||||
except IndexError:
|
||||
# NOTE: Django's `Parser.parse()` raises a `TemplateSyntaxError` when there
|
||||
# was a an empty block tag, e.g. `{% %}`.
|
||||
# We skip raising an error here and let `Parser.parse()` raise it.
|
||||
contents.append("{% " + token.contents + " %}")
|
||||
if command in until_blocks:
|
||||
return "".join(contents)
|
||||
else:
|
||||
contents.append("{% " + token.contents + " %}")
|
||||
elif token_type == 3: # TokenType.COMMENT
|
||||
contents.append("{# " + token.contents + " #}")
|
||||
else:
|
||||
raise ValueError(f"Unknown token type {token_type}")
|
||||
|
||||
# NOTE: If we got here, then we've reached the end of the tag body without
|
||||
# encountering any of the `until_blocks`.
|
||||
# Django's `Parser.parse()` raises a `TemplateSyntaxError` in such case.
|
||||
#
|
||||
# Currently `_extract_contents_until()` runs right before `parser.parse()`,
|
||||
# so we skip raising an error here.
|
||||
return "".join(contents)
|
||||
|
||||
|
||||
def _extract_flags(
|
||||
tag_name: str, attrs: List[TagAttr], allowed_flags: List[str]
|
||||
) -> Tuple[List[TagAttr], Dict[str, bool]]:
|
||||
|
|
|
@ -2,11 +2,14 @@ import inspect
|
|||
import re
|
||||
import pytest
|
||||
from django.template import Context, Template
|
||||
from django.template.base import TextNode, VariableNode
|
||||
from django.template.defaulttags import IfNode, LoremNode
|
||||
from django.template.exceptions import TemplateSyntaxError
|
||||
|
||||
from django_components import types
|
||||
from django_components.node import BaseNode, template_tag
|
||||
from django_components.templatetags import component_tags
|
||||
from django_components.util.tag_parser import TagAttr
|
||||
|
||||
from django_components.testing import djc_test
|
||||
from .testutils import setup_test_config
|
||||
|
@ -836,6 +839,96 @@ class TestSignatureBasedValidation:
|
|||
|
||||
TestNode.unregister(component_tags.register)
|
||||
|
||||
def test_node_class_attributes(self):
|
||||
captured = None
|
||||
|
||||
class TestNodeWithEndTag(BaseNode):
|
||||
tag = "mytag"
|
||||
end_tag = "endmytag"
|
||||
|
||||
@force_signature_validation
|
||||
def render(self, context: Context, name: str, **kwargs) -> str:
|
||||
nonlocal captured
|
||||
captured = self.params, self.nodelist, self.node_id, self.contents
|
||||
return f"Hello, {name}!"
|
||||
|
||||
# Case 1 - Node with end tag and NOT self-closing
|
||||
TestNodeWithEndTag.register(component_tags.register)
|
||||
|
||||
template_str1 = """
|
||||
{% load component_tags %}
|
||||
{% mytag 'John' %}
|
||||
INSIDE TAG {{ my_var }} {# comment #} {% lorem 1 w %} {% if True %} henlo {% endif %}
|
||||
{% endmytag %}
|
||||
"""
|
||||
template1 = Template(template_str1)
|
||||
template1.render(Context({}))
|
||||
|
||||
params1, nodelist1, node_id1, contents1 = captured # type: ignore
|
||||
assert len(params1) == 1
|
||||
assert isinstance(params1[0], TagAttr)
|
||||
# NOTE: The comment node is not included in the nodelist
|
||||
assert len(nodelist1) == 8
|
||||
assert isinstance(nodelist1[0], TextNode)
|
||||
assert isinstance(nodelist1[1], VariableNode)
|
||||
assert isinstance(nodelist1[2], TextNode)
|
||||
assert isinstance(nodelist1[3], TextNode)
|
||||
assert isinstance(nodelist1[4], LoremNode)
|
||||
assert isinstance(nodelist1[5], TextNode)
|
||||
assert isinstance(nodelist1[6], IfNode)
|
||||
assert isinstance(nodelist1[7], TextNode)
|
||||
assert contents1 == "\n INSIDE TAG {{ my_var }} {# comment #} {% lorem 1 w %} {% if True %} henlo {% endif %}\n " # noqa: E501
|
||||
assert node_id1 == "a1bc3e"
|
||||
|
||||
captured = None # Reset captured
|
||||
|
||||
# Case 2 - Node with end tag and NOT self-closing
|
||||
template_str2 = """
|
||||
{% load component_tags %}
|
||||
{% mytag 'John' / %}
|
||||
"""
|
||||
template2 = Template(template_str2)
|
||||
template2.render(Context({}))
|
||||
|
||||
params2, nodelist2, node_id2, contents2 = captured # type: ignore
|
||||
assert len(params2) == 1 # type: ignore
|
||||
assert isinstance(params2[0], TagAttr) # type: ignore
|
||||
assert len(nodelist2) == 0 # type: ignore
|
||||
assert contents2 is None # type: ignore
|
||||
assert node_id2 == "a1bc3f" # type: ignore
|
||||
|
||||
captured = None # Reset captured
|
||||
|
||||
# Case 3 - Node without end tag
|
||||
class TestNodeWithoutEndTag(BaseNode):
|
||||
tag = "mytag2"
|
||||
|
||||
@force_signature_validation
|
||||
def render(self, context: Context, name: str, **kwargs) -> str:
|
||||
nonlocal captured
|
||||
captured = self.params, self.nodelist, self.node_id, self.contents
|
||||
return f"Hello, {name}!"
|
||||
|
||||
TestNodeWithoutEndTag.register(component_tags.register)
|
||||
|
||||
template_str3 = """
|
||||
{% load component_tags %}
|
||||
{% mytag2 'John' %}
|
||||
"""
|
||||
template3 = Template(template_str3)
|
||||
template3.render(Context({}))
|
||||
|
||||
params3, nodelist3, node_id3, contents3 = captured # type: ignore
|
||||
assert len(params3) == 1
|
||||
assert isinstance(params3[0], TagAttr)
|
||||
assert len(nodelist3) == 0
|
||||
assert contents3 is None
|
||||
assert node_id3 == "a1bc40"
|
||||
|
||||
# Cleanup
|
||||
TestNodeWithEndTag.unregister(component_tags.register)
|
||||
TestNodeWithoutEndTag.unregister(component_tags.register)
|
||||
|
||||
def test_node_render(self):
|
||||
# Check that the render function is called with the context
|
||||
captured = None
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue