refactor: Backbone for passing JS and CSS variables (#861)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Juro Oravec 2024-12-28 19:27:19 +01:00 committed by GitHub
parent 296da4a1e8
commit fe67d90547
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1181 additions and 594 deletions

View file

@ -1,5 +1,15 @@
# Release notes
## v0.124
#### Refactor
- The undocumented `Component.component_id` was removed. Instead, use `Component.id`. Changes:
- While `component_id` was unique every time you instantiated `Component`, The new `id` is unique
every time you render the component (e.g. with `Component.render()`)
- The new `id` is available only during render, so e.g. from within `get_context_data()`
## v0.123
#### Fix

View file

@ -45,7 +45,14 @@ from django_components.context import (
get_injected_context_var,
make_isolated_context_copy,
)
from django_components.dependencies import RenderType, cache_inlined_css, cache_inlined_js, postprocess_component_html
from django_components.dependencies import (
RenderType,
cache_component_css,
cache_component_css_vars,
cache_component_js,
cache_component_js_vars,
postprocess_component_html,
)
from django_components.expression import Expression, RuntimeKwargs, safe_resolve_list
from django_components.node import BaseNode
from django_components.slots import (
@ -92,6 +99,7 @@ _type = type
@dataclass(frozen=True)
class RenderInput(Generic[ArgsType, KwargsType, SlotsType]):
id: str
context: Context
args: ArgsType
kwargs: KwargsType
@ -297,7 +305,6 @@ class Component(
def __init__(
self,
registered_name: Optional[str] = None,
component_id: Optional[str] = None,
outer_context: Optional[Context] = None,
registry: Optional[ComponentRegistry] = None, # noqa F811
):
@ -318,7 +325,6 @@ class Component(
self.registered_name: Optional[str] = registered_name
self.outer_context: Context = outer_context or Context()
self.component_id = component_id or gen_id()
self.registry = registry or registry_
self._render_stack: Deque[RenderStackItem[ArgsType, KwargsType, SlotsType]] = deque()
# None == uninitialized, False == No types, Tuple == types
@ -331,6 +337,19 @@ class Component(
def name(self) -> str:
return self.registered_name or self.__class__.__name__
@property
def id(self) -> str:
"""
Render ID - This ID is unique for every time a `Component.render()` (or equivalent) is called.
This is useful for logging or debugging.
Render IDs have the chance of collision 1 in 3.3M.
Raises RuntimeError if called outside of rendering execution.
"""
return self.input.id
@property
def input(self) -> RenderInput[ArgsType, KwargsType, SlotsType]:
"""
@ -702,6 +721,7 @@ class Component(
self._render_stack.append(
RenderStackItem(
input=RenderInput(
id=gen_id(),
context=context,
slots=slots,
args=args,
@ -716,9 +736,14 @@ class Component(
context_data = self.get_context_data(*args, **kwargs)
self._validate_outputs(data=context_data)
# Process JS and CSS files
cache_inlined_js(self.__class__, self.js or "")
cache_inlined_css(self.__class__, self.css or "")
# Process Component's JS and CSS
js_data: Dict = {} # TODO
cache_component_js(self.__class__)
js_input_hash = cache_component_js_vars(self.__class__, js_data) if js_data else None
css_data: Dict = {} # TODO
cache_component_css(self.__class__)
css_input_hash = cache_component_css_vars(self.__class__, css_data) if css_data else None
with _prepare_template(self, context, context_data) as template:
# For users, we expose boolean variables that they may check
@ -763,8 +788,10 @@ class Component(
output = postprocess_component_html(
component_cls=self.__class__,
component_id=self.component_id,
component_id=self.id,
html_content=html_content,
css_input_hash=css_input_hash,
js_input_hash=js_input_hash,
type=type,
render_dependencies=render_dependencies,
)
@ -951,7 +978,6 @@ class ComponentNode(BaseNode):
component: Component = component_cls(
registered_name=self.name,
outer_context=context,
component_id=self.node_id,
registry=self.registry,
)

View file

@ -113,7 +113,6 @@ class DynamicComponent(Component):
# NOTE: Slots are passed at component instantiation
comp = comp_class(
registered_name=self.registered_name,
component_id=self.component_id,
outer_context=self.outer_context,
registry=self.registry,
)

View file

@ -11,11 +11,10 @@ from typing import (
TYPE_CHECKING,
Callable,
Dict,
Iterable,
List,
Literal,
NamedTuple,
Optional,
Sequence,
Set,
Tuple,
Type,
@ -35,7 +34,7 @@ from django.utils.decorators import sync_and_async_middleware
from django.utils.safestring import SafeString, mark_safe
from django_components.util.html import SoupNode
from django_components.util.misc import get_import_path
from django_components.util.misc import get_import_path, is_nonempty_str
if TYPE_CHECKING:
from django_components.component import Component
@ -112,16 +111,21 @@ def _hash_comp_cls(comp_cls: Type["Component"]) -> str:
def _gen_cache_key(
comp_cls_hash: str,
script_type: ScriptType,
input_hash: Optional[str],
) -> str:
return f"__components:{comp_cls_hash}:{script_type}"
if input_hash:
return f"__components:{comp_cls_hash}:{script_type}:{input_hash}"
else:
return f"__components:{comp_cls_hash}:{script_type}"
def _is_script_in_cache(
comp_cls: Type["Component"],
script_type: ScriptType,
input_hash: Optional[str],
) -> bool:
comp_cls_hash = _hash_comp_cls(comp_cls)
cache_key = _gen_cache_key(comp_cls_hash, script_type)
cache_key = _gen_cache_key(comp_cls_hash, script_type, input_hash)
return comp_media_cache.has(cache_key)
@ -129,6 +133,7 @@ def _cache_script(
comp_cls: Type["Component"],
script: str,
script_type: ScriptType,
input_hash: Optional[str],
) -> None:
"""
Given a component and it's inlined JS or CSS, store the JS/CSS in a cache,
@ -138,7 +143,7 @@ def _cache_script(
# E.g. `__components:MyButton:js:df7c6d10`
if script_type in ("js", "css"):
cache_key = _gen_cache_key(comp_cls_hash, script_type)
cache_key = _gen_cache_key(comp_cls_hash, script_type, input_hash)
else:
raise ValueError(f"Unexpected script_type '{script_type}'")
@ -147,33 +152,115 @@ def _cache_script(
comp_media_cache.set(cache_key, script.strip())
def cache_inlined_js(comp_cls: Type["Component"], content: str) -> None:
if not _is_nonempty_str(comp_cls.js):
return
def cache_component_js(comp_cls: Type["Component"]) -> None:
"""
Cache the content from `Component.js`. This is the common JS that's shared
among all instances of the same component. So even if the component is rendered multiple
times, this JS is loaded only once.
"""
if not comp_cls.js or not is_nonempty_str(comp_cls.js) or _is_script_in_cache(comp_cls, "js", None):
return None
# Prepare the script that's common to all instances of the same component
# E.g. `my_table.js`
if not _is_script_in_cache(comp_cls, "js"):
_cache_script(
comp_cls=comp_cls,
script=comp_cls.js,
script_type="js",
input_hash=None,
)
# NOTE: In CSS, we link the CSS vars to the component via a stylesheet that defines
# the CSS vars under `[data-djc-css-a1b2c3]`. Because of this we define the variables
# separately from the rest of the CSS definition.
#
# We use conceptually similar approach for JS, except in JS we have to manually associate
# the JS variables ("stylesheet") with the target HTML element ("component").
#
# It involves 3 steps:
# 1. Register the common logic (equivalent to registering common CSS).
# with `Components.manager.registerComponent`.
# 2. Register the unique set of JS variables (equivalent to defining CSS vars)
# with `Components.manager.registerComponentData`.
# 3. Actually run a component's JS instance with `Components.manager.callComponent`,
# specifying the components HTML elements with `component_id`, and JS vars with `input_hash`.
def cache_component_js_vars(comp_cls: Type["Component"], js_vars: Dict) -> Optional[str]:
if not is_nonempty_str(comp_cls.js):
return None
# The hash for the file that holds the JS variables is derived from the variables themselves.
json_data = json.dumps(js_vars)
input_hash = md5(json_data.encode()).hexdigest()[0:6]
# Generate and cache a JS script that contains the JS variables.
if not _is_script_in_cache(comp_cls, "js", input_hash):
_cache_script(
comp_cls=comp_cls,
script=content,
script="", # TODO
script_type="js",
input_hash=input_hash,
)
return input_hash
def cache_inlined_css(comp_cls: Type["Component"], content: str) -> None:
if not _is_nonempty_str(comp_cls.js):
return
# Prepare the script that's common to all instances of the same component
if not _is_script_in_cache(comp_cls, "css"):
# E.g. `my_table.css`
def wrap_component_js(comp_cls: Type["Component"], content: str) -> SafeString:
if "</script" in content:
raise RuntimeError(
f"Content of `Component.js` for component '{comp_cls.__name__}' contains '</script>' end tag. "
"This is not allowed, as it would break the HTML."
)
return f"<script>{content}</script>"
def cache_component_css(comp_cls: Type["Component"]) -> None:
"""
Cache the content from `Component.css`. This is the common CSS that's shared
among all instances of the same component. So even if the component is rendered multiple
times, this CSS is loaded only once.
"""
if not comp_cls.css or not is_nonempty_str(comp_cls.css) or _is_script_in_cache(comp_cls, "css", None):
return None
_cache_script(
comp_cls=comp_cls,
script=comp_cls.css,
script_type="css",
input_hash=None,
)
# NOTE: In CSS, we link the CSS vars to the component via a stylesheet that defines
# the CSS vars under the CSS selector `[data-djc-css-a1b2c3]`. We define the stylesheet
# with variables separately from `Component.css`, because different instances may return different
# data from `get_css_data()`, which will live in different stylesheets.
def cache_component_css_vars(comp_cls: Type["Component"], css_vars: Dict) -> Optional[str]:
if not is_nonempty_str(comp_cls.css):
return None
# The hash for the file that holds the CSS variables is derived from the variables themselves.
json_data = json.dumps(css_vars)
input_hash = md5(json_data.encode()).hexdigest()[0:6]
# Generate and cache a CSS stylesheet that contains the CSS variables.
if not _is_script_in_cache(comp_cls, "css", input_hash):
_cache_script(
comp_cls=comp_cls,
script=content,
script="", # TODO
script_type="css",
input_hash=input_hash,
)
return input_hash
def wrap_component_css(comp_cls: Type["Component"], content: str) -> SafeString:
if "</style" in content:
raise RuntimeError(
f"Content of `Component.css` for component '{comp_cls.__name__}' contains '</style>' end tag. "
"This is not allowed, as it would break the HTML."
)
return f"<style>{content}</style>"
#########################################################
# 2. Modify the HTML to use the same IDs defined in previous
@ -183,16 +270,43 @@ def cache_inlined_css(comp_cls: Type["Component"], content: str) -> None:
#########################################################
class Dependencies(NamedTuple):
# NOTE: We pass around the component CLASS, so the dependencies logic is not
# dependent on ComponentRegistries
component_cls: Type["Component"]
component_id: str
def _link_dependencies_with_component_html(
component_id: str,
html_content: str,
css_input_hash: Optional[str],
) -> str:
elems = SoupNode.from_fragment(html_content)
# Insert component ID
for elem in elems:
# Ignore comments, text, doctype, etc.
if not elem.is_element():
continue
# Component ID is used for executing JS script, e.g. `data-djc-id-a1b2c3`
#
# NOTE: We use `data-djc-css-a1b2c3` and `data-djc-id-a1b2c3` instead of
# `data-djc-css="a1b2c3"` and `data-djc-id="a1b2c3"`, to allow
# multiple values to be associated with the same element, which may happen if
# One component renders another.
elem.set_attr(f"data-djc-id-{component_id}", True)
# Attribute by which we bind the CSS variables to the component's CSS,
# e.g. `data-djc-css-a1b2c3`
if css_input_hash:
elem.set_attr(f"data-djc-css-{css_input_hash}", True)
return SoupNode.to_html_multiroot(elems)
def _insert_component_comment(
content: str,
deps: Dependencies,
# NOTE: We pass around the component CLASS, so the dependencies logic is not
# dependent on ComponentRegistries
component_cls: Type["Component"],
component_id: str,
js_input_hash: Optional[str],
css_input_hash: Optional[str],
) -> str:
"""
Given some textual content, prepend it with a short string that
@ -200,14 +314,14 @@ def _insert_component_comment(
declared JS / CSS scripts.
"""
# Add components to the cache
comp_cls_hash = _hash_comp_cls(deps.component_cls)
comp_hash_mapping[comp_cls_hash] = deps.component_cls
comp_cls_hash = _hash_comp_cls(component_cls)
comp_hash_mapping[comp_cls_hash] = component_cls
data = f"{comp_cls_hash},{deps.component_id}"
data = f"{comp_cls_hash},{component_id},{js_input_hash or ''},{css_input_hash or ''}"
# NOTE: It's important that we put the comment BEFORE the content, so we can
# use the order of comments to evaluate components' instance JS code in the correct order.
output = mark_safe(COMPONENT_DEPS_COMMENT.format(data=data)) + content
output = mark_safe(COMPONENT_DEPS_COMMENT.format(data=data) + content)
return output
@ -217,9 +331,18 @@ def postprocess_component_html(
component_cls: Type["Component"],
component_id: str,
html_content: str,
css_input_hash: Optional[str],
js_input_hash: Optional[str],
type: RenderType,
render_dependencies: bool,
) -> str:
# Make the HTML work with JS and CSS dependencies
html_content = _link_dependencies_with_component_html(
component_id=component_id,
html_content=html_content,
css_input_hash=css_input_hash,
)
# NOTE: To better understand the next section, consider this:
#
# We define and cache the component's JS and CSS at the same time as
@ -242,10 +365,10 @@ def postprocess_component_html(
# scripts are associated with it.
output = _insert_component_comment(
html_content,
Dependencies(
component_cls=component_cls,
component_id=component_id,
),
component_cls=component_cls,
component_id=component_id,
js_input_hash=js_input_hash,
css_input_hash=css_input_hash,
)
if render_dependencies:
@ -266,21 +389,34 @@ def postprocess_component_html(
TContent = TypeVar("TContent", bound=Union[bytes, str])
CSS_DEPENDENCY_PLACEHOLDER = '<link name="CSS_PLACEHOLDER">'
JS_DEPENDENCY_PLACEHOLDER = '<script name="JS_PLACEHOLDER"></script>'
CSS_PLACEHOLDER_BYTES = bytes(CSS_DEPENDENCY_PLACEHOLDER, encoding="utf-8")
JS_PLACEHOLDER_BYTES = bytes(JS_DEPENDENCY_PLACEHOLDER, encoding="utf-8")
CSS_PLACEHOLDER_NAME = "CSS_PLACEHOLDER"
CSS_PLACEHOLDER_NAME_B = CSS_PLACEHOLDER_NAME.encode()
JS_PLACEHOLDER_NAME = "JS_PLACEHOLDER"
JS_PLACEHOLDER_NAME_B = JS_PLACEHOLDER_NAME.encode()
CSS_DEPENDENCY_PLACEHOLDER = f'<link name="{CSS_PLACEHOLDER_NAME}"/>'
JS_DEPENDENCY_PLACEHOLDER = f'<script name="{JS_PLACEHOLDER_NAME}"></script>'
COMPONENT_DEPS_COMMENT = "<!-- _RENDERED {data} -->"
# E.g. `<!-- _RENDERED table,123 -->`
COMPONENT_COMMENT_REGEX = re.compile(rb"<!-- _RENDERED (?P<data>[\w\-,/]+?) -->")
# E.g. `table,123`
SCRIPT_NAME_REGEX = re.compile(rb"^(?P<comp_cls_hash>[\w\-\./]+?),(?P<id>[\w]+?)$")
# E.g. `<!-- _RENDERED table,123,a92ef298,bd002c3 -->`
COMPONENT_COMMENT_REGEX = re.compile(rb"<!--\s+_RENDERED\s+(?P<data>[\w\-,/]+?)\s+-->")
# E.g. `table,123,a92ef298,bd002c3`
# - comp_cls_hash - Cache key of the component class that was rendered
# - id - Component render ID
# - js - Cache key for the JS data from `get_js_data()`
# - css - Cache key for the CSS data from `get_css_data()`
SCRIPT_NAME_REGEX = re.compile(
rb"^(?P<comp_cls_hash>[\w\-\./]+?),(?P<id>[\w]+?),(?P<js>[0-9a-f]*?),(?P<css>[0-9a-f]*?)$"
)
# E.g. `data-djc-id-a1b2c3`
MAYBE_COMP_ID = r"(?: data-djc-id-\w{6})?"
# E.g. `data-djc-css-99914b`
MAYBE_COMP_CSS_ID = r"(?: data-djc-css-\w{6})?"
PLACEHOLDER_REGEX = re.compile(
r"{css_placeholder}|{js_placeholder}".format(
css_placeholder=CSS_DEPENDENCY_PLACEHOLDER,
js_placeholder=JS_DEPENDENCY_PLACEHOLDER,
css_placeholder=f'<link{MAYBE_COMP_CSS_ID}{MAYBE_COMP_ID} name="{CSS_PLACEHOLDER_NAME}"/>',
js_placeholder=f'<script{MAYBE_COMP_CSS_ID}{MAYBE_COMP_ID} name="{JS_PLACEHOLDER_NAME}"></script>',
).encode()
)
@ -350,10 +486,10 @@ def render_dependencies(content: TContent, type: RenderType = "document") -> TCo
nonlocal did_find_css_placeholder
nonlocal did_find_js_placeholder
if match[0] == CSS_PLACEHOLDER_BYTES:
if CSS_PLACEHOLDER_NAME_B in match[0]:
replacement = css_replacement
did_find_css_placeholder = True
elif match[0] == JS_PLACEHOLDER_BYTES:
elif JS_PLACEHOLDER_NAME_B in match[0]:
replacement = js_replacement
did_find_js_placeholder = True
else:
@ -418,11 +554,11 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
Process a textual content that may include metadata on rendered components.
The metadata has format like this
`<!-- _RENDERED component_name,component_id -->`
`<!-- _RENDERED component_name,component_id,js_hash,css_hash;... -->`
E.g.
`<!-- _RENDERED table_10bac31,123 -->`
`<!-- _RENDERED table_10bac31,123,a92ef298,bd002c3 -->`
"""
# Extract all matched instances of `<!-- _RENDERED ... -->` while also removing them from the text
all_parts: List[bytes] = list()
@ -436,23 +572,49 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
# NOTE: Python's set does NOT preserve order
seen_comp_hashes: Set[str] = set()
comp_hashes: List[str] = []
# Used for passing Python vars to JS/CSS
inputs_data: List[Tuple[str, ScriptType, Optional[str]]] = []
comp_data: List[Tuple[str, ScriptType, Optional[str]]] = []
# Process individual parts. Each part is like a CSV row of `name,id`.
# Process individual parts. Each part is like a CSV row of `name,id,js,css`.
# E.g. something like this:
# `table_10bac31,1234`
# `table_10bac31,1234,a92ef298,a92ef298`
for part in all_parts:
part_match = SCRIPT_NAME_REGEX.match(part)
if not part_match:
raise RuntimeError("Malformed dependencies data")
comp_cls_hash = part_match.group("comp_cls_hash").decode("utf-8")
comp_cls_hash: str = part_match.group("comp_cls_hash").decode("utf-8")
js_input_hash: Optional[str] = part_match.group("js").decode("utf-8") or None
css_input_hash: Optional[str] = part_match.group("css").decode("utf-8") or None
if comp_cls_hash in seen_comp_hashes:
continue
comp_hashes.append(comp_cls_hash)
seen_comp_hashes.add(comp_cls_hash)
# Schedule to load the `<script>` / `<link>` tags for the JS / CSS from `Component.js/css`.
comp_data.append((comp_cls_hash, "js", None))
comp_data.append((comp_cls_hash, "css", None))
# Schedule to load the `<script>` / `<link>` tags for the JS / CSS variables.
# Skip if no variables are defined.
if js_input_hash is not None:
inputs_data.append((comp_cls_hash, "js", js_input_hash))
if css_input_hash is not None:
inputs_data.append((comp_cls_hash, "css", css_input_hash))
(
to_load_input_js_urls,
to_load_input_css_urls,
inlined_input_js_tags,
inlined_input_css_tags,
loaded_input_js_urls,
loaded_input_css_urls,
) = _prepare_tags_and_urls(inputs_data, type)
(
to_load_component_js_urls,
to_load_component_css_urls,
@ -460,7 +622,7 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
inlined_component_css_tags,
loaded_component_js_urls,
loaded_component_css_urls,
) = _prepare_tags_and_urls(comp_hashes, type)
) = _prepare_tags_and_urls(comp_data, type)
def get_component_media(comp_cls_hash: str) -> Media:
comp_cls = comp_hash_mapping[comp_cls_hash]
@ -473,8 +635,8 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
*[get_component_media(comp_cls_hash) for comp_cls_hash in comp_hashes],
# All the inlined scripts that we plan to fetch / load
Media(
js=to_load_component_js_urls,
css={"all": to_load_component_css_urls},
js=[*to_load_component_js_urls, *to_load_input_js_urls],
css={"all": [*to_load_component_css_urls, *to_load_input_css_urls]},
),
]
@ -492,6 +654,7 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
loaded_css_urls = sorted(
[
*loaded_component_css_urls,
*loaded_input_css_urls,
# NOTE: When rendering a document, the initial CSS is inserted directly into the HTML
# to avoid a flash of unstyled content. In the dependency manager, we only mark those
# scripts as loaded.
@ -501,6 +664,7 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
loaded_js_urls = sorted(
[
*loaded_component_js_urls,
*loaded_input_js_urls,
# NOTE: When rendering a document, the initial JS is inserted directly into the HTML
# so the scripts are executed at proper order. In the dependency manager, we only mark those
# scripts as loaded.
@ -533,6 +697,8 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
# so the scripts are executed at proper order. In the dependency manager, we only mark those
# scripts as loaded.
*(to_load_js_tags if type == "document" else []),
# JS variables
*[tag for tag in inlined_input_js_tags],
# JS from `Component.js` (if not fragment)
*[tag for tag in inlined_component_js_tags],
]
@ -544,6 +710,8 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
# <NONE>
# CSS from `Component.css` (if not fragment)
*[tag for tag in inlined_component_css_tags],
# CSS variables
*[tag for tag in inlined_input_css_tags],
# CSS from `Media.css` (plus from `Component.css` if fragment)
# NOTE: Similarly to JS, the initial CSS is loaded outside of the dependency
# manager, and only marked as loaded, to avoid a flash of unstyled content.
@ -554,10 +722,6 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
return (content, final_script_tags.encode("utf-8"), final_css_tags.encode("utf-8"))
def _is_nonempty_str(txt: Optional[str]) -> bool:
return txt is not None and bool(txt.strip())
# Detect duplicates by URLs, extract URLs, and sort by URLs
def _postprocess_media_tags(
script_type: ScriptType,
@ -572,7 +736,7 @@ def _postprocess_media_tags(
attr = "src" if script_type == "js" else "href"
maybe_url = node.get_attr(attr, None)
if not _is_nonempty_str(maybe_url):
if not is_nonempty_str(maybe_url):
raise RuntimeError(
f"One of entries for `Component.Media.{script_type}` media is missing a "
f"value for attribute '{attr}'. If there is content inlined inside the `<{node.name()}>` tags, "
@ -595,7 +759,7 @@ def _postprocess_media_tags(
def _prepare_tags_and_urls(
data: Iterable[str],
data: List[Tuple[str, ScriptType, Optional[str]]],
type: RenderType,
) -> Tuple[List[str], List[str], List[str], List[str], List[str], List[str]]:
to_load_js_urls: List[str] = []
@ -609,7 +773,7 @@ def _prepare_tags_and_urls(
# But even in that case we still need to call `Components.manager.markScriptLoaded`,
# so the client knows NOT to fetch them again.
# So in that case we populate both `inlined` and `loaded` lists
for comp_cls_hash in data:
for comp_cls_hash, script_type, input_hash in data:
# NOTE: When CSS is scoped, then EVERY component instance will have different
# copy of the style, because each copy will have component's ID embedded.
# So, in that case we inline the style into the HTML (See `_link_dependencies_with_component_html`),
@ -618,22 +782,27 @@ def _prepare_tags_and_urls(
if type == "document":
# NOTE: Skip fetching of inlined JS/CSS if it's not defined or empty for given component
if _is_nonempty_str(comp_cls.js):
inlined_js_tags.append(_get_script_tag("js", comp_cls))
loaded_js_urls.append(get_script_url("js", comp_cls))
#
# NOTE: If `input_hash` is `None`, then we get the component's JS/CSS
# (e.g. `/components/cache/table.js`).
# And if `input_hash` is given, we get the component's JS/CSS variables
# (e.g. `/components/cache/table.0ab2c3.js`).
if script_type == "js" and is_nonempty_str(comp_cls.js):
inlined_js_tags.append(get_script_tag("js", comp_cls, input_hash))
loaded_js_urls.append(get_script_url("js", comp_cls, input_hash))
if _is_nonempty_str(comp_cls.css):
inlined_css_tags.append(_get_script_tag("css", comp_cls))
loaded_css_urls.append(get_script_url("css", comp_cls))
if script_type == "css" and is_nonempty_str(comp_cls.css):
inlined_css_tags.append(get_script_tag("css", comp_cls, input_hash))
loaded_css_urls.append(get_script_url("css", comp_cls, input_hash))
# When NOT a document (AKA is a fragment), then scripts are NOT inserted into
# the HTML, and instead we fetch and load them all via our JS dependency manager.
else:
if _is_nonempty_str(comp_cls.js):
to_load_js_urls.append(get_script_url("js", comp_cls))
if script_type == "js" and is_nonempty_str(comp_cls.js):
to_load_js_urls.append(get_script_url("js", comp_cls, input_hash))
if _is_nonempty_str(comp_cls.css):
to_load_css_urls.append(get_script_url("css", comp_cls))
if script_type == "css" and is_nonempty_str(comp_cls.css):
to_load_css_urls.append(get_script_url("css", comp_cls, input_hash))
return (
to_load_js_urls,
@ -648,43 +817,36 @@ def _prepare_tags_and_urls(
def get_script_content(
script_type: ScriptType,
comp_cls: Type["Component"],
input_hash: Optional[str],
) -> SafeString:
comp_cls_hash = _hash_comp_cls(comp_cls)
cache_key = _gen_cache_key(comp_cls_hash, script_type)
cache_key = _gen_cache_key(comp_cls_hash, script_type, input_hash)
script = comp_media_cache.get(cache_key)
return script
def _get_script_tag(
def get_script_tag(
script_type: ScriptType,
comp_cls: Type["Component"],
input_hash: Optional[str],
) -> SafeString:
script = get_script_content(script_type, comp_cls)
content = get_script_content(script_type, comp_cls, input_hash)
if script_type == "js":
if "</script" in script:
raise RuntimeError(
f"Content of `Component.js` for component '{comp_cls.__name__}' contains '</script>' end tag. "
"This is not allowed, as it would break the HTML."
)
return f"<script>{script}</script>"
content = wrap_component_js(comp_cls, content)
elif script_type == "css":
if "</style" in script:
raise RuntimeError(
f"Content of `Component.css` for component '{comp_cls.__name__}' contains '</style>' end tag. "
"This is not allowed, as it would break the HTML."
)
content = wrap_component_css(comp_cls, content)
else:
raise ValueError(f"Unexpected script_type '{script_type}'")
return f"<style>{script}</style>"
return script
return content
def get_script_url(
script_type: ScriptType,
comp_cls: Type["Component"],
input_hash: Optional[str],
) -> str:
comp_cls_hash = _hash_comp_cls(comp_cls)
@ -693,6 +855,7 @@ def get_script_url(
kwargs={
"comp_cls_hash": comp_cls_hash,
"script_type": script_type,
**({"input_hash": input_hash} if input_hash is not None else {}),
},
)
@ -703,10 +866,11 @@ def _gen_exec_script(
loaded_js_urls: List[str],
loaded_css_urls: List[str],
) -> Optional[str]:
if not to_load_js_tags and not to_load_css_tags and not loaded_css_urls and not loaded_js_urls:
# Return None if all lists are empty
if not any([to_load_js_tags, to_load_css_tags, loaded_css_urls, loaded_js_urls]):
return None
def map_to_base64(lst: List[str]) -> List[str]:
def map_to_base64(lst: Sequence[str]) -> List[str]:
return [base64.b64encode(tag.encode()).decode() for tag in lst]
# Generate JSON that will tell the JS dependency manager which JS and CSS to load
@ -797,14 +961,16 @@ def cached_script_view(
req: HttpRequest,
comp_cls_hash: str,
script_type: ScriptType,
input_hash: Optional[str] = None,
) -> HttpResponse:
if req.method != "GET":
return HttpResponseNotAllowed(["GET"])
# Otherwise check if the file is among the dynamically generated files in the cache
cache_key = _gen_cache_key(comp_cls_hash, script_type)
script = comp_media_cache.get(cache_key)
comp_cls = comp_hash_mapping.get(comp_cls_hash)
if comp_cls is None:
return HttpResponseNotFound()
script = get_script_content(script_type, comp_cls, input_hash)
if script is None:
return HttpResponseNotFound()
@ -813,7 +979,8 @@ def cached_script_view(
urlpatterns = [
# E.g. `/components/cache/table.js`
# E.g. `/components/cache/table.js` or `/components/cache/table.0ab2c3.js`
path("cache/<str:comp_cls_hash>.<str:input_hash>.<str:script_type>", cached_script_view, name=CACHE_ENDPOINT_NAME),
path("cache/<str:comp_cls_hash>.<str:script_type>", cached_script_view, name=CACHE_ENDPOINT_NAME),
]

View file

@ -8,7 +8,6 @@ from django_components.context import set_provided_context_var
from django_components.expression import RuntimeKwargs
from django_components.node import BaseNode
from django_components.util.logger import trace_msg
from django_components.util.misc import gen_id
PROVIDE_NAME_KWARG = "name"
@ -28,10 +27,7 @@ class ProvideNode(BaseNode):
):
super().__init__(nodelist=nodelist, args=None, kwargs=kwargs, node_id=node_id)
self.nodelist = nodelist
self.node_id = node_id or gen_id()
self.trace_id = trace_id
self.kwargs = kwargs or RuntimeKwargs({})
def __repr__(self) -> str:
return f"<Provide Node: {self.node_id}. Contents: {repr(self.nodelist)}>"

View file

@ -1 +1 @@
(()=>{var x=o=>new DOMParser().parseFromString(o,"text/html").documentElement.textContent,E=Array.isArray,m=o=>typeof o=="function",H=o=>o!==null&&typeof o=="object",S=o=>(H(o)||m(o))&&m(o.then)&&m(o.catch);function N(o,i){try{return i?o.apply(null,i):o()}catch(s){L(s)}}function g(o,i){if(m(o)){let s=N(o,i);return s&&S(s)&&s.catch(c=>{L(c)}),[s]}if(E(o)){let s=[];for(let c=0;c<o.length;c++)s.push(g(o[c],i));return s}else console.warn(`[Components] Invalid value type passed to callWithAsyncErrorHandling(): ${typeof o}`)}function L(o){console.error(o)}var M=o=>{let i=new MutationObserver(s=>{for(let c of s)c.type==="childList"&&c.addedNodes.forEach(p=>{p.nodeName==="SCRIPT"&&p.hasAttribute("data-djc")&&o(p)})});return i.observe(document,{childList:!0,subtree:!0}),i};var y=()=>{let o=new Set,i=new Set,s={},c={},p=t=>{let e=new DOMParser().parseFromString(t,"text/html").querySelector("script");if(!e)throw Error("[Components] Failed to extract <script> tag. Make sure that the string contains <script><\/script> and is a valid HTML");return e},F=t=>{let e=new DOMParser().parseFromString(t,"text/html").querySelector("link");if(!e)throw Error("[Components] Failed to extract <link> tag. Make sure that the string contains <link></link> and is a valid HTML");return e},T=t=>{let e=document.createElement(t.tagName);e.innerHTML=t.innerHTML;for(let r of t.attributes)e.setAttributeNode(r.cloneNode());return e},f=t=>{let e=p(t),r=e.getAttribute("src");if(!r||C("js",r))return;d("js",r);let a=T(e),l=e.getAttribute("async")!=null||e.getAttribute("defer")!=null||e.getAttribute("type")==="module";a.async=l;let u=new Promise((n,b)=>{a.onload=()=>{n()},globalThis.document.body.append(a)});return{el:a,promise:u}},h=t=>{let e=F(t),r=e.getAttribute("href");if(!r||C("css",r))return;let a=T(e);return globalThis.document.head.append(a),d("css",r),{el:a,promise:Promise.resolve()}},d=(t,e)=>{if(t!=="js"&&t!=="css")throw Error(`[Components] markScriptLoaded received invalid script type '${t}'. Must be one of 'js', 'css'`);(t==="js"?o:i).add(e)},C=(t,e)=>{if(t!=="js"&&t!=="css")throw Error(`[Components] isScriptLoaded received invalid script type '${t}'. Must be one of 'js', 'css'`);return(t==="js"?o:i).has(e)},w=(t,e)=>{s[t]=e},j=(t,e,r)=>{let a=`${t}:${e}`;c[a]=r},A=(t,e,r)=>{let a=s[t];if(!a)throw Error(`[Components] '${t}': No component registered for that name`);let l=Array.from(document.querySelectorAll(`[data-comp-id-${e}]`));if(!l.length)throw Error(`[Components] '${t}': No elements with component ID '${e}' found`);let u=`${t}:${r}`,n=c[u];if(!n)throw Error(`[Components] '${t}': Cannot find input for hash '${r}'`);let b=n(),v={name:t,id:e,els:l},[P]=g(a,[b,v]);return P},k=async t=>{let e=t.loadedCssUrls.map(n=>atob(n)),r=t.loadedJsUrls.map(n=>atob(n)),a=t.toLoadCssTags.map(n=>atob(n)),l=t.toLoadJsTags.map(n=>atob(n));e.forEach(n=>d("css",n)),r.forEach(n=>d("js",n)),Promise.all(a.map(n=>h(n))).catch(console.error);let u=Promise.all(l.map(n=>f(n))).catch(console.error)};return M(t=>{let e=JSON.parse(t.text);k(e)}),{callComponent:A,registerComponent:w,registerComponentData:j,loadJs:f,loadCss:h,markScriptLoaded:d}};var $={manager:y(),createComponentsManager:y,unescapeJs:x};globalThis.Components=$;})();
(()=>{var x=o=>new DOMParser().parseFromString(o,"text/html").documentElement.textContent,E=Array.isArray,m=o=>typeof o=="function",H=o=>o!==null&&typeof o=="object",S=o=>(H(o)||m(o))&&m(o.then)&&m(o.catch);function N(o,i){try{return i?o.apply(null,i):o()}catch(s){L(s)}}function g(o,i){if(m(o)){let s=N(o,i);return s&&S(s)&&s.catch(c=>{L(c)}),[s]}if(E(o)){let s=[];for(let c=0;c<o.length;c++)s.push(g(o[c],i));return s}else console.warn(`[Components] Invalid value type passed to callWithAsyncErrorHandling(): ${typeof o}`)}function L(o){console.error(o)}var M=o=>{let i=new MutationObserver(s=>{for(let c of s)c.type==="childList"&&c.addedNodes.forEach(d=>{d.nodeName==="SCRIPT"&&d.hasAttribute("data-djc")&&o(d)})});return i.observe(document,{childList:!0,subtree:!0}),i};var y=()=>{let o=new Set,i=new Set,s={},c={},d=t=>{let e=new DOMParser().parseFromString(t,"text/html").querySelector("script");if(!e)throw Error("[Components] Failed to extract <script> tag. Make sure that the string contains <script><\/script> and is a valid HTML");return e},F=t=>{let e=new DOMParser().parseFromString(t,"text/html").querySelector("link");if(!e)throw Error("[Components] Failed to extract <link> tag. Make sure that the string contains <link></link> and is a valid HTML");return e},T=t=>{let e=document.createElement(t.tagName);e.innerHTML=t.innerHTML;for(let r of t.attributes)e.setAttributeNode(r.cloneNode());return e},f=t=>{let e=d(t),r=e.getAttribute("src");if(!r||C("js",r))return;p("js",r);let a=T(e),l=e.getAttribute("async")!=null||e.getAttribute("defer")!=null||e.getAttribute("type")==="module";a.async=l;let u=new Promise((n,b)=>{a.onload=()=>{n()},globalThis.document.body.append(a)});return{el:a,promise:u}},h=t=>{let e=F(t),r=e.getAttribute("href");if(!r||C("css",r))return;let a=T(e);return globalThis.document.head.append(a),p("css",r),{el:a,promise:Promise.resolve()}},p=(t,e)=>{if(t!=="js"&&t!=="css")throw Error(`[Components] markScriptLoaded received invalid script type '${t}'. Must be one of 'js', 'css'`);(t==="js"?o:i).add(e)},C=(t,e)=>{if(t!=="js"&&t!=="css")throw Error(`[Components] isScriptLoaded received invalid script type '${t}'. Must be one of 'js', 'css'`);return(t==="js"?o:i).has(e)},w=(t,e)=>{s[t]=e},j=(t,e,r)=>{let a=`${t}:${e}`;c[a]=r},A=(t,e,r)=>{let a=s[t];if(!a)throw Error(`[Components] '${t}': No component registered for that name`);let l=Array.from(document.querySelectorAll(`[data-djc-id-${e}]`));if(!l.length)throw Error(`[Components] '${t}': No elements with component ID '${e}' found`);let u=`${t}:${r}`,n=c[u];if(!n)throw Error(`[Components] '${t}': Cannot find input for hash '${r}'`);let b=n(),v={name:t,id:e,els:l},[P]=g(a,[b,v]);return P},k=async t=>{let e=t.loadedCssUrls.map(n=>atob(n)),r=t.loadedJsUrls.map(n=>atob(n)),a=t.toLoadCssTags.map(n=>atob(n)),l=t.toLoadJsTags.map(n=>atob(n));e.forEach(n=>p("css",n)),r.forEach(n=>p("js",n)),Promise.all(a.map(n=>h(n))).catch(console.error);let u=Promise.all(l.map(n=>f(n))).catch(console.error)};return M(t=>{let e=JSON.parse(t.text);k(e)}),{callComponent:A,registerComponent:w,registerComponentData:j,loadJs:f,loadCss:h,markScriptLoaded:p}};var $={manager:y(),createComponentsManager:y,unescapeJs:x};globalThis.Components=$;})();

View file

@ -59,3 +59,7 @@ def get_last_index(lst: List, key: Callable[[Any], bool]) -> Optional[int]:
if key(item):
return len(lst) - 1 - index
return None
def is_nonempty_str(txt: Optional[str]) -> bool:
return txt is not None and bool(txt.strip())

View file

@ -26,7 +26,7 @@ Components.registerComponentData(
Components.callComponent(
"table", // Component name
12345, // Component ID - An HTML element with corresponding
// attribute (`data-comp-id-12345`) MUST
// attribute (`data-djc-id-12345`) MUST
// be present in the DOM.
"3d09cf", // Input ID
);

View file

@ -209,7 +209,7 @@ export const createComponentsManager = () => {
const initFn = components[name];
if (!initFn) throw Error(`[Components] '${name}': No component registered for that name`);
const elems = Array.from(document.querySelectorAll<HTMLElement>(`[data-comp-id-${compId}]`));
const elems = Array.from(document.querySelectorAll<HTMLElement>(`[data-djc-id-${compId}]`));
if (!elems.length) throw Error(`[Components] '${name}': No elements with component ID '${compId}' found`);
const dataKey = `${name}:${inputHash}`;

View file

@ -101,7 +101,7 @@ class HtmlAttrsTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div @click.stop="dispatch('click_event')" x-data="{hello: 'world'}" class="padding-top-8 added_class another-class" data-id=123>
<div @click.stop="dispatch('click_event')" x-data="{hello: 'world'}" class="padding-top-8 added_class another-class" data-djc-id-a1bc3f data-id=123>
content
</div>
""", # noqa: E501
@ -153,7 +153,7 @@ class HtmlAttrsTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div @click.stop="dispatch('click_event')" class="added_class another-class padding-top-8" data-id="123" x-data="{hello: 'world'}">
<div @click.stop="dispatch('click_event')" class="added_class another-class padding-top-8" data-djc-id-a1bc3f data-id="123" x-data="{hello: 'world'}">
content
</div>
""", # noqa: E501
@ -181,7 +181,7 @@ class HtmlAttrsTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div @click.stop="dispatch('click_event')" x-data="{hello: 'world'}" class="padding-top-8 added_class another-class" data-id=123>
<div @click.stop="dispatch('click_event')" x-data="{hello: 'world'}" class="padding-top-8 added_class another-class" data-djc-id-a1bc3f data-id=123>
content
</div>
""", # noqa: E501
@ -213,7 +213,7 @@ class HtmlAttrsTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div @click.stop="dispatch('click_event')" class="added_class another-class padding-top-8" data-id="123" x-data="{hello: 'world'}">
<div @click.stop="dispatch('click_event')" class="added_class another-class padding-top-8" data-djc-id-a1bc3f data-id="123" x-data="{hello: 'world'}">
content
</div>
""", # noqa: E501
@ -240,7 +240,7 @@ class HtmlAttrsTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div class="added_class another-class from_agg_key" data-id="123" type="submit">
<div class="added_class another-class from_agg_key" data-djc-id-a1bc3f data-id="123" type="submit">
content
</div>
""", # noqa: E501
@ -304,7 +304,7 @@ class HtmlAttrsTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div class="added_class another-class override-me" data-id=123>
<div class="added_class another-class override-me" data-djc-id-a1bc3f data-id=123>
content
</div>
""",
@ -333,7 +333,7 @@ class HtmlAttrsTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div @click.stop="dispatch('click_event')" x-data="{hello: 'world'}" class="padding-top-8 added_class another-class" data-id=123>
<div @click.stop="dispatch('click_event')" x-data="{hello: 'world'}" class="padding-top-8 added_class another-class" data-djc-id-a1bc3f data-id=123>
content
</div>
""", # noqa: E501
@ -358,7 +358,7 @@ class HtmlAttrsTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div class="added_class another-class" data-id="123">
<div class="added_class another-class" data-djc-id-a1bc3f data-id="123">
content
</div>
""",
@ -386,7 +386,7 @@ class HtmlAttrsTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div >
<div data-djc-id-a1bc3f>
content
</div>
""",
@ -414,7 +414,7 @@ class HtmlAttrsTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div >
<div data-djc-id-a1bc3f>
content
</div>
""",

View file

@ -101,7 +101,7 @@ class ComponentOldTemplateApiTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
Variable: <strong>test</strong>
Variable: <strong data-djc-id-a1bc3e>test</strong>
""",
)
@ -175,7 +175,7 @@ class ComponentTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
Variable: <strong>test</strong>
Variable: <strong data-djc-id-a1bc3e>test</strong>
""",
)
@ -201,7 +201,7 @@ class ComponentTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
Variable: <strong>test</strong>
Variable: <strong data-djc-id-a1bc3e>test</strong>
""",
)
@ -223,7 +223,7 @@ class ComponentTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
Variable: <strong>test</strong>
Variable: <strong data-djc-id-a1bc3e>test</strong>
""",
)
@ -244,13 +244,13 @@ class ComponentTest(BaseTestCase):
self.assertHTMLEqual(
SvgComponent.render(kwargs={"name": "svg1"}),
"""
<svg>Dynamic1</svg>
<svg data-djc-id-a1bc3e>Dynamic1</svg>
""",
)
self.assertHTMLEqual(
SvgComponent.render(kwargs={"name": "svg2"}),
"""
<svg>Dynamic2</svg>
<svg data-djc-id-a1bc3f>Dynamic2</svg>
""",
)
@ -270,7 +270,7 @@ class ComponentTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
Variable: <strong>test</strong>
Variable: <strong data-djc-id-a1bc3e>test</strong>
""",
)
@ -314,7 +314,7 @@ class ComponentTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
Variable: <strong>test</strong> MY_SLOT
Variable: <strong data-djc-id-a1bc3e>test</strong> MY_SLOT
""",
)
@ -407,7 +407,7 @@ class ComponentValidationTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
Variable: <strong>test</strong>
Variable: <strong data-djc-id-a1bc3e>test</strong>
Slot 1: MY_SLOT
Slot 2: abc
""",
@ -536,7 +536,7 @@ class ComponentValidationTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
Variable: <strong>test</strong>
Variable: <strong data-djc-id-a1bc3e>test</strong>
Slot 1: 123.5
Slot 2: abc
""",
@ -568,7 +568,7 @@ class ComponentValidationTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
Variable: <strong>test</strong>
Variable: <strong data-djc-id-a1bc3e>test</strong>
Slot 1: MY_SLOT
Slot 2: abc
""",
@ -655,7 +655,7 @@ class ComponentValidationTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
Name: <strong>TestComponent</strong>
Name: <strong data-djc-id-a1bc3e>TestComponent</strong>
""",
)
@ -960,7 +960,7 @@ class ComponentRenderTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<custom-template data-djc-id-a1bc3e>
<header>Default header</header>
<main>Default main</main>
<footer>Default footer</footer>
@ -982,7 +982,7 @@ class ComponentRenderTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<custom-template data-djc-id-a1bc3e>
<header>Default header</header>
<main>Default main</main>
<footer>Default footer</footer>
@ -1005,7 +1005,7 @@ class ComponentRenderTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<custom-template data-djc-id-a1bc3e>
<header>Default header</header>
<main>Default main</main>
<footer>Default footer</footer>
@ -1057,7 +1057,7 @@ class ComponentRenderTest(BaseTestCase):
# """
self.assertInHTML(
"""
<kbd>
<kbd data-djc-id-a1bc3e>
Rendered via GET request
</kbd>
""",
@ -1150,7 +1150,7 @@ class ComponentRenderTest(BaseTestCase):
rendered,
"""
<!DOCTYPE html>
<html lang="en">
<html data-djc-id-a1bc3e lang="en">
<body>
<main role="main">
<div class='container main-container'>
@ -1169,13 +1169,13 @@ class ComponentRenderTest(BaseTestCase):
def get_context_data(self, **attrs):
return {
"id": self.component_id,
"id": self.id,
}
rendered = TestComponent(component_id="123").render()
rendered = TestComponent.render()
self.assertHTMLEqual(
rendered,
"Variable: <strong>123</strong>",
"Variable: <strong data-djc-id-a1bc3e>a1bc3e</strong>",
)
@parametrize_context_behavior(["django", "isolated"])
@ -1185,13 +1185,13 @@ class ComponentRenderTest(BaseTestCase):
def get_context_data(self, **attrs):
return {
"id": self.component_id,
"id": self.id,
}
rendered_resp = TestComponent(component_id="123").render_to_response()
rendered_resp = TestComponent.render_to_response()
self.assertHTMLEqual(
rendered_resp.content.decode("utf-8"),
"Variable: <strong>123</strong>",
"Variable: <strong data-djc-id-a1bc3e>a1bc3e</strong>",
)

View file

@ -56,9 +56,9 @@ class TestComponentAsView(BaseTestCase):
client = CustomClient(urlpatterns=[path("test_template/", render_template_view)])
response = client.get("/test_template/")
self.assertEqual(response.status_code, 200)
self.assertIn(
b'<input type="text" name="variable" value="TEMPLATE">',
response.content,
self.assertInHTML(
'<input type="text" name="variable" value="TEMPLATE">',
response.content.decode(),
)
def test_get_request(self):
@ -81,9 +81,9 @@ class TestComponentAsView(BaseTestCase):
client = CustomClient(urlpatterns=[path("test/", MockComponentRequest.as_view())])
response = client.get("/test/")
self.assertEqual(response.status_code, 200)
self.assertIn(
b'<input type="text" name="variable" value="GET">',
response.content,
self.assertInHTML(
'<input type="text" name="variable" value="GET">',
response.content.decode(),
)
def test_get_request_shortcut(self):
@ -105,9 +105,9 @@ class TestComponentAsView(BaseTestCase):
client = CustomClient(urlpatterns=[path("test/", MockComponentRequest.as_view())])
response = client.get("/test/")
self.assertEqual(response.status_code, 200)
self.assertIn(
b'<input type="text" name="variable" value="GET">',
response.content,
self.assertInHTML(
'<input type="text" name="variable" value="GET">',
response.content.decode(),
)
def test_post_request(self):
@ -131,9 +131,9 @@ class TestComponentAsView(BaseTestCase):
client = CustomClient(urlpatterns=[path("test/", MockComponentRequest.as_view())])
response = client.post("/test/", {"variable": "POST"})
self.assertEqual(response.status_code, 200)
self.assertIn(
b'<input type="text" name="variable" value="POST">',
response.content,
self.assertInHTML(
'<input type="text" name="variable" value="POST">',
response.content.decode(),
)
def test_post_request_shortcut(self):
@ -156,9 +156,9 @@ class TestComponentAsView(BaseTestCase):
client = CustomClient(urlpatterns=[path("test/", MockComponentRequest.as_view())])
response = client.post("/test/", {"variable": "POST"})
self.assertEqual(response.status_code, 200)
self.assertIn(
b'<input type="text" name="variable" value="POST">',
response.content,
self.assertInHTML(
'<input type="text" name="variable" value="POST">',
response.content.decode(),
)
def test_instantiate_component(self):
@ -178,9 +178,9 @@ class TestComponentAsView(BaseTestCase):
client = CustomClient(urlpatterns=[path("test/", MockComponentRequest("my_comp").as_view())])
response = client.get("/test/")
self.assertEqual(response.status_code, 200)
self.assertIn(
b'<input type="text" name="variable" value="my_comp">',
response.content,
self.assertInHTML(
'<input type="text" name="variable" value="my_comp">',
response.content.decode(),
)
@parametrize_context_behavior(["django", "isolated"])

View file

@ -24,7 +24,7 @@ class InlineComponentTest(BaseTestCase):
self.assertHTMLEqual(
InlineHTMLComponent.render(),
"<div class='inline'>Hello Inline</div>",
'<div class="inline" data-djc-id-a1bc3e>Hello Inline</div>',
)
def test_inlined_js_and_css(self):
@ -41,7 +41,7 @@ class InlineComponentTest(BaseTestCase):
rendered = TestComponent.render()
self.assertInHTML(
"<div class='html-css-only'>Content</div>",
'<div class="html-css-only" data-djc-id-a1bc3e>Content</div>',
rendered,
)
self.assertInHTML(
@ -62,7 +62,7 @@ class InlineComponentTest(BaseTestCase):
context = Context({"variable": "Dynamic Content"})
self.assertHTMLEqual(
comp.render(context),
"<div class='variable-html'>Dynamic Content</div>",
'<div class="variable-html" data-djc-id-a1bc3e>Dynamic Content</div>',
)
def test_html_variable_filtered(self):
@ -82,8 +82,8 @@ class InlineComponentTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
Var1: <strong>test1</strong>
Var2 (uppercased): <strong>TEST2</strong>
Var1: <strong data-djc-id-a1bc3e>test1</strong>
Var2 (uppercased): <strong data-djc-id-a1bc3e>TEST2</strong>
""",
)
@ -691,7 +691,7 @@ class MediaRelativePathTests(BaseTestCase):
self.assertInHTML(
"""
<form method="post">
<form data-djc-id-a1bc41 method="post">
<input type="text" name="variable" value="test">
<input type="submit">
</form>
@ -730,7 +730,7 @@ class MediaRelativePathTests(BaseTestCase):
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertIn('<input type="text" name="variable" value="hello">', rendered, rendered)
self.assertInHTML('<input type="text" name="variable" value="hello">', rendered)
# Settings required for autodiscover to work
@override_settings(

View file

@ -100,13 +100,9 @@ class ContextTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context())
self.assertIn("<h1>Shadowing variable = override</h1>", rendered, rendered)
self.assertIn(
"<h1>Shadowing variable = slot_default_override</h1>",
rendered,
rendered,
)
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
self.assertInHTML("<h1 data-djc-id-a1bc43>Shadowing variable = override</h1>", rendered)
self.assertInHTML("<h1 data-djc-id-a1bc44>Shadowing variable = slot_default_override</h1>", rendered)
self.assertNotIn("Shadowing variable = NOT SHADOWED", rendered)
@parametrize_context_behavior(["django", "isolated"])
def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_tag(
@ -119,10 +115,9 @@ class ContextTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context())
self.assertIn("<h1>Uniquely named variable = unique_val</h1>", rendered, rendered)
self.assertIn(
"<h1>Uniquely named variable = slot_default_unique</h1>",
rendered,
self.assertInHTML("<h1 data-djc-id-a1bc43>Uniquely named variable = unique_val</h1>", rendered)
self.assertInHTML(
"<h1 data-djc-id-a1bc44>Uniquely named variable = slot_default_unique</h1>",
rendered,
)
@ -140,13 +135,9 @@ class ContextTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context())
self.assertIn("<h1>Shadowing variable = override</h1>", rendered, rendered)
self.assertIn(
"<h1>Shadowing variable = shadow_from_slot</h1>",
rendered,
rendered,
)
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
self.assertInHTML("<h1 data-djc-id-a1bc45>Shadowing variable = override</h1>", rendered)
self.assertInHTML("<h1 data-djc-id-a1bc46>Shadowing variable = shadow_from_slot</h1>", rendered)
self.assertNotIn("Shadowing variable = NOT SHADOWED", rendered)
@parametrize_context_behavior(["django", "isolated"])
def test_nested_component_instances_have_unique_context_with_filled_slots(self):
@ -162,12 +153,8 @@ class ContextTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context())
self.assertIn("<h1>Uniquely named variable = unique_val</h1>", rendered, rendered)
self.assertIn(
"<h1>Uniquely named variable = unique_from_slot</h1>",
rendered,
rendered,
)
self.assertInHTML('<h1 data-djc-id-a1bc45="">Uniquely named variable = unique_val</h1>', rendered)
self.assertInHTML('<h1 data-djc-id-a1bc46="">Uniquely named variable = unique_from_slot</h1>', rendered)
@parametrize_context_behavior(["django", "isolated"])
def test_nested_component_context_shadows_outer_context_with_unfilled_slots_and_component_tag(
@ -180,13 +167,9 @@ class ContextTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
self.assertIn("<h1>Shadowing variable = override</h1>", rendered, rendered)
self.assertIn(
"<h1>Shadowing variable = slot_default_override</h1>",
rendered,
rendered,
)
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
self.assertInHTML("<h1 data-djc-id-a1bc43>Shadowing variable = override</h1>", rendered)
self.assertInHTML("<h1 data-djc-id-a1bc44>Shadowing variable = slot_default_override</h1>", rendered)
self.assertNotIn("Shadowing variable = NOT SHADOWED", rendered)
@parametrize_context_behavior(["django", "isolated"])
def test_nested_component_context_shadows_outer_context_with_filled_slots(
@ -204,13 +187,9 @@ class ContextTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
self.assertIn("<h1>Shadowing variable = override</h1>", rendered, rendered)
self.assertIn(
"<h1>Shadowing variable = shadow_from_slot</h1>",
rendered,
rendered,
)
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
self.assertInHTML("<h1 data-djc-id-a1bc45>Shadowing variable = override</h1>", rendered)
self.assertInHTML("<h1 data-djc-id-a1bc46>Shadowing variable = shadow_from_slot</h1>", rendered)
self.assertNotIn("Shadowing variable = NOT SHADOWED", rendered)
class ParentArgsTests(BaseTestCase):
@ -253,15 +232,15 @@ class ParentArgsTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div>
<div data-djc-id-a1bc3f>
<h1>Parent content</h1>
<h1>Shadowing variable = passed_in</h1>
<h1>Uniquely named variable = unique_val</h1>
<h1 data-djc-id-a1bc43>Shadowing variable = passed_in</h1>
<h1 data-djc-id-a1bc43>Uniquely named variable = unique_val</h1>
</div>
<div>
<div data-djc-id-a1bc3f>
<h2>Slot content</h2>
<h1>Shadowing variable = slot_default_override</h1>
<h1>Uniquely named variable = passed_in</h1>
<h1 data-djc-id-a1bc44>Shadowing variable = slot_default_override</h1>
<h1 data-djc-id-a1bc44>Uniquely named variable = passed_in</h1>
</div>
""",
)
@ -275,9 +254,9 @@ class ParentArgsTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context())
self.assertIn("<h1>Shadowing variable = passed_in</h1>", rendered, rendered)
self.assertIn("<h1>Uniquely named variable = passed_in</h1>", rendered, rendered)
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
self.assertInHTML("<h1 data-djc-id-a1bc43>Shadowing variable = passed_in</h1>", rendered)
self.assertInHTML("<h1 data-djc-id-a1bc44>Uniquely named variable = passed_in</h1>", rendered)
self.assertNotIn("Shadowing variable = NOT SHADOWED", rendered)
# NOTE: Second arg in tuple are expected values passed through components.
@parametrize_context_behavior(
@ -300,17 +279,18 @@ class ParentArgsTests(BaseTestCase):
""" # noqa: E501
template = Template(template_str)
rendered = template.render(Context())
self.assertHTMLEqual(
rendered,
f"""
<div>
<div data-djc-id-a1bc41>
<h1>Parent content</h1>
<h1>Shadowing variable = {first_val}</h1>
<h1>Uniquely named variable = unique_val</h1>
<h1 data-djc-id-a1bc45>Shadowing variable = {first_val}</h1>
<h1 data-djc-id-a1bc45>Uniquely named variable = unique_val</h1>
</div>
<div>
<h1>Shadowing variable = value_from_slot</h1>
<h1>Uniquely named variable = {second_val}</h1>
<div data-djc-id-a1bc41>
<h1 data-djc-id-a1bc46>Shadowing variable = value_from_slot</h1>
<h1 data-djc-id-a1bc46>Uniquely named variable = {second_val}</h1>
</div>
""",
)
@ -331,7 +311,7 @@ class ContextCalledOnceTests(BaseTestCase):
rendered = template.render(Context()).strip().replace("\n", "")
self.assertHTMLEqual(
rendered,
'<p class="incrementer">value=1;calls=1</p>',
'<p class="incrementer" data-djc-id-a1bc3f>value=1;calls=1</p>',
)
@parametrize_context_behavior(["django", "isolated"])
@ -343,7 +323,12 @@ class ContextCalledOnceTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context()).strip()
self.assertHTMLEqual(rendered, '<p class="incrementer">value=3;calls=1</p>', rendered)
self.assertHTMLEqual(
rendered,
"""
<p class="incrementer" data-djc-id-a1bc3f>value=3;calls=1</p>
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_one_context_call_with_component(self):
@ -354,7 +339,7 @@ class ContextCalledOnceTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context()).strip()
self.assertHTMLEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered)
self.assertHTMLEqual(rendered, '<p class="incrementer" data-djc-id-a1bc3f>value=1;calls=1</p>')
@parametrize_context_behavior(["django", "isolated"])
def test_one_context_call_with_component_and_arg(self):
@ -365,7 +350,7 @@ class ContextCalledOnceTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context()).strip()
self.assertHTMLEqual(rendered, '<p class="incrementer">value=4;calls=1</p>', rendered)
self.assertHTMLEqual(rendered, '<p class="incrementer" data-djc-id-a1bc3f>value=4;calls=1</p>')
@parametrize_context_behavior(["django", "isolated"])
def test_one_context_call_with_slot(self):
@ -382,7 +367,10 @@ class ContextCalledOnceTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
'<p class="incrementer">value=1;calls=1</p>\n<p>slot</p>',
"""
<p class="incrementer" data-djc-id-a1bc40>value=1;calls=1</p>
<p data-djc-id-a1bc40>slot</p>
""",
rendered,
)
@ -401,7 +389,10 @@ class ContextCalledOnceTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
'<p class="incrementer">value=4;calls=1</p>\n<p>slot</p>',
"""
<p class="incrementer" data-djc-id-a1bc40>value=4;calls=1</p>
<p data-djc-id-a1bc40>slot</p>
""",
rendered,
)
@ -428,7 +419,7 @@ class ComponentsCanAccessOuterContext(BaseTestCase):
self.assertHTMLEqual(
rendered,
f"""
Variable: <strong> {context_behavior_data} </strong>
Variable: <strong data-djc-id-a1bc3f> {context_behavior_data} </strong>
""",
)
@ -613,7 +604,7 @@ class ContextVarsIsFilledTests(BaseTestCase):
rendered = Template(template).render(Context())
expected = """
<div class="frontmatter-component">
<div class="frontmatter-component" data-djc-id-a1bc42>
title: True
my_title: False
my_title_1: False
@ -635,7 +626,7 @@ class ContextVarsIsFilledTests(BaseTestCase):
"""
rendered = Template(template).render(Context())
expected = """
<div class="frontmatter-component">
<div class="frontmatter-component" data-djc-id-a1bc3f>
bla bla
title: False
my_title: False
@ -653,7 +644,7 @@ class ContextVarsIsFilledTests(BaseTestCase):
{% component "conditional_slots" %}{% endcomponent %}
"""
expected = """
<div class="frontmatter-component">
<div class="frontmatter-component" data-djc-id-a1bc3f>
<div class="title">
Title
</div>
@ -671,7 +662,7 @@ class ContextVarsIsFilledTests(BaseTestCase):
{% endcomponent %}
"""
expected = """
<div class="frontmatter-component">
<div class="frontmatter-component" data-djc-id-a1bc40>
<div class="title">
Title
</div>
@ -692,7 +683,7 @@ class ContextVarsIsFilledTests(BaseTestCase):
{% endcomponent %}
"""
expected = """
<div class="frontmatter-component">
<div class="frontmatter-component" data-djc-id-a1bc40>
<div class="title">
Title
</div>
@ -712,7 +703,7 @@ class ContextVarsIsFilledTests(BaseTestCase):
{% endcomponent %}
"""
expected = """
<div class="frontmatter-component">
<div class="frontmatter-component" data-djc-id-a1bc3f>
<div class="title">
Title
</div>
@ -746,7 +737,7 @@ class ContextVarsIsFilledTests(BaseTestCase):
{% endcomponent %}
"""
expected = """
<div class="frontmatter-component">
<div class="frontmatter-component" data-djc-id-a1bc3f>
<div class="title">
Title
</div>

View file

@ -1,3 +1,10 @@
"""
These tests check the public API side of managing dependencies - We check
if calling `Component.render()` or `render_dependencies()` behave as expected.
For checking the OUTPUT of the dependencies, see `test_dependency_rendering.py`.
"""
from unittest.mock import Mock
from django.http import HttpResponseNotModified
@ -54,7 +61,7 @@ class RenderDependenciesTests(BaseTestCase):
rendered_raw = template.render(Context({}))
# Placeholders
self.assertEqual(rendered_raw.count('<link name="CSS_PLACEHOLDER">'), 1)
self.assertEqual(rendered_raw.count('<link name="CSS_PLACEHOLDER"/>'), 1)
self.assertEqual(rendered_raw.count('<script name="JS_PLACEHOLDER"></script>'), 1)
self.assertEqual(rendered_raw.count("<script"), 1)
@ -258,7 +265,7 @@ class RenderDependenciesTests(BaseTestCase):
self.assertInHTML(
"""
<body>
Variable: <strong>foo</strong>
Variable: <strong data-djc-id-a1bc41>foo</strong>
<style>.xyz { color: red; }</style>
<link href="style.css" media="all" rel="stylesheet">
@ -415,7 +422,7 @@ class RenderDependenciesTests(BaseTestCase):
<td class="whitespace-nowrap w-fit text-center px-4 w-px"
aria-colindex="1">
1
Variable: <strong>hi</strong>
Variable: <strong data-djc-id-a1bc3f>hi</strong>
</td>
</tr>
</tbody>
@ -471,37 +478,59 @@ class MiddlewareTests(BaseTestCase):
request = Mock()
self.assertEqual(response, middleware(request=request))
def test_middleware_response_with_components_with_slash_dash_and_underscore(
self,
):
def test_middleware_response_with_components_with_slash_dash_and_underscore(self):
registry.register("dynamic", DynamicComponent)
registry.register("test-component", component=SimpleComponent)
registry.register("test/component", component=SimpleComponent)
registry.register("test_component", component=SimpleComponent)
component_names = [
"test-component",
"test/component",
"test_component",
]
for component_name in component_names:
registry.register(name=component_name, component=SimpleComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component_css_dependencies %}
{% component_js_dependencies %}
{% component "dynamic" is=component_name variable='value' / %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(
template, context=Context({"component_name": component_name})
)
template_str: types.django_html = """
{% load component_tags %}
{% component_css_dependencies %}
{% component_js_dependencies %}
{% component "dynamic" is=component_name variable='value' / %}
"""
template = Template(template_str)
def assert_dependencies(content: str):
# Dependency manager script (empty)
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', content, count=1)
# Inlined JS
self.assertInHTML('<script>console.log("xyz");</script>', rendered, count=1)
self.assertInHTML('<script>console.log("xyz");</script>', content, count=1)
# Inlined CSS
self.assertInHTML("<style>.xyz { color: red; }</style>", rendered, count=1)
self.assertInHTML("<style>.xyz { color: red; }</style>", content, count=1)
# Media.css
self.assertInHTML('<link href="style.css" media="all" rel="stylesheet">', rendered, count=1)
self.assertInHTML('<link href="style.css" media="all" rel="stylesheet">', content, count=1)
self.assertEqual(rendered.count("Variable: <strong>value</strong>"), 1)
rendered1 = create_and_process_template_response(
template,
context=Context({"component_name": "test-component"}),
)
assert_dependencies(rendered1)
self.assertEqual(
rendered1.count('Variable: <strong data-djc-id-a1bc41 data-djc-id-a1bc42="">value</strong>'),
1,
)
rendered2 = create_and_process_template_response(
template,
context=Context({"component_name": "test-component"}),
)
assert_dependencies(rendered2)
self.assertEqual(
rendered2.count('Variable: <strong data-djc-id-a1bc43 data-djc-id-a1bc44="">value</strong>'),
1,
)
rendered3 = create_and_process_template_response(
template,
context=Context({"component_name": "test_component"}),
)
assert_dependencies(rendered3)
self.assertEqual(
rendered3.count('Variable: <strong data-djc-id-a1bc45 data-djc-id-a1bc46="">value</strong>'),
1,
)

View file

@ -215,7 +215,7 @@ class CallComponentTests(_BaseDepManagerTestCase):
const inputHash = 'input-abc';
// Pretend that this HTML belongs to our component
document.body.insertAdjacentHTML('beforeend', '<div data-comp-id-12345> abc </div>');
document.body.insertAdjacentHTML('beforeend', '<div data-djc-id-12345> abc </div>');
let captured = null;
manager.registerComponent(compName, (data, ctx) => {
@ -248,7 +248,7 @@ class CallComponentTests(_BaseDepManagerTestCase):
"hello": "world",
},
"ctx": {
"els": ['<div data-comp-id-12345=""> abc </div>'],
"els": ['<div data-djc-id-12345=""> abc </div>'],
"id": "12345",
"name": "my_comp",
},
@ -269,7 +269,7 @@ class CallComponentTests(_BaseDepManagerTestCase):
const inputHash = 'input-abc';
// Pretend that this HTML belongs to our component
document.body.insertAdjacentHTML('beforeend', '<div data-comp-id-12345> abc </div>');
document.body.insertAdjacentHTML('beforeend', '<div data-djc-id-12345> abc </div>');
manager.registerComponent(compName, (data, ctx) => {
return Promise.resolve(123);
@ -309,7 +309,7 @@ class CallComponentTests(_BaseDepManagerTestCase):
const inputHash = 'input-abc';
// Pretend that this HTML belongs to our component
document.body.insertAdjacentHTML('beforeend', '<div data-comp-id-12345> abc </div>');
document.body.insertAdjacentHTML('beforeend', '<div data-djc-id-12345> abc </div>');
manager.registerComponent(compName, (data, ctx) => {
throw Error('Oops!');
@ -343,7 +343,7 @@ class CallComponentTests(_BaseDepManagerTestCase):
const inputHash = 'input-abc';
// Pretend that this HTML belongs to our component
document.body.insertAdjacentHTML('beforeend', '<div data-comp-id-12345> abc </div>');
document.body.insertAdjacentHTML('beforeend', '<div data-djc-id-12345> abc </div>');
manager.registerComponent(compName, async (data, ctx) => {
throw Error('Oops!');
@ -408,7 +408,7 @@ class CallComponentTests(_BaseDepManagerTestCase):
const compId = '12345';
const inputHash = 'input-abc';
document.body.insertAdjacentHTML('beforeend', '<div data-comp-id-12345> abc </div>');
document.body.insertAdjacentHTML('beforeend', '<div data-djc-id-12345> abc </div>');
manager.registerComponent(compName, (data, ctx) => {
return Promise.resolve(123);
@ -434,7 +434,7 @@ class CallComponentTests(_BaseDepManagerTestCase):
const compId = '12345';
const inputHash = 'input-abc';
document.body.insertAdjacentHTML('beforeend', '<div data-comp-id-12345> abc </div>');
document.body.insertAdjacentHTML('beforeend', '<div data-djc-id-12345> abc </div>');
manager.registerComponentData(compName, inputHash, () => {
return { hello: 'world' };

View file

@ -5,7 +5,7 @@ During actual rendering, the HTML is then picked up by the JS-side dependency ma
import re
from django.template import Template
from django.template import Context, Template
from django_components import Component, registry, types
@ -456,8 +456,8 @@ class DependencyRenderingTests(BaseTestCase):
# `c3R5bGUyLmNzcw==` -> `style2.css`
# `eHl6MS5jc3M=` -> `xyz1.css`
# `L2NvbXBvbmVudHMvY2FjaGUvT3RoZXJDb21wb25lbnRfNjMyOWFlLmNzcw==` -> `/components/cache/OtherComponent_6329ae.css`
# `L2NvbXBvbmVudHMvY2FjaGUvU2ltcGxlQ29tcG9uZW50TmVzdGVkX2YwMmQzMi5jc3M=` -> `/components/cache/SimpleComponentNested_f02d32.css`
# `L2NvbXBvbmVudHMvY2FjaGUvT3RoZXJDb21wb25lbnRfNjMyOWFlLmpz` -> `/components/cache/OtherComponent_6329ae.js`
# `L2NvbXBvbmVudHMvY2FjaGUvU2ltcGxlQ29tcG9uZW50TmVzdGVkX2YwMmQzMi5jc3M=` -> `/components/cache/SimpleComponentNested_f02d32.css`
# `L2NvbXBvbmVudHMvY2FjaGUvU2ltcGxlQ29tcG9uZW50TmVzdGVkX2YwMmQzMi5qcw==` -> `/components/cache/SimpleComponentNested_f02d32.js`
# `c2NyaXB0Lmpz` -> `script.js`
# `c2NyaXB0Mi5qcw==` -> `script2.js`
@ -465,8 +465,16 @@ class DependencyRenderingTests(BaseTestCase):
self.assertInHTML(
"""
<script type="application/json" data-djc>
{"loadedCssUrls": ["L2NvbXBvbmVudHMvY2FjaGUvT3RoZXJDb21wb25lbnRfNjMyOWFlLmNzcw==", "L2NvbXBvbmVudHMvY2FjaGUvU2ltcGxlQ29tcG9uZW50TmVzdGVkX2YwMmQzMi5jc3M=", "c3R5bGUuY3Nz", "c3R5bGUyLmNzcw==", "eHl6MS5jc3M="],
"loadedJsUrls": ["L2NvbXBvbmVudHMvY2FjaGUvT3RoZXJDb21wb25lbnRfNjMyOWFlLmpz", "L2NvbXBvbmVudHMvY2FjaGUvU2ltcGxlQ29tcG9uZW50TmVzdGVkX2YwMmQzMi5qcw==", "c2NyaXB0Lmpz", "c2NyaXB0Mi5qcw==", "eHl6MS5qcw=="],
{"loadedCssUrls": ["L2NvbXBvbmVudHMvY2FjaGUvT3RoZXJDb21wb25lbnRfNjMyOWFlLmNzcw==",
"L2NvbXBvbmVudHMvY2FjaGUvU2ltcGxlQ29tcG9uZW50TmVzdGVkX2YwMmQzMi5jc3M=",
"c3R5bGUuY3Nz",
"c3R5bGUyLmNzcw==",
"eHl6MS5jc3M="],
"loadedJsUrls": ["L2NvbXBvbmVudHMvY2FjaGUvT3RoZXJDb21wb25lbnRfNjMyOWFlLmpz",
"L2NvbXBvbmVudHMvY2FjaGUvU2ltcGxlQ29tcG9uZW50TmVzdGVkX2YwMmQzMi5qcw==",
"c2NyaXB0Lmpz",
"c2NyaXB0Mi5qcw==",
"eHl6MS5qcw=="],
"toLoadCssTags": [],
"toLoadJsTags": []}
</script>
@ -491,3 +499,131 @@ class DependencyRenderingTests(BaseTestCase):
template = Template(template_str)
rendered = create_and_process_template_response(template)
self.assertNotIn("_RENDERED", rendered)
def test_adds_component_id_html_attr_single(self):
registry.register(name="test", component=SimpleComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'test' variable='foo' / %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template)
self.assertHTMLEqual(rendered, "Variable: <strong data-djc-id-a1bc3f>foo</strong>")
def test_adds_component_id_html_attr_single_multiroot(self):
class SimpleMultiroot(SimpleComponent):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
Variable2: <div>{{ variable }}</div>
Variable3: <span>{{ variable }}</span>
"""
registry.register(name="test", component=SimpleMultiroot)
template_str: types.django_html = """
{% load component_tags %}
{% component 'test' variable='foo' / %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template)
self.assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-a1bc3f>foo</strong>
Variable2: <div data-djc-id-a1bc3f>foo</div>
Variable3: <span data-djc-id-a1bc3f>foo</span>
""",
)
# Test that, if multiple components share the same root HTML elements,
# then those elemens will have the `data-djc-id-` attribute added for each component.
def test_adds_component_id_html_attr_nested(self):
class SimpleMultiroot(SimpleComponent):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
Variable2: <div>{{ variable }}</div>
Variable3: <span>{{ variable }}</span>
"""
class SimpleOuter(SimpleComponent):
template: types.django_html = """
{% load component_tags %}
{% component 'multiroot' variable='foo' / %}
<div>Another</div>
"""
registry.register(name="multiroot", component=SimpleMultiroot)
registry.register(name="outer", component=SimpleOuter)
template_str: types.django_html = """
{% load component_tags %}
{% component 'outer' variable='foo' / %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template)
self.assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc41>foo</strong>
Variable2: <div data-djc-id-a1bc3f data-djc-id-a1bc41>foo</div>
Variable3: <span data-djc-id-a1bc3f data-djc-id-a1bc41>foo</span>
<div data-djc-id-a1bc3f>Another</div>
""",
)
# `data-djc-id-` attribute should be added on each instance in the RESULTING HTML.
# So if in a loop, each iteration creates a new component, and each of those should
# have a unique `data-djc-id-` attribute.
def test_adds_component_id_html_attr_loops(self):
class SimpleMultiroot(SimpleComponent):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
Variable2: <div>{{ variable }}</div>
Variable3: <span>{{ variable }}</span>
"""
class SimpleOuter(SimpleComponent):
template: types.django_html = """
{% load component_tags %}
{% component 'multiroot' variable='foo' / %}
<div>Another</div>
"""
registry.register(name="multiroot", component=SimpleMultiroot)
registry.register(name="outer", component=SimpleOuter)
template_str: types.django_html = """
{% load component_tags %}
{% for i in lst %}
{% component 'outer' variable='foo' / %}
{% endfor %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(
template,
context=Context({"lst": range(3)}),
)
self.assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc41>foo</strong>
Variable2: <div data-djc-id-a1bc3f data-djc-id-a1bc41>foo</div>
Variable3: <span data-djc-id-a1bc3f data-djc-id-a1bc41>foo</span>
<div data-djc-id-a1bc3f>Another</div>
Variable: <strong data-djc-id-a1bc42 data-djc-id-a1bc43>foo</strong>
Variable2: <div data-djc-id-a1bc42 data-djc-id-a1bc43>foo</div>
Variable3: <span data-djc-id-a1bc42 data-djc-id-a1bc43>foo</span>
<div data-djc-id-a1bc42>Another</div>
Variable: <strong data-djc-id-a1bc44 data-djc-id-a1bc45>foo</strong>
Variable2: <div data-djc-id-a1bc44 data-djc-id-a1bc45>foo</div>
Variable3: <span data-djc-id-a1bc44 data-djc-id-a1bc45>foo</span>
<div data-djc-id-a1bc44>Another</div>
""",
)

View file

@ -3,6 +3,8 @@ Here we check that all parts of managing JS and CSS dependencies work together
in an actual browser.
"""
import re
from playwright.async_api import Page
from django_components import types
@ -44,7 +46,10 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js)
# Check that the actual HTML content was loaded
self.assertIn('Variable: <strong class="inner">foo</strong>', data["bodyHTML"])
self.assertRegex(
data["bodyHTML"],
re.compile(r'Variable: <strong class="inner" data-djc-id-\w{6}="">foo</strong>'),
)
self.assertInHTML('<div class="my-style"> 123 </div>', data["bodyHTML"], count=1)
self.assertInHTML('<div class="my-style2"> xyz </div>', data["bodyHTML"], count=1)
@ -106,17 +111,34 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js)
# Check that the actual HTML content was loaded
self.assertInHTML(
"""
<div class="outer">
Variable: <strong class="inner">variable</strong>
XYZ: <strong class="other">variable_inner</strong>
</div>
<div class="my-style">123</div>
<div class="my-style2">xyz</div>
""",
self.assertRegex(
data["bodyHTML"],
count=1,
# <div class="outer" data-djc-id-10uLMD>
# Variable:
# <strong class="inner" data-djc-id-DZEnUC>
# variable
# </strong>
# XYZ:
# <strong class="other" data-djc-id-IYirHK>
# variable_inner
# </strong>
# </div>
# <div class="my-style">123</div>
# <div class="my-style2">xyz</div>
re.compile(
r'<div class="outer" data-djc-id-\w{6}="">\s*'
r"Variable:\s*"
r'<strong class="inner" data-djc-id-\w{6}="">\s*'
r"variable\s*"
r"<\/strong>\s*"
r"XYZ:\s*"
r'<strong class="other" data-djc-id-\w{6}="">\s*'
r"variable_inner\s*"
r"<\/strong>\s*"
r"<\/div>\s*"
r'<div class="my-style">123<\/div>\s*'
r'<div class="my-style2">xyz<\/div>\s*'
),
)
# Check components' inlined JS got loaded
@ -183,17 +205,34 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js)
# Check that the actual HTML content was loaded
self.assertInHTML(
"""
<div class="outer">
Variable: <strong class="inner">variable</strong>
XYZ: <strong class="other">variable_inner</strong>
</div>
<div class="my-style">123</div>
<div class="my-style2">xyz</div>
""",
self.assertRegex(
data["bodyHTML"],
count=1,
# <div class="outer" data-djc-id-10uLMD>
# Variable:
# <strong class="inner" data-djc-id-DZEnUC>
# variable
# </strong>
# XYZ:
# <strong data-djc-id-IYirHK class="other">
# variable_inner
# </strong>
# </div>
# <div class="my-style">123</div>
# <div class="my-style2">xyz</div>
re.compile(
r'<div class="outer" data-djc-id-\w{6}="">\s*'
r"Variable:\s*"
r'<strong class="inner" data-djc-id-\w{6}="">\s*'
r"variable\s*"
r"<\/strong>\s*"
r"XYZ:\s*"
r'<strong class="other" data-djc-id-\w{6}="">\s*'
r"variable_inner\s*"
r"<\/strong>\s*"
r"<\/div>\s*"
r'<div class="my-style">123<\/div>\s*'
r'<div class="my-style2">xyz<\/div>\s*'
),
)
# Check components' inlined JS did NOT get loaded
@ -342,7 +381,12 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js)
self.assertEqual(data["targetHtml"], None)
self.assertHTMLEqual('<div class="frag"> 123 <span id="frag-text">xxx</span></div>', data["fragHtml"])
self.assertRegex(
data["fragHtml"],
re.compile(
r'<div class="frag" data-djc-id-\w{6}="">\s*' r"123\s*" r'<span id="frag-text">xxx</span>\s*' r"</div>"
),
)
self.assertIn("rgb(0, 0, 255)", data["fragBg"]) # AKA 'background: blue'
await page.close()
@ -390,7 +434,12 @@ class E2eDependencyRenderingTests(BaseTestCase):
data = await page.evaluate(test_js)
self.assertEqual(data["targetHtml"], None)
self.assertHTMLEqual('<div class="frag"> 123 <span id="frag-text">xxx</span></div>', data["fragHtml"])
self.assertRegex(
data["fragHtml"],
re.compile(
r'<div class="frag" data-djc-id-\w{6}="">\s*' r"123\s*" r'<span id="frag-text">xxx</span>\s*' r"</div>"
),
)
self.assertIn("rgb(0, 0, 255)", data["fragBg"]) # AKA 'background: blue'
await page.close()
@ -441,11 +490,12 @@ class E2eDependencyRenderingTests(BaseTestCase):
# NOTE: Unlike the vanilla JS tests, for the Alpine test we don't remove the targetHtml,
# but only change its contents.
self.assertInHTML(
'<div class="frag"> 123 <span id="frag-text">xxx</span></div>',
self.assertRegex(
data["targetHtml"],
re.compile(
r'<div class="frag" data-djc-id-\w{6}="">\s*' r"123\s*" r'<span id="frag-text">xxx</span>\s*' r"</div>"
),
)
self.assertHTMLEqual(data["fragHtml"], '<div class="frag"> 123 <span id="frag-text">xxx</span></div>')
self.assertIn("rgb(0, 0, 255)", data["fragBg"]) # AKA 'background: blue'
await page.close()
@ -497,9 +547,9 @@ class E2eDependencyRenderingTests(BaseTestCase):
self.assertEqual(data["targetHtml"], None)
# NOTE: We test only the inner HTML, because the element itself may or may not have
# extra CSS classes added by HTMX, which results in flaky tests.
self.assertHTMLEqual(
self.assertRegex(
data["fragInnerHtml"],
'123 <span id="frag-text">xxx</span>',
re.compile(r'123\s*<span id="frag-text">xxx</span>'),
)
self.assertIn("rgb(0, 0, 255)", data["fragBg"]) # AKA 'background: blue'

View file

@ -150,7 +150,7 @@ class DynamicExprTests(BaseTestCase):
self.assertEqual(
rendered.strip(),
"<!-- _RENDERED SimpleComponent_5b8d97,a1bc3e -->\n <div>lorem</div>\n <div>True</div>\n <div>[{'a': 1}, {'a': 2}]</div>", # noqa: E501
"<!-- _RENDERED SimpleComponent_5b8d97,a1bc3f,, -->\n<div data-djc-id-a1bc3f>lorem</div>\n<div data-djc-id-a1bc3f>True</div>\n<div data-djc-id-a1bc3f>[{'a': 1}, {'a': 2}]</div>", # noqa: E501
)
@parametrize_context_behavior(["django", "isolated"])
@ -220,7 +220,13 @@ class DynamicExprTests(BaseTestCase):
self.assertEqual(
rendered.strip(),
"<!-- _RENDERED SimpleComponent_743413,a1bc3e -->\n <div>lorem ipsum dolor</div>\n <div>True</div>\n <div>[{'a': 1}, {'a': 2}]</div>\n <div>{'a': 3}</div>", # noqa E501
(
"<!-- _RENDERED SimpleComponent_743413,a1bc3f,, -->\n"
"<div data-djc-id-a1bc3f>lorem ipsum dolor</div>\n"
"<div data-djc-id-a1bc3f>True</div>\n"
"<div data-djc-id-a1bc3f>[{'a': 1}, {'a': 2}]</div>\n"
"<div data-djc-id-a1bc3f>{'a': 3}</div>"
),
)
@parametrize_context_behavior(["django", "isolated"])
@ -290,7 +296,13 @@ class DynamicExprTests(BaseTestCase):
self.assertEqual(
rendered.strip(),
"<!-- _RENDERED SimpleComponent_e258c0,a1bc3e -->\n <div></div>\n <div> abc</div>\n <div></div>\n <div> </div>", # noqa E501
(
"<!-- _RENDERED SimpleComponent_e258c0,a1bc3f,, -->\n"
"<div data-djc-id-a1bc3f></div>\n"
"<div data-djc-id-a1bc3f> abc</div>\n"
"<div data-djc-id-a1bc3f></div>\n"
"<div data-djc-id-a1bc3f> </div>"
),
)
@parametrize_context_behavior(["django", "isolated"])
@ -364,7 +376,14 @@ class DynamicExprTests(BaseTestCase):
self.assertEqual(
rendered.strip(),
"<!-- _RENDERED SimpleComponent_6c8e94,a1bc3e -->\n <div> lorem ipsum dolor </div>\n <div> lorem ipsum dolor [{&#x27;a&#x27;: 1}] </div>\n <div> True </div>\n <div> [{'a': 1}, {'a': 2}] </div>\n <div> {'a': 3} </div>", # noqa E501
(
"<!-- _RENDERED SimpleComponent_6c8e94,a1bc3f,, -->\n"
"<div data-djc-id-a1bc3f> lorem ipsum dolor </div>\n"
"<div data-djc-id-a1bc3f> lorem ipsum dolor [{'a': 1}] </div>\n"
"<div data-djc-id-a1bc3f> True </div>\n"
"<div data-djc-id-a1bc3f> [{'a': 1}, {'a': 2}] </div>\n"
"<div data-djc-id-a1bc3f> {'a': 3} </div>"
),
)
@parametrize_context_behavior(["django", "isolated"])
@ -408,7 +427,12 @@ class DynamicExprTests(BaseTestCase):
self.assertEqual(
rendered.strip(),
'<!-- _RENDERED SimpleComponent_c7a5c3,a1bc3e -->\n <div>"</div>\n <div>{%}</div>\n <div>True</div>', # noqa: E501
(
"<!-- _RENDERED SimpleComponent_c7a5c3,a1bc3f,, -->\n"
'<div data-djc-id-a1bc3f>"</div>\n'
"<div data-djc-id-a1bc3f>{%}</div>\n"
"<div data-djc-id-a1bc3f>True</div>"
),
)
@parametrize_context_behavior(["django", "isolated"])
@ -457,7 +481,14 @@ class DynamicExprTests(BaseTestCase):
self.assertEqual(
rendered.strip(),
"<!-- _RENDERED SimpleComponent_5c8766,a1bc3e -->\n <div><!-- _RENDERED SimpleComponent_5c8766,a1bc3f -->\n <div>3</div>\n <div>True</div>\n </div>\n <div>True</div>", # noqa E501
(
"<!-- _RENDERED SimpleComponent_5c8766,a1bc41,, -->\n"
"<div data-djc-id-a1bc41><!-- _RENDERED SimpleComponent_5c8766,a1bc40,, -->\n"
'<div data-djc-id-a1bc40="">3</div>\n'
'<div data-djc-id-a1bc40="">True</div>\n'
"</div>\n"
"<div data-djc-id-a1bc41>True</div>"
),
)
@ -528,11 +559,11 @@ class SpreadOperatorTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div>LoREM</div>
<div>{'@click': '() =&gt; {}', 'style': 'height: 20px'}</div>
<div>[1, 2, 3]</div>
<div>1</div>
<div>123</div>
<div data-djc-id-a1bc3f>LoREM</div>
<div data-djc-id-a1bc3f>{'@click': '() =&gt; {}', 'style': 'height: 20px'}</div>
<div data-djc-id-a1bc3f>[1, 2, 3]</div>
<div data-djc-id-a1bc3f>1</div>
<div data-djc-id-a1bc3f>123</div>
""",
)
@ -665,9 +696,9 @@ class SpreadOperatorTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div>{'@click': '() =&gt; {}', 'style': 'height: 20px'}</div>
<div>[1, 2, 3]</div>
<div>1</div>
<div data-djc-id-a1bc40>{'@click': '() =&gt; {}', 'style': 'height: 20px'}</div>
<div data-djc-id-a1bc40>[1, 2, 3]</div>
<div data-djc-id-a1bc40>1</div>
""",
)
@ -748,10 +779,10 @@ class SpreadOperatorTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div>{'@click': '() =&gt; {}', 'style': 'OVERWRITTEN'}</div>
<div>[1, 2, 3]</div>
<div>1</div>
<div>OVERWRITTEN_X</div>
<div data-djc-id-a1bc3f>{'@click': '() =&gt; {}', 'style': 'OVERWRITTEN'}</div>
<div data-djc-id-a1bc3f>[1, 2, 3]</div>
<div data-djc-id-a1bc3f>1</div>
<div data-djc-id-a1bc3f>OVERWRITTEN_X</div>
""",
)

View file

@ -203,11 +203,11 @@ class MultipleComponentRegistriesTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
Variable: <strong>123</strong>
Variable: <strong data-djc-id-a1bc40>123</strong>
Slot:
SLOT 123
Variable: <strong>123</strong>
Variable: <strong data-djc-id-a1bc42>123</strong>
Slot:
SLOT ABC
""",

View file

@ -68,7 +68,7 @@ class ComponentTagTests(BaseTestCase):
rendered,
"""
hello1
<div>
<div data-djc-id-a1bc3f>
SLOT_DEFAULT
</div>
hello2
@ -101,7 +101,7 @@ class ComponentTagTests(BaseTestCase):
rendered,
"""
hello1
<div>
<div data-djc-id-a1bc3f>
OVERRIDEN!
</div>
hello2
@ -139,7 +139,7 @@ class ComponentTagTests(BaseTestCase):
rendered,
"""
hello1
<div>
<div data-djc-id-a1bc3f>
SLOT_DEFAULT
</div>
hello2
@ -179,7 +179,7 @@ class ComponentTagTests(BaseTestCase):
rendered,
"""
hello1
<div>
<div data-djc-id-a1bc3f>
OVERRIDEN!
</div>
hello2
@ -217,7 +217,7 @@ class ComponentTagTests(BaseTestCase):
rendered,
"""
hello1
<div>
<div data-djc-id-a1bc3f>
SLOT_DEFAULT
</div>
hello2
@ -257,7 +257,7 @@ class ComponentTagTests(BaseTestCase):
rendered,
"""
hello1
<div>
<div data-djc-id-a1bc3f>
OVERRIDEN!
</div>
hello2
@ -297,7 +297,7 @@ class ComponentTagTests(BaseTestCase):
rendered,
"""
hello1
<div>
<div data-djc-id-a1bc3f>
OVERRIDEN!
</div>
hello2
@ -334,7 +334,7 @@ class ComponentTagTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div>
<div data-djc-id-a1bc3f>
OVERRIDEN!
</div>
""",
@ -419,7 +419,7 @@ class ComponentTagTests(BaseTestCase):
rendered,
"""
hello1
<div>
<div data-djc-id-a1bc3f>
SLOT_DEFAULT
</div>
hello2
@ -439,7 +439,7 @@ class ComponentTagTests(BaseTestCase):
rendered,
"""
hello1
<div>
<div data-djc-id-a1bc42>
OVERRIDEN!
</div>
hello2

View file

@ -120,7 +120,7 @@ class MultilineTagsTests(BaseTestCase):
"""
rendered = Template(template).render(Context())
expected = """
Variable: <strong>123</strong>
Variable: <strong data-djc-id-a1bc3f>123</strong>
"""
self.assertHTMLEqual(rendered, expected)
@ -147,7 +147,7 @@ class NestedTagsTests(BaseTestCase):
"""
rendered = Template(template).render(Context())
expected = """
Variable: <strong>lorem</strong>
Variable: <strong data-djc-id-a1bc3f>lorem</strong>
"""
self.assertHTMLEqual(rendered, expected)
@ -161,7 +161,7 @@ class NestedTagsTests(BaseTestCase):
"""
rendered = Template(template).render(Context())
expected = """
Variable: <strong>organisation&#x27;s</strong>
Variable: <strong data-djc-id-a1bc3f>organisation&#x27;s</strong>
"""
self.assertHTMLEqual(rendered, expected)
@ -175,7 +175,7 @@ class NestedTagsTests(BaseTestCase):
"""
rendered = Template(template).render(Context())
expected = """
Variable: <strong>organisation&#x27;s</strong>
Variable: <strong data-djc-id-a1bc3f>organisation&#x27;s</strong>
"""
self.assertHTMLEqual(rendered, expected)
@ -189,7 +189,7 @@ class NestedTagsTests(BaseTestCase):
"""
rendered = Template(template).render(Context())
expected = """
Variable: <strong>organisation"s</strong>
Variable: <strong data-djc-id-a1bc3f>organisation"s</strong>
"""
self.assertHTMLEqual(rendered, expected)
@ -203,6 +203,6 @@ class NestedTagsTests(BaseTestCase):
"""
rendered = Template(template).render(Context())
expected = """
Variable: <strong>organisation"s</strong>
Variable: <strong data-djc-id-a1bc3f>organisation"s</strong>
"""
self.assertHTMLEqual(rendered, expected)

View file

@ -1,5 +1,3 @@
import textwrap
from django.template import Context, Template, TemplateSyntaxError
from django_components import AlreadyRegistered, Component, NotRegistered, register, registry, types
@ -60,7 +58,7 @@ class ComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
self.assertHTMLEqual(rendered, "Variable: <strong data-djc-id-a1bc3f>variable</strong>\n")
@parametrize_context_behavior(["django", "isolated"])
def test_single_component_self_closing(self):
@ -73,7 +71,7 @@ class ComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
self.assertHTMLEqual(rendered, "Variable: <strong data-djc-id-a1bc3f>variable</strong>\n")
@parametrize_context_behavior(["django", "isolated"])
def test_call_with_invalid_name(self):
@ -99,7 +97,7 @@ class ComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
self.assertHTMLEqual(rendered, "Variable: <strong data-djc-id-a1bc3f>variable</strong>\n")
@parametrize_context_behavior(["django", "isolated"])
def test_call_component_with_two_variables(self):
@ -129,8 +127,13 @@ class ComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
rendered = template.render(Context({}))
expected_outcome = """Variable: <strong>variable</strong>\n""" """Variable2: <strong>hej</strong>"""
self.assertHTMLEqual(rendered, textwrap.dedent(expected_outcome))
self.assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-a1bc3f>variable</strong>
Variable2: <strong data-djc-id-a1bc3f>hej</strong>
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_component_called_with_singlequoted_name(self):
@ -143,7 +146,7 @@ class ComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
self.assertHTMLEqual(rendered, "Variable: <strong data-djc-id-a1bc3f>variable</strong>\n")
@parametrize_context_behavior(["django", "isolated"])
def test_raises_on_component_called_with_variable_as_name(self):
@ -183,7 +186,10 @@ class ComponentTemplateTagTest(BaseTestCase):
rendered = template.render(Context({}))
self.assertHTMLEqual(
rendered,
"Provided variable: <strong>provided value</strong>\nDefault: <p>default text</p>",
"""
Provided variable: <strong data-djc-id-a1bc3f>provided value</strong>
Default: <p data-djc-id-a1bc3f>default text</p>
""",
)
@ -222,7 +228,10 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
self.assertHTMLEqual(
rendered,
"Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>",
)
@parametrize_context_behavior(["django", "isolated"])
def test_call_with_invalid_name(self):
@ -250,7 +259,10 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
self.assertHTMLEqual(
rendered,
"Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>",
)
@parametrize_context_behavior(["django", "isolated"])
def test_component_called_with_variable_as_spread(self):
@ -272,7 +284,10 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
}
)
)
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
self.assertHTMLEqual(
rendered,
"Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>",
)
@parametrize_context_behavior(["django", "isolated"])
def test_component_as_class(self):
@ -291,7 +306,10 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
}
)
)
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
self.assertHTMLEqual(
rendered,
"Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>",
)
@parametrize_context_behavior(
["django", "isolated"],
@ -316,7 +334,7 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
self.assertHTMLEqual(rendered, "Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>\n")
@parametrize_context_behavior(
["django", "isolated"],
@ -342,7 +360,10 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
template = Template(simple_tag_template)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
self.assertHTMLEqual(
rendered,
"Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>",
)
@parametrize_context_behavior(["django", "isolated"])
def test_raises_already_registered_on_name_conflict(self):
@ -380,7 +401,7 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
Variable: <strong>variable</strong>
Variable: <strong data-djc-id-a1bc3f data-djc-id-a1bc40>variable</strong>
Slot: HELLO_FROM_SLOT
""",
)
@ -422,7 +443,7 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
Variable: <strong>variable</strong>
Variable: <strong data-djc-id-a1bc41 data-djc-id-a1bc42>variable</strong>
Slot 1: HELLO_FROM_SLOT_1
Slot 2: HELLO_FROM_SLOT_2
""",
@ -465,7 +486,7 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
Variable: <strong>variable</strong>
Variable: <strong data-djc-id-a1bc41 data-djc-id-a1bc42>variable</strong>
Slot 1: HELLO_FROM_SLOT_1
Slot 2:
""",
@ -488,75 +509,149 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
class MultiComponentTests(BaseTestCase):
def register_components(self):
@parametrize_context_behavior(["django", "isolated"])
def test_both_components_render_correctly_with_no_slots(self):
registry.register("first_component", SlottedComponent)
registry.register("second_component", SlottedComponentWithContext)
def make_template(self, first_slot: str = "", second_slot: str = "") -> Template:
template_str: types.django_html = f"""
{{% load component_tags %}}
{{% component 'first_component' %}}
{first_slot}
{{% endcomponent %}}
{{% component 'second_component' variable='xyz' %}}
{second_slot}
{{% endcomponent %}}
template_str: types.django_html = """
{% load component_tags %}
{% component 'first_component' %}
{% endcomponent %}
{% component 'second_component' variable='xyz' %}
{% endcomponent %}
"""
return Template(template_str)
template = Template(template_str)
rendered = template.render(Context())
def expected_result(self, first_slot: str = "", second_slot: str = "") -> str:
first_slot = first_slot or "Default header"
second_slot = second_slot or "Default header"
return f"""
<custom-template>
<header>{first_slot}</header>
self.assertHTMLEqual(
rendered,
"""
<custom-template data-djc-id-a1bc40>
<header>
Default header
</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
<custom-template>
<header>{second_slot}</header>
<custom-template data-djc-id-a1bc44>
<header>
Default header
</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
"""
def wrap_with_slot_tags(self, s):
return '{% fill "header" %}' + s + "{% endfill %}"
@parametrize_context_behavior(["django", "isolated"])
def test_both_components_render_correctly_with_no_slots(self):
self.register_components()
rendered = self.make_template().render(Context({}))
self.assertHTMLEqual(rendered, self.expected_result())
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_both_components_render_correctly_with_slots(self):
self.register_components()
first_slot_content = "<p>Slot #1</p>"
second_slot_content = "<div>Slot #2</div>"
first_slot = self.wrap_with_slot_tags(first_slot_content)
second_slot = self.wrap_with_slot_tags(second_slot_content)
rendered = self.make_template(first_slot, second_slot).render(Context({}))
registry.register("first_component", SlottedComponent)
registry.register("second_component", SlottedComponentWithContext)
template_str: types.django_html = """
{% load component_tags %}
{% component 'first_component' %}
{% fill "header" %}<p>Slot #1</p>{% endfill %}
{% endcomponent %}
{% component 'second_component' variable='xyz' %}
{% fill "header" %}<div>Slot #2</div>{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context())
self.assertHTMLEqual(
rendered,
self.expected_result(first_slot_content, second_slot_content),
"""
<custom-template data-djc-id-a1bc42>
<header>
<p>Slot #1</p>
</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
<custom-template data-djc-id-a1bc46>
<header>
<div>Slot #2</div>
</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_both_components_render_correctly_when_only_first_has_slots(self):
self.register_components()
first_slot_content = "<p>Slot #1</p>"
first_slot = self.wrap_with_slot_tags(first_slot_content)
rendered = self.make_template(first_slot).render(Context({}))
self.assertHTMLEqual(rendered, self.expected_result(first_slot_content))
registry.register("first_component", SlottedComponent)
registry.register("second_component", SlottedComponentWithContext)
template_str: types.django_html = """
{% load component_tags %}
{% component 'first_component' %}
{% fill "header" %}<p>Slot #1</p>{% endfill %}
{% endcomponent %}
{% component 'second_component' variable='xyz' %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
rendered,
"""
<custom-template data-djc-id-a1bc41>
<header>
<p>Slot #1</p>
</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
<custom-template data-djc-id-a1bc45>
<header>
Default header
</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_both_components_render_correctly_when_only_second_has_slots(self):
self.register_components()
second_slot_content = "<div>Slot #2</div>"
second_slot = self.wrap_with_slot_tags(second_slot_content)
rendered = self.make_template("", second_slot).render(Context({}))
self.assertHTMLEqual(rendered, self.expected_result("", second_slot_content))
registry.register("first_component", SlottedComponent)
registry.register("second_component", SlottedComponentWithContext)
template_str: types.django_html = """
{% load component_tags %}
{% component 'first_component' %}
{% endcomponent %}
{% component 'second_component' variable='xyz' %}
{% fill "header" %}<div>Slot #2</div>{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
rendered,
"""
<custom-template data-djc-id-a1bc41>
<header>
Default header
</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
<custom-template data-djc-id-a1bc45>
<header>
<div>Slot #2</div>
</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
""",
)
class ComponentIsolationTests(BaseTestCase):
@ -596,17 +691,17 @@ class ComponentIsolationTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<custom-template data-djc-id-a1bc4a>
<header>Override header</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
<custom-template>
<custom-template data-djc-id-a1bc4b>
<header>Default header</header>
<main>Override main</main>
<footer>Default footer</footer>
</custom-template>
<custom-template>
<custom-template data-djc-id-a1bc4c>
<header>Default header</header>
<main>Default main</main>
<footer>Override footer</footer>
@ -641,7 +736,7 @@ class AggregateInputTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div>
<div data-djc-id-a1bc3f>
attrs: {'@click.stop': "dispatch('click_event')", 'x-data': "{hello: 'world'}", 'class': 'padding-top-8'}
my_dict: {'one': 2}
</div>

View file

@ -49,14 +49,15 @@ class ExtendsCompatTests(BaseTestCase):
{% endblock %}
"""
rendered = Template(template).render(Context())
expected = """
<!DOCTYPE html>
<html lang="en">
<body>
<main role="main">
<div class='container main-container'>
<div>BLOCK OVERRIDEN</div>
<custom-template>
<div data-djc-id-a1bc40>BLOCK OVERRIDEN</div>
<custom-template data-djc-id-a1bc40>
<header>SLOT OVERRIDEN</header>
<main>Default main</main>
<footer>Default footer</footer>
@ -105,14 +106,14 @@ class ExtendsCompatTests(BaseTestCase):
<body>
<main role="main">
<div class='container main-container'>
<div>BLOCK OVERRIDEN</div>
<custom-template>
<div data-djc-id-a1bc42>BLOCK OVERRIDEN</div>
<custom-template data-djc-id-a1bc42>
<header>SLOT OVERRIDEN</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
<div>BLOCK OVERRIDEN</div>
<custom-template>
<div data-djc-id-a1bc46>BLOCK OVERRIDEN</div>
<custom-template data-djc-id-a1bc46>
<header>SLOT OVERRIDEN 2</header>
<main>Default main</main>
<footer>Default footer</footer>
@ -171,14 +172,14 @@ class ExtendsCompatTests(BaseTestCase):
<body>
<main role="main">
<div class='container main-container'>
<div>BLOCK OVERRIDEN</div>
<custom-template>
<div data-djc-id-a1bc42>BLOCK OVERRIDEN</div>
<custom-template data-djc-id-a1bc42>
<header>SLOT OVERRIDEN</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
<div>BLOCK OVERRIDEN</div>
<custom-template>
<div data-djc-id-a1bc46>BLOCK OVERRIDEN</div>
<custom-template data-djc-id-a1bc46>
<header>SLOT OVERRIDEN 2</header>
<main>Default main</main>
<footer>Default footer</footer>
@ -230,20 +231,21 @@ class ExtendsCompatTests(BaseTestCase):
{% endblock %}
"""
rendered = Template(template).render(Context())
expected = """
<!DOCTYPE html>
<html lang="en">
<body>
<main role="main">
<div class='container main-container'>
<div>BLOCK OVERRIDEN</div>
<custom-template>
<div data-djc-id-a1bc42>BLOCK OVERRIDEN</div>
<custom-template data-djc-id-a1bc42>
<header>SLOT OVERRIDEN</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
<div>BLOCK OVERRIDEN</div>
<custom-template>
<div data-djc-id-a1bc46>BLOCK OVERRIDEN</div>
<custom-template data-djc-id-a1bc46>
<header>SLOT OVERRIDEN 2</header>
<main>Default main</main>
<footer>Default footer</footer>
@ -287,8 +289,8 @@ class ExtendsCompatTests(BaseTestCase):
<!DOCTYPE html>
<html lang="en">
<body>
<div>BLOCK OVERRIDEN</div>
<custom-template>
<div data-djc-id-a1bc40>BLOCK OVERRIDEN</div>
<custom-template data-djc-id-a1bc40>
<header>SLOT OVERRIDEN</header>
<main>Default main</main>
<footer>Default footer</footer>
@ -335,14 +337,14 @@ class ExtendsCompatTests(BaseTestCase):
<!DOCTYPE html>
<html lang="en">
<body>
<div>BLOCK OVERRIDEN</div>
<custom-template>
<div data-djc-id-a1bc42>BLOCK OVERRIDEN</div>
<custom-template data-djc-id-a1bc42>
<header>SLOT OVERRIDEN</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
<div>BLOCK OVERRIDEN</div>
<custom-template>
<div data-djc-id-a1bc46>BLOCK OVERRIDEN</div>
<custom-template data-djc-id-a1bc46>
<header>SLOT OVERRIDEN 2</header>
<main>Default main</main>
<footer>Default footer</footer>
@ -389,11 +391,11 @@ class ExtendsCompatTests(BaseTestCase):
<body>
<main role="main">
<div class='container main-container'>
<custom-template>
<custom-template data-djc-id-a1bc42>
<header>Default header</header>
<main>
<div>BLOCK OVERRIDEN</div>
<custom-template>
<div data-djc-id-a1bc46>BLOCK OVERRIDEN</div>
<custom-template data-djc-id-a1bc46>
<header>SLOT OVERRIDEN</header>
<main>Default main</main>
<footer>Default footer</footer>
@ -435,7 +437,7 @@ class ExtendsCompatTests(BaseTestCase):
<main role="main">
<div class='container main-container'>
Variable: <strong></strong>
Variable: <strong></strong>
Variable: <strong data-djc-id-a1bc3f></strong>
</div>
</main>
</body>
@ -469,9 +471,9 @@ class ExtendsCompatTests(BaseTestCase):
rendered = Template(template).render(Context())
expected = """
<!DOCTYPE html>
<html lang="en">
<html data-djc-id-a1bc40 lang="en">
<body>
<custom-template>
<custom-template data-djc-id-a1bc45>
<header></header>
<main>BODY_FROM_FILL</main>
<footer>Default footer</footer>
@ -502,9 +504,9 @@ class ExtendsCompatTests(BaseTestCase):
rendered = Template(template).render(Context())
expected = """
<!DOCTYPE html>
<html lang="en">
<html data-djc-id-a1bc40 lang="en">
<body>
<custom-template>
<custom-template data-djc-id-a1bc45>
<header></header>
<main>BODY_FROM_FILL</main>
<footer>Default footer</footer>
@ -537,7 +539,7 @@ class ExtendsCompatTests(BaseTestCase):
<body>
<main role="main">
<div class='container main-container'>
<custom-template>
<custom-template data-djc-id-a1bc42>
<header></header>
<main>TEST</main>
<footer></footer>
@ -567,7 +569,7 @@ class ExtendsCompatTests(BaseTestCase):
<!DOCTYPE html>
<html lang="en">
<body>
<custom-template>
<custom-template data-djc-id-a1bc41>
<header></header>
<main>
<div> 58 giraffes and 2 pantaloons </div>
@ -594,9 +596,9 @@ class ExtendsCompatTests(BaseTestCase):
rendered = Template(template).render(Context())
expected = """
<!DOCTYPE html>
<html lang="en">
<html data-djc-id-a1bc3f lang="en">
<body>
<custom-template>
<custom-template data-djc-id-a1bc43>
<header></header>
<main>
<div> 58 giraffes and 2 pantaloons </div>
@ -634,9 +636,9 @@ class ExtendsCompatTests(BaseTestCase):
rendered = Template(template).render(Context())
expected = """
<!DOCTYPE html>
<html lang="en">
<html data-djc-id-a1bc40 lang="en">
<body>
<custom-template>
<custom-template data-djc-id-a1bc45>
<header></header>
<main>BODY_FROM_FILL</main>
<footer>Default footer</footer>
@ -664,9 +666,9 @@ class ExtendsCompatTests(BaseTestCase):
rendered = Template(template).render(Context())
expected = """
<!DOCTYPE html>
<html lang="en">
<html data-djc-id-a1bc3f lang="en">
<body>
<custom-template>
<custom-template data-djc-id-a1bc44>
<header></header>
<main>
Helloodiddoo
@ -700,9 +702,9 @@ class ExtendsCompatTests(BaseTestCase):
rendered = Template(template).render(Context())
expected = """
<!DOCTYPE html>
<html lang="en">
<html data-djc-id-a1bc3f lang="en">
<body>
<custom-template>
<custom-template data-djc-id-a1bc44>
<header></header>
<main>
Helloodiddoo
@ -736,9 +738,9 @@ class ExtendsCompatTests(BaseTestCase):
rendered = Template(template).render(Context())
expected = """
<!DOCTYPE html>
<html lang="en">
<html data-djc-id-a1bc40 lang="en">
<body>
<custom-template>
<custom-template data-djc-id-a1bc45>
<header></header>
<main>
Helloodiddoo
@ -782,9 +784,9 @@ class ExtendsCompatTests(BaseTestCase):
rendered = Template(template).render(Context())
expected = """
<!DOCTYPE html>
<html lang="en">
<html data-djc-id-a1bc41 lang="en">
<body>
<custom-template>
<custom-template data-djc-id-a1bc47>
<header></header>
<main>
Helloodiddoo
@ -824,10 +826,10 @@ class ExtendsCompatTests(BaseTestCase):
<!DOCTYPE html>
<html lang="en">
<body>
<custom-template>
<custom-template data-djc-id-a1bc43>
<header></header>
<main>
<div> injected: DepInject(hello='from_block') </div>
<div data-djc-id-a1bc47> injected: DepInject(hello='from_block') </div>
</main>
<footer>Default footer</footer>
</custom-template>

View file

@ -36,7 +36,7 @@ class ProvideTemplateTagTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div> injected: DepInject(key='hi', another=123) </div>
<div data-djc-id-a1bc40> injected: DepInject(key='hi', another=123) </div>
""",
)
@ -87,8 +87,8 @@ class ProvideTemplateTagTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div> key: hi </div>
<div> another: 123 </div>
<div data-djc-id-a1bc40> key: hi </div>
<div data-djc-id-a1bc40> another: 123 </div>
""",
)
@ -120,8 +120,8 @@ class ProvideTemplateTagTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div> key: hi </div>
<div> another: 123 </div>
<div data-djc-id-a1bc40> key: hi </div>
<div data-djc-id-a1bc40> another: 123 </div>
""",
)
@ -150,7 +150,7 @@ class ProvideTemplateTagTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div> injected: default </div>
<div data-djc-id-a1bc40> injected: default </div>
""",
)
@ -183,8 +183,8 @@ class ProvideTemplateTagTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div> injected: DepInject() </div>
<div> injected: default </div>
<div data-djc-id-a1bc41> injected: DepInject() </div>
<div data-djc-id-a1bc42> injected: default </div>
""",
)
@ -216,8 +216,8 @@ class ProvideTemplateTagTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div></div>
<div></div>
<div data-djc-id-a1bc41></div>
<div data-djc-id-a1bc42></div>
""",
)
@ -248,8 +248,8 @@ class ProvideTemplateTagTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div> injected: DepInject(key='hi', another=123) </div>
<div> injected: default </div>
<div data-djc-id-a1bc41> injected: DepInject(key='hi', another=123) </div>
<div data-djc-id-a1bc42> injected: default </div>
""",
)
@ -286,8 +286,8 @@ class ProvideTemplateTagTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div> injected: DepInject(key='hi', another=123) </div>
<div> injected: default </div>
<div data-djc-id-a1bc41> injected: DepInject(key='hi', another=123) </div>
<div data-djc-id-a1bc42> injected: default </div>
""",
)
@ -328,8 +328,8 @@ class ProvideTemplateTagTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div> injected: DepInject(key='hi', another=123) </div>
<div> injected: default </div>
<div data-djc-id-a1bc41> injected: DepInject(key='hi', another=123) </div>
<div data-djc-id-a1bc42> injected: default </div>
""",
)
@ -431,7 +431,7 @@ class ProvideTemplateTagTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div> injected: DepInject(var1={'key': 'hi', 'another': 123}, var2={'x': 'y'}) </div>
<div data-djc-id-a1bc40> injected: DepInject(var1={'key': 'hi', 'another': 123}, var2={'x': 'y'}) </div>
""",
)
@ -505,9 +505,9 @@ class ProvideTemplateTagTest(BaseTestCase):
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>
<div data-djc-id-a1bc43> injected: DepInject(key='hi1', another=1231, new=3) </div>
<div data-djc-id-a1bc44> injected: DepInject(key='hi', another=123, lost=0) </div>
<div data-djc-id-a1bc45> injected: default </div>
""",
)
@ -545,8 +545,8 @@ class ProvideTemplateTagTest(BaseTestCase):
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>
<div data-djc-id-a1bc41> first_provide: DepInject(key='hi', another=123, lost=0) </div>
<div data-djc-id-a1bc41> second_provide: DepInject(key='hi1', another=1231, new=3) </div>
""",
)
@ -575,7 +575,7 @@ class ProvideTemplateTagTest(BaseTestCase):
rendered,
"""
<div>
<div> injected: DepInject(key='hi', another=123) </div>
<div data-djc-id-a1bc40> injected: DepInject(key='hi', another=123) </div>
</div>
""",
)
@ -613,7 +613,7 @@ class ProvideTemplateTagTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div>
<div data-djc-id-a1bc40 data-djc-id-a1bc43>
injected: DepInject(key='hi', another=123)
</div>
""",
@ -646,7 +646,7 @@ class InjectTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div> injected: DepInject(key='hi', another=123) </div>
<div data-djc-id-a1bc40> injected: DepInject(key='hi', another=123) </div>
""",
)
@ -694,7 +694,7 @@ class InjectTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div> injected: default </div>
<div data-djc-id-a1bc3f> injected: default </div>
""",
)
@ -799,10 +799,12 @@ class InjectTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div>
<div data-djc-id-a1bc3e data-djc-id-a1bc41 data-djc-id-a1bc45 data-djc-id-a1bc48>
injected: DepInject(key='hi', data=123)
</div>
<main>456</main>
<main data-djc-id-a1bc3e data-djc-id-a1bc41 data-djc-id-a1bc45 data-djc-id-a1bc48>
456
</main>
""",
)
@ -861,9 +863,10 @@ class InjectTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div>
<div data-djc-id-a1bc3e data-djc-id-a1bc41 data-djc-id-a1bc44 data-djc-id-a1bc47>
injected: DepInject(key='hi', data=123)
</div>
<main></main>
<main data-djc-id-a1bc3e data-djc-id-a1bc41 data-djc-id-a1bc44 data-djc-id-a1bc47>
</main>
""",
)

View file

@ -67,9 +67,11 @@ class ComponentSlotTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<custom-template data-djc-id-a1bc42>
<header>Custom header</header>
<main>Variable: <strong>variable</strong></main>
<main>
Variable: <strong data-djc-id-a1bc46>variable</strong>
</main>
<footer>Default footer</footer>
</custom-template>
""",
@ -117,8 +119,10 @@ class ComponentSlotTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<header> Variable: <strong>variable</strong> </header>
<custom-template data-djc-id-a1bc43>
<header>
Variable: <strong data-djc-id-a1bc47>variable</strong>
</header>
<main></main>
<footer></footer>
</custom-template>
@ -149,7 +153,7 @@ class ComponentSlotTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
f"""
<custom-template>
<custom-template data-djc-id-a1bc41>
<header>Default header</header>
<main>test123 - {context_behavior_data} </main>
<footer>test321</footer>
@ -171,7 +175,7 @@ class ComponentSlotTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<custom-template data-djc-id-a1bc3f>
<header>Default header</header>
<main>Default main</main>
<footer>Default footer</footer>
@ -194,7 +198,7 @@ class ComponentSlotTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "<custom-template></custom-template>")
self.assertHTMLEqual(rendered, "<custom-template data-djc-id-a1bc3f></custom-template>")
@parametrize_context_behavior(["django", "isolated"])
def test_slotted_template_without_slots_and_single_quotes(self):
@ -211,7 +215,7 @@ class ComponentSlotTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "<custom-template></custom-template>")
self.assertHTMLEqual(rendered, "<custom-template data-djc-id-a1bc3f></custom-template>")
@parametrize_context_behavior(["django", "isolated"])
def test_variable_fill_name(self):
@ -227,7 +231,7 @@ class ComponentSlotTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({}))
expected = """
<custom-template>
<custom-template data-djc-id-a1bc40>
<header>Hi there!</header>
<main>Default main</main>
<footer>Default footer</footer>
@ -296,7 +300,7 @@ class ComponentSlotTests(BaseTestCase):
rendered,
"""
<body>
<div>
<div data-djc-id-a1bc3f>
<main> ABC: carl var </main>
</div>
</body>
@ -327,7 +331,7 @@ class ComponentSlotTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div>
<div data-djc-id-a1bc40>
<h1> Custom title </h1>
<h2> Default subtitle </h2>
</div>
@ -451,8 +455,8 @@ class ComponentSlotTests(BaseTestCase):
render_dependencies=False,
)
self.assertInHTML(rendered1, "<div><div>MAIN</div></div>")
self.assertInHTML(rendered2, "<div><div>MAIN</div></div>")
self.assertHTMLEqual(rendered1, "<div data-djc-id-a1bc3e><div>MAIN</div></div>")
self.assertHTMLEqual(rendered2, "<div data-djc-id-a1bc41><div>MAIN</div></div>")
# 3. Specify the required slot by its name
rendered3 = TestComp.render(
@ -462,7 +466,7 @@ class ComponentSlotTests(BaseTestCase):
},
render_dependencies=False,
)
self.assertInHTML(rendered3, "<div><main>MAIN</main><div>MAIN</div></div>")
self.assertHTMLEqual(rendered3, "<div data-djc-id-a1bc42><main>MAIN</main><div>MAIN</div></div>")
# 4. RAISES: Specify the required slot by the "default" name
# This raises because the slot that is marked as 'required' is NOT marked as 'default'.
@ -499,7 +503,7 @@ class ComponentSlotTests(BaseTestCase):
rendered = Template(template_str).render(Context({}))
expected = """
<custom-template>
<custom-template data-djc-id-a1bc42>
<header>Custom header</header>
<main>Custom main</main>
<footer>Custom footer</footer>
@ -553,8 +557,10 @@ class ComponentSlotDefaultTests(BaseTestCase):
template = Template(template_str)
expected = """
<div>
<main><p>This fills the 'main' slot.</p></main>
<div data-djc-id-a1bc3f>
<main>
<p>This fills the 'main' slot.</p>
</main>
</div>
"""
rendered = template.render(Context({}))
@ -579,8 +585,10 @@ class ComponentSlotDefaultTests(BaseTestCase):
"""
template = Template(template_str)
expected = """
<div>
<main><p>This fills the 'main' slot.</p></main>
<div data-djc-id-a1bc40>
<main>
<p>This fills the 'main' slot.</p>
</main>
</div>
"""
rendered = template.render(Context({}))
@ -606,9 +614,9 @@ class ComponentSlotDefaultTests(BaseTestCase):
"""
template = Template(template_str)
expected = """
<div>
<main><p>This fills the 'main' slot.</p></main>
<div><p>This fills the 'main' slot.</p></div>
<div data-djc-id-a1bc40>
<main><p>This fills the 'main' slot.</p></main>
<div><p>This fills the 'main' slot.</p></div>
</div>
"""
rendered = template.render(Context({}))
@ -686,14 +694,14 @@ class ComponentSlotDefaultTests(BaseTestCase):
"""
template = Template(template_str)
expected = """
<div>
<main>
<custom-template>
<header>This Is Allowed</header>
<main></main>
<footer></footer>
</custom-template>
</main>
<div data-djc-id-a1bc43>
<main>
<custom-template data-djc-id-a1bc45>
<header>This Is Allowed</header>
<main></main>
<footer></footer>
</custom-template>
</main>
</div>
"""
rendered = template.render(Context({}))
@ -761,7 +769,7 @@ class ComponentSlotDefaultTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<custom-template data-djc-id-a1bc3f>
<header>Default header</header>
<main>Default main</main>
<footer>Default footer</footer>
@ -799,7 +807,7 @@ class ComponentSlotDefaultTests(BaseTestCase):
self.assertHTMLEqual(
rendered_truthy,
"""
<custom-template>
<custom-template data-djc-id-a1bc3f>
<header>123</header>
<main>Default main</main>
<footer>Default footer</footer>
@ -811,7 +819,7 @@ class ComponentSlotDefaultTests(BaseTestCase):
self.assertHTMLEqual(
rendered_falsy,
"""
<custom-template>
<custom-template data-djc-id-a1bc43>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
@ -862,7 +870,7 @@ class PassthroughSlotsTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<custom-template data-djc-id-a1bc41>
<header>
OVERRIDEN_SLOT "header" - INDEX 0 - ORIGINAL "Default header"
</header>
@ -910,7 +918,7 @@ class PassthroughSlotsTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<custom-template data-djc-id-a1bc40>
<header>
OVERRIDEN_SLOT "header" - ORIGINAL "Default header"
</header>
@ -997,9 +1005,9 @@ class PassthroughSlotsTest(BaseTestCase):
rendered = template.render(Context())
expected = """
<div>CUSTOM HEADER</div>
<div>CUSTOM MAIN</div>
<div>footer</div>
<div data-djc-id-a1bc41>CUSTOM HEADER</div>
<div data-djc-id-a1bc41>CUSTOM MAIN</div>
<div data-djc-id-a1bc41>footer</div>
"""
self.assertHTMLEqual(rendered, expected)
@ -1042,11 +1050,11 @@ class PassthroughSlotsTest(BaseTestCase):
rendered = template.render(Context())
expected = """
<div>
<custom-template>
<header>CUSTOM HEADER</header>
<main>CUSTOM MAIN</main>
<footer>Default footer</footer>
<div data-djc-id-a1bc41>
<custom-template data-djc-id-a1bc45>
<header>CUSTOM HEADER</header>
<main>CUSTOM MAIN</main>
<footer>Default footer</footer>
</custom-template>
</div>
"""
@ -1093,11 +1101,11 @@ class PassthroughSlotsTest(BaseTestCase):
rendered = template.render(Context())
expected = """
<div>
<custom-template>
<header>Default header</header>
<main>CUSTOM MAIN</main>
<footer>Default footer</footer>
<div data-djc-id-a1bc41>
<custom-template data-djc-id-a1bc45>
<header>Default header</header>
<main>CUSTOM MAIN</main>
<footer>Default footer</footer>
</custom-template>
</div>
"""
@ -1145,7 +1153,7 @@ class NestedSlotsTests(BaseTestCase):
rendered = Template(template_str).render(Context())
expected = """
<div>
<div data-djc-id-a1bc3f>
Wrapper Default
<div>
Parent1 Default
@ -1175,7 +1183,7 @@ class NestedSlotsTests(BaseTestCase):
rendered = Template(template_str).render(Context())
expected = """
<div>
<div data-djc-id-a1bc40>
Entire Wrapper Replaced
</div>
"""
@ -1196,7 +1204,7 @@ class NestedSlotsTests(BaseTestCase):
rendered = Template(template_str).render(Context())
expected = """
<div>
<div data-djc-id-a1bc40>
Wrapper Default
<div>
Parent1 Replaced
@ -1223,7 +1231,7 @@ class NestedSlotsTests(BaseTestCase):
rendered = Template(template_str).render(Context())
expected = """
<div>
<div data-djc-id-a1bc40>
Wrapper Default
<div>
Parent1 Default
@ -1263,7 +1271,7 @@ class NestedSlotsTests(BaseTestCase):
rendered = Template(template_str).render(Context())
expected = """
<div>
<div data-djc-id-a1bc42>
Entire Wrapper Replaced
</div>
"""
@ -1295,7 +1303,7 @@ class SlottedTemplateRegressionTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<custom-template data-djc-id-a1bc3f>
<header>Default header</header>
<main>Default main</main>
<footer>Default footer</footer>
@ -1325,7 +1333,7 @@ class SlotDefaultTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<custom-template data-djc-id-a1bc42>
<header>Before: Default header</header>
<main>Default main</main>
<footer>Default footer, after</footer>
@ -1350,7 +1358,7 @@ class SlotDefaultTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<custom-template data-djc-id-a1bc40>
<header>First: Default header; Second: Default header</header>
<main>Default main</main>
<footer>Default footer</footer>
@ -1380,7 +1388,7 @@ class SlotDefaultTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<custom-template data-djc-id-a1bc40>
<header>First Default header Later Default header Later Default header</header>
<main>Default main</main>
<footer>Default footer</footer>
@ -1414,10 +1422,10 @@ class SlotDefaultTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<custom-template data-djc-id-a1bc43>
<header>
header1_in_header1: Default header
<custom-template>
<custom-template data-djc-id-a1bc47>
<header>
header1_in_header2: Default header
header2_in_header2: Default header
@ -1465,7 +1473,7 @@ class ScopedSlotTest(BaseTestCase):
"""
rendered = Template(template).render(Context())
expected = """
<div>
<div data-djc-id-a1bc40>
def
456
</div>
@ -1500,7 +1508,7 @@ class ScopedSlotTest(BaseTestCase):
"""
rendered = Template(template).render(Context())
expected = """
<div>
<div data-djc-id-a1bc40>
def
456
</div>
@ -1536,7 +1544,7 @@ class ScopedSlotTest(BaseTestCase):
"""
rendered = Template(template).render(Context())
expected = """
<div>
<div data-djc-id-a1bc40>
Default text
def
456
@ -1573,7 +1581,7 @@ class ScopedSlotTest(BaseTestCase):
"""
rendered = Template(template).render(Context())
expected = """
<div>
<div data-djc-id-a1bc40>
def
456
</div>
@ -1611,7 +1619,7 @@ class ScopedSlotTest(BaseTestCase):
"""
rendered = Template(template).render(Context())
expected = """
<div>
<div data-djc-id-a1bc40>
def
456
</div>
@ -1648,7 +1656,7 @@ class ScopedSlotTest(BaseTestCase):
"""
rendered = Template(template).render(Context())
expected = """
<div>
<div data-djc-id-a1bc40>
<b>Default text A</b>
<b>xyz Default text B 456</b>
</div>
@ -1712,7 +1720,7 @@ class ScopedSlotTest(BaseTestCase):
{% endcomponent %}
"""
rendered = Template(template).render(Context())
expected = "<div> overriden </div>"
expected = "<div data-djc-id-a1bc40> overriden </div>"
self.assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
@ -1735,7 +1743,7 @@ class ScopedSlotTest(BaseTestCase):
{% endcomponent %}
"""
rendered = Template(template).render(Context())
expected = "<div> {} </div>"
expected = "<div data-djc-id-a1bc40> {} </div>"
self.assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
@ -1761,7 +1769,7 @@ class ScopedSlotTest(BaseTestCase):
{% endcomponent %}
"""
rendered = Template(template).render(Context())
expected = "<div> Default text </div>"
expected = "<div data-djc-id-a1bc3f> Default text </div>"
self.assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
@ -1800,7 +1808,7 @@ class ScopedSlotTest(BaseTestCase):
)
expected = """
<div>
<div data-djc-id-a1bc40>
def
456
</div>
@ -1845,7 +1853,7 @@ class ScopedSlotTest(BaseTestCase):
)
expected = """
<div>
<div data-djc-id-a1bc40>
def
456
</div>
@ -1889,9 +1897,9 @@ class ScopedSlotTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<div>
<div data-djc-id-a1bc42>
data1_in_slot1: {'abc': 'def', 'input': 1}
<div>
<div data-djc-id-a1bc44>
data1_in_slot2: {'abc': 'def', 'input': 1}
data2_in_slot2: {'abc': 'def', 'input': 2}
</div>
@ -1990,9 +1998,9 @@ class DuplicateSlotTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<header>Name: Jannete</header>
<main>Name: Jannete</main>
<footer>Hello</footer>
<header data-djc-id-a1bc41>Name: Jannete</header>
<main data-djc-id-a1bc41>Name: Jannete</main>
<footer data-djc-id-a1bc41>Hello</footer>
""",
)
@ -2010,9 +2018,9 @@ class DuplicateSlotTest(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<header>Default header</header>
<main>Default main header</main>
<footer>Default footer</footer>
<header data-djc-id-a1bc3f>Default header</header>
<main data-djc-id-a1bc3f>Default main header</main>
<footer data-djc-id-a1bc3f>Default footer</footer>
""",
)
@ -2034,8 +2042,8 @@ class DuplicateSlotTest(BaseTestCase):
rendered,
"""
OVERRIDDEN!
<div class="dashboard-component">
<div class="calendar-component">
<div class="dashboard-component" data-djc-id-a1bc40>
<div class="calendar-component" data-djc-id-a1bc47>
<h1>
OVERRIDDEN!
</h1>
@ -2071,8 +2079,8 @@ class DuplicateSlotTest(BaseTestCase):
rendered,
"""
START
<div class="dashboard-component">
<div class="calendar-component">
<div class="dashboard-component" data-djc-id-a1bc3f>
<div class="calendar-component" data-djc-id-a1bc46>
<h1>
NESTED
</h1>
@ -2198,11 +2206,11 @@ class SlotBehaviorTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<custom-template data-djc-id-a1bc45>
<header>Name: Igor</header>
<main>Day: Monday</main>
<footer>
<custom-template>
<custom-template data-djc-id-a1bc49>
<header>Name2: Joe2</header>
<main>Day2: Monday</main>
<footer>Default footer</footer>
@ -2214,7 +2222,22 @@ class SlotBehaviorTests(BaseTestCase):
# {{ name }} should be effectively the same as before, because overriden everywhere
rendered2 = template.render(Context({"day": "Monday"}))
self.assertHTMLEqual(rendered2, rendered)
self.assertHTMLEqual(
rendered2,
"""
<custom-template data-djc-id-a1bc4a>
<header>Name: Igor</header>
<main>Day: Monday</main>
<footer>
<custom-template data-djc-id-a1bc4b>
<header>Name2: Joe2</header>
<main>Day2: Monday</main>
<footer>Default footer</footer>
</custom-template>
</footer>
</custom-template>
""",
)
@parametrize_context_behavior(["isolated"])
def test_slot_context__isolated(self):
@ -2224,11 +2247,11 @@ class SlotBehaviorTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<custom-template data-djc-id-a1bc45>
<header>Name: Jannete</header>
<main>Day: Monday</main>
<footer>
<custom-template>
<custom-template data-djc-id-a1bc49>
<header>Name2: Jannete</header>
<main>Day2: Monday</main>
<footer>Default footer</footer>
@ -2243,11 +2266,11 @@ class SlotBehaviorTests(BaseTestCase):
self.assertHTMLEqual(
rendered2,
"""
<custom-template>
<custom-template data-djc-id-a1bc4a>
<header>Name: </header>
<main>Day: Monday</main>
<footer>
<custom-template>
<custom-template data-djc-id-a1bc4b>
<header>Name2: </header>
<main>Day2: Monday</main>
<footer>Default footer</footer>

View file

@ -52,7 +52,7 @@ class NestedSlotTests(BaseTestCase):
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, '<div id="outer">Default</div>')
self.assertHTMLEqual(rendered, '<div id="outer" data-djc-id-a1bc3f>Default</div>')
@parametrize_context_behavior(["django", "isolated"])
def test_inner_slot_overriden(self):
@ -66,7 +66,7 @@ class NestedSlotTests(BaseTestCase):
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, '<div id="outer">Override</div>')
self.assertHTMLEqual(rendered, '<div id="outer" data-djc-id-a1bc40>Override</div>')
@parametrize_context_behavior(["django", "isolated"])
def test_outer_slot_overriden(self):
@ -78,7 +78,7 @@ class NestedSlotTests(BaseTestCase):
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "<p>Override</p>")
self.assertHTMLEqual(rendered, "<p data-djc-id-a1bc40>Override</p>")
@parametrize_context_behavior(["django", "isolated"])
def test_both_overriden_and_inner_removed(self):
@ -93,7 +93,7 @@ class NestedSlotTests(BaseTestCase):
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "<p>Override</p>")
self.assertHTMLEqual(rendered, "<p data-djc-id-a1bc41>Override</p>")
# NOTE: Second arg in tuple is expected name in nested fill. In "django" mode,
# the value should be overridden by the component, while in "isolated" it should
@ -145,9 +145,9 @@ class NestedSlotTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
f"""
<custom-template>
<custom-template data-djc-id-a1bc45>
<header>
<custom-template>
<custom-template data-djc-id-a1bc49>
<header>Name2: {context_behavior_data}</header>
<main>Day2: Monday</main>
<footer>XYZ</footer>
@ -202,7 +202,13 @@ class ConditionalSlotTests(BaseTestCase):
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, '<p id="a">Default A</p><p id="b">Default B</p>')
self.assertHTMLEqual(
rendered,
"""
<p id="a" data-djc-id-a1bc40>Default A</p>
<p id="b" data-djc-id-a1bc43>Default B</p>
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_one_slot_overridden(self):
@ -217,7 +223,13 @@ class ConditionalSlotTests(BaseTestCase):
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, '<p id="a">Default A</p><p id="b">Override B</p>')
self.assertHTMLEqual(
rendered,
"""
<p id="a" data-djc-id-a1bc42>Default A</p>
<p id="b" data-djc-id-a1bc45>Override B</p>
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_both_slots_overridden(self):
@ -234,7 +246,13 @@ class ConditionalSlotTests(BaseTestCase):
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, '<p id="a">Override A</p><p id="b">Override B</p>')
self.assertHTMLEqual(
rendered,
"""
<p id="a" data-djc-id-a1bc44>Override A</p>
<p id="b" data-djc-id-a1bc47>Override B</p>
""",
)
class SlotIterationTest(BaseTestCase):
@ -670,11 +688,11 @@ class ComponentNestingTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
f"""
<custom-template>
<custom-template data-djc-id-a1bc45>
<header>Name: {first_name}</header>
<main>Day: Monday</main>
<footer>
<custom-template>
<custom-template data-djc-id-a1bc49>
<header>Name2: {second_name}</header>
<main>Day2: Monday</main>
<footer>Default footer</footer>
@ -699,8 +717,8 @@ class ComponentNestingTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({"items": [1, 2, 3]}))
expected = f"""
<div class="dashboard-component">
<div class="calendar-component">
<div class="dashboard-component" data-djc-id-a1bc3f>
<div class="calendar-component" data-djc-id-a1bc44>
<h1>
Welcome to your dashboard!
</h1>
@ -734,8 +752,8 @@ class ComponentNestingTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({"items": [1, 2, 3]}))
expected = f"""
<div class="dashboard-component">
<div class="calendar-component">
<div class="dashboard-component" data-djc-id-a1bc40>
<div class="calendar-component" data-djc-id-a1bc45>
<h1>
Whoa!
</h1>
@ -759,16 +777,17 @@ class ComponentNestingTests(BaseTestCase):
template = Template(template_str)
items = [{"value": 1}, {"value": 2}, {"value": 3}]
rendered = template.render(Context({"items": items}))
expected = """
ITEMS: [{'value': 1}, {'value': 2}, {'value': 3}]
<li>
<div> 1 </div>
<li data-djc-id-a1bc3f>
<div data-djc-id-a1bc41> 1 </div>
</li>
<li>
<div> 2 </div>
<li data-djc-id-a1bc3f>
<div data-djc-id-a1bc43> 2 </div>
</li>
<li>
<div> 3 </div>
<li data-djc-id-a1bc3f>
<div data-djc-id-a1bc44> 3 </div>
</li>
"""
self.assertHTMLEqual(rendered, expected)
@ -790,8 +809,8 @@ class ComponentNestingTests(BaseTestCase):
template = Template(template_str)
rendered = template.render(Context({"items": [1, 2]}))
expected = f"""
<div class="dashboard-component">
<div class="calendar-component">
<div class="dashboard-component" data-djc-id-a1bc40>
<div class="calendar-component" data-djc-id-a1bc45>
<h1>
Hello! Welcome to your dashboard!
</h1>

View file

@ -176,6 +176,12 @@ def parametrize_context_behavior(cases: List[ContextBehParam], settings: Optiona
self._stop_gen_id_patch()
self._start_gen_id_patch()
# Reset template cache
from django_components.template import template_cache
if template_cache: # May be None if the cache was not initialized
template_cache.clear()
case_has_data = not isinstance(case, str)
if isinstance(case, str):