mirror of
https://github.com/django-components/django-components.git
synced 2025-09-26 07:29:09 +00:00
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:
parent
296da4a1e8
commit
fe67d90547
28 changed files with 1181 additions and 594 deletions
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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),
|
||||
]
|
||||
|
||||
|
|
|
@ -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)}>"
|
||||
|
|
|
@ -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=$;})();
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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}`;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue