mirror of https://github.com/sveltejs/svelte
move element code (#10778)
* move element code * eslint nonsense --------- Co-authored-by: Rich Harris <rich.harris@vercel.com>pull/10770/head
parent
84fa18a850
commit
de021aebb5
@ -0,0 +1,363 @@
|
||||
import { DEV } from 'esm-env';
|
||||
import { hydrating } from '../../hydration.js';
|
||||
import { render_effect } from '../../reactivity/effects.js';
|
||||
import { get_descriptors, object_assign } from '../../utils.js';
|
||||
import { map_get, map_set } from '../../operations.js';
|
||||
import { AttributeAliases, DelegatedEvents, namespace_svg } from '../../../../constants.js';
|
||||
import { delegate } from './events.js';
|
||||
import { autofocus } from './misc.js';
|
||||
|
||||
/**
|
||||
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
|
||||
* to remove it upon hydration to avoid a bug when someone resets the form value.
|
||||
* @param {HTMLInputElement | HTMLSelectElement} dom
|
||||
* @returns {void}
|
||||
*/
|
||||
export function remove_input_attr_defaults(dom) {
|
||||
if (hydrating) {
|
||||
attr(dom, 'value', null);
|
||||
attr(dom, 'checked', null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} dom
|
||||
* @param {string} attribute
|
||||
* @param {() => string} value
|
||||
*/
|
||||
export function attr_effect(dom, attribute, value) {
|
||||
render_effect(() => {
|
||||
attr(dom, attribute, value());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} dom
|
||||
* @param {string} attribute
|
||||
* @param {string | null} value
|
||||
*/
|
||||
export function attr(dom, attribute, value) {
|
||||
value = value == null ? null : value + '';
|
||||
|
||||
if (DEV) {
|
||||
check_src_in_dev_hydration(dom, attribute, value);
|
||||
}
|
||||
|
||||
if (
|
||||
!hydrating ||
|
||||
(dom.getAttribute(attribute) !== value &&
|
||||
// If we reset those, they would result in another network request, which we want to avoid.
|
||||
// We assume they are the same between client and server as checking if they are equal is expensive
|
||||
// (we can't just compare the strings as they can be different between client and server but result in the
|
||||
// same url, so we would need to create hidden anchor elements to compare them)
|
||||
attribute !== 'src' &&
|
||||
attribute !== 'href' &&
|
||||
attribute !== 'srcset')
|
||||
) {
|
||||
if (value === null) {
|
||||
dom.removeAttribute(attribute);
|
||||
} else {
|
||||
dom.setAttribute(attribute, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} dom
|
||||
* @param {string} attribute
|
||||
* @param {() => string} value
|
||||
*/
|
||||
export function xlink_attr_effect(dom, attribute, value) {
|
||||
render_effect(() => {
|
||||
xlink_attr(dom, attribute, value());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} dom
|
||||
* @param {string} attribute
|
||||
* @param {string} value
|
||||
*/
|
||||
export function xlink_attr(dom, attribute, value) {
|
||||
dom.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} node
|
||||
* @param {string} prop
|
||||
* @param {() => any} value
|
||||
*/
|
||||
export function set_custom_element_data_effect(node, prop, value) {
|
||||
render_effect(() => {
|
||||
set_custom_element_data(node, prop, value());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} node
|
||||
* @param {string} prop
|
||||
* @param {any} value
|
||||
*/
|
||||
export function set_custom_element_data(node, prop, value) {
|
||||
if (prop in node) {
|
||||
node[prop] = typeof node[prop] === 'boolean' && value === '' ? true : value;
|
||||
} else {
|
||||
attr(node, prop, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `spread_attributes` but self-contained
|
||||
* @param {Element & ElementCSSInlineStyle} dom
|
||||
* @param {() => Record<string, unknown>[]} attrs
|
||||
* @param {boolean} lowercase_attributes
|
||||
* @param {string} css_hash
|
||||
*/
|
||||
export function spread_attributes_effect(dom, attrs, lowercase_attributes, css_hash) {
|
||||
/** @type {Record<string, any> | undefined} */
|
||||
var current;
|
||||
|
||||
render_effect(() => {
|
||||
current = spread_attributes(dom, current, attrs(), lowercase_attributes, css_hash);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Spreads attributes onto a DOM element, taking into account the currently set attributes
|
||||
* @param {Element & ElementCSSInlineStyle} dom
|
||||
* @param {Record<string, unknown> | undefined} prev
|
||||
* @param {Record<string, unknown>[]} attrs
|
||||
* @param {boolean} lowercase_attributes
|
||||
* @param {string} css_hash
|
||||
* @returns {Record<string, unknown>}
|
||||
*/
|
||||
export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_hash) {
|
||||
var next = object_assign({}, ...attrs);
|
||||
var has_hash = css_hash.length !== 0;
|
||||
|
||||
for (var key in prev) {
|
||||
if (!(key in next)) {
|
||||
next[key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (has_hash && !next.class) {
|
||||
next.class = '';
|
||||
}
|
||||
|
||||
var setters = map_get(setters_cache, dom.nodeName);
|
||||
if (!setters) map_set(setters_cache, dom.nodeName, (setters = get_setters(dom)));
|
||||
|
||||
for (key in next) {
|
||||
var value = next[key];
|
||||
if (value === prev?.[key]) continue;
|
||||
|
||||
var prefix = key[0] + key[1]; // this is faster than key.slice(0, 2)
|
||||
if (prefix === '$$') continue;
|
||||
|
||||
if (prefix === 'on') {
|
||||
/** @type {{ capture?: true }} */
|
||||
var opts = {};
|
||||
var event_name = key.slice(2);
|
||||
var delegated = DelegatedEvents.includes(event_name);
|
||||
|
||||
if (
|
||||
event_name.endsWith('capture') &&
|
||||
event_name !== 'ongotpointercapture' &&
|
||||
event_name !== 'onlostpointercapture'
|
||||
) {
|
||||
event_name = event_name.slice(0, -7);
|
||||
opts.capture = true;
|
||||
}
|
||||
|
||||
if (!delegated && prev?.[key]) {
|
||||
dom.removeEventListener(event_name, /** @type {any} */ (prev[key]), opts);
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
if (!delegated) {
|
||||
dom.addEventListener(event_name, value, opts);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
dom[`__${event_name}`] = value;
|
||||
delegate([event_name]);
|
||||
}
|
||||
}
|
||||
} else if (value == null) {
|
||||
dom.removeAttribute(key);
|
||||
} else if (key === 'style') {
|
||||
dom.style.cssText = value + '';
|
||||
} else if (key === 'autofocus') {
|
||||
autofocus(/** @type {HTMLElement} */ (dom), Boolean(value));
|
||||
} else if (key === '__value' || key === 'value') {
|
||||
// @ts-ignore
|
||||
dom.value = dom[key] = dom.__value = value;
|
||||
} else {
|
||||
var name = key;
|
||||
if (lowercase_attributes) {
|
||||
name = name.toLowerCase();
|
||||
name = AttributeAliases[name] || name;
|
||||
}
|
||||
|
||||
if (setters.includes(name)) {
|
||||
if (DEV) {
|
||||
check_src_in_dev_hydration(dom, name, value);
|
||||
}
|
||||
|
||||
if (
|
||||
!hydrating ||
|
||||
// @ts-ignore see attr method for an explanation of src/srcset
|
||||
(dom[name] !== value && name !== 'src' && name !== 'href' && name !== 'srcset')
|
||||
) {
|
||||
// @ts-ignore
|
||||
dom[name] = value;
|
||||
}
|
||||
} else if (typeof value !== 'function') {
|
||||
if (has_hash && name === 'class') {
|
||||
if (value) value += ' ';
|
||||
value += css_hash;
|
||||
}
|
||||
|
||||
attr(dom, name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} node
|
||||
* @param {() => Record<string, unknown>[]} attrs
|
||||
* @param {string} css_hash
|
||||
*/
|
||||
export function spread_dynamic_element_attributes_effect(node, attrs, css_hash) {
|
||||
/** @type {Record<string, any> | undefined} */
|
||||
var current;
|
||||
|
||||
render_effect(() => {
|
||||
current = spread_dynamic_element_attributes(node, current, attrs(), css_hash);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} node
|
||||
* @param {Record<string, unknown> | undefined} prev
|
||||
* @param {Record<string, unknown>[]} attrs
|
||||
* @param {string} css_hash
|
||||
*/
|
||||
export function spread_dynamic_element_attributes(node, prev, attrs, css_hash) {
|
||||
if (node.tagName.includes('-')) {
|
||||
var next = object_assign({}, ...attrs);
|
||||
|
||||
for (var key in prev) {
|
||||
if (!(key in next)) {
|
||||
next[key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
for (key in next) {
|
||||
set_custom_element_data(node, key, next[key]);
|
||||
}
|
||||
|
||||
return next;
|
||||
} else {
|
||||
return spread_attributes(
|
||||
/** @type {Element & ElementCSSInlineStyle} */ (node),
|
||||
prev,
|
||||
attrs,
|
||||
node.namespaceURI !== namespace_svg,
|
||||
css_hash
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List of attributes that should always be set through the attr method,
|
||||
* because updating them through the property setter doesn't work reliably.
|
||||
* In the example of `width`/`height`, the problem is that the setter only
|
||||
* accepts numeric values, but the attribute can also be set to a string like `50%`.
|
||||
* If this list becomes too big, rethink this approach.
|
||||
*/
|
||||
var always_set_through_set_attribute = ['width', 'height'];
|
||||
|
||||
/** @type {Map<string, string[]>} */
|
||||
var setters_cache = new Map();
|
||||
|
||||
/** @param {Element} element */
|
||||
function get_setters(element) {
|
||||
/** @type {string[]} */
|
||||
var setters = [];
|
||||
|
||||
// @ts-expect-error
|
||||
var descriptors = get_descriptors(element.__proto__);
|
||||
|
||||
for (var key in descriptors) {
|
||||
if (descriptors[key].set && !always_set_through_set_attribute.includes(key)) {
|
||||
setters.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return setters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} dom
|
||||
* @param {string} attribute
|
||||
* @param {string | null} value
|
||||
*/
|
||||
function check_src_in_dev_hydration(dom, attribute, value) {
|
||||
if (!hydrating) return;
|
||||
if (attribute !== 'src' && attribute !== 'href' && attribute !== 'srcset') return;
|
||||
|
||||
if (attribute === 'srcset' && srcset_url_equal(dom, value)) return;
|
||||
if (src_url_equal(dom.getAttribute(attribute) ?? '', value ?? '')) return;
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Detected a ${attribute} attribute value change during hydration. This will not be repaired during hydration, ` +
|
||||
`the ${attribute} value that came from the server will be used. Related element:`,
|
||||
dom,
|
||||
' Differing value:',
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} element_src
|
||||
* @param {string} url
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function src_url_equal(element_src, url) {
|
||||
if (element_src === url) return true;
|
||||
return new URL(element_src, document.baseURI).href === new URL(url, document.baseURI).href;
|
||||
}
|
||||
|
||||
/** @param {string} srcset */
|
||||
function split_srcset(srcset) {
|
||||
return srcset.split(',').map((src) => src.trim().split(' ').filter(Boolean));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLSourceElement | HTMLImageElement} element
|
||||
* @param {string | undefined | null} srcset
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function srcset_url_equal(element, srcset) {
|
||||
var element_urls = split_srcset(element.srcset);
|
||||
var urls = split_srcset(srcset ?? '');
|
||||
|
||||
return (
|
||||
urls.length === element_urls.length &&
|
||||
urls.every(
|
||||
([url, width], i) =>
|
||||
width === element_urls[i][1] &&
|
||||
// We need to test both ways because Vite will create an a full URL with
|
||||
// `new URL(asset, import.meta.url).href` for the client when `base: './'`, and the
|
||||
// relative URLs inside srcset are not automatically resolved to absolute URLs by
|
||||
// browsers (in contrast to img.src). This means both SSR and DOM code could
|
||||
// contain relative or absolute URLs.
|
||||
(src_url_equal(element_urls[i][0], url) || src_url_equal(url, element_urls[i][0]))
|
||||
)
|
||||
);
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
import { hydrating } from '../../hydration.js';
|
||||
import { set_class_name } from '../../operations.js';
|
||||
import { render_effect } from '../../reactivity/effects.js';
|
||||
|
||||
/**
|
||||
* @param {Element} dom
|
||||
* @param {() => string} value
|
||||
* @returns {void}
|
||||
*/
|
||||
export function class_name_effect(dom, value) {
|
||||
render_effect(() => {
|
||||
class_name(dom, value());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} dom
|
||||
* @param {string} value
|
||||
* @returns {void}
|
||||
*/
|
||||
export function class_name(dom, value) {
|
||||
// @ts-expect-error need to add __className to patched prototype
|
||||
var prev_class_name = dom.__className;
|
||||
var next_class_name = to_class(value);
|
||||
|
||||
if (hydrating && dom.className === next_class_name) {
|
||||
// In case of hydration don't reset the class as it's already correct.
|
||||
// @ts-expect-error need to add __className to patched prototype
|
||||
dom.__className = next_class_name;
|
||||
} else if (
|
||||
prev_class_name !== next_class_name ||
|
||||
(hydrating && dom.className !== next_class_name)
|
||||
) {
|
||||
if (next_class_name === '') {
|
||||
dom.removeAttribute('class');
|
||||
} else {
|
||||
set_class_name(dom, next_class_name);
|
||||
}
|
||||
|
||||
// @ts-expect-error need to add __className to patched prototype
|
||||
dom.__className = next_class_name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template V
|
||||
* @param {V} value
|
||||
* @returns {string | V}
|
||||
*/
|
||||
export function to_class(value) {
|
||||
return value == null ? '' : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} dom
|
||||
* @param {string} class_name
|
||||
* @param {boolean} value
|
||||
* @returns {void}
|
||||
*/
|
||||
export function class_toggle(dom, class_name, value) {
|
||||
if (value) {
|
||||
dom.classList.add(class_name);
|
||||
} else {
|
||||
dom.classList.remove(class_name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} dom
|
||||
* @param {string} class_name
|
||||
* @param {() => boolean} value
|
||||
* @returns {void}
|
||||
*/
|
||||
export function class_toggle_effect(dom, class_name, value) {
|
||||
render_effect(() => {
|
||||
class_toggle(dom, class_name, value());
|
||||
});
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
import { render_effect } from '../../reactivity/effects.js';
|
||||
import { all_registered_events, root_event_handles } from '../../render.js';
|
||||
import { define_property, is_array } from '../../utils.js';
|
||||
|
||||
/**
|
||||
* @param {string} event_name
|
||||
* @param {Element} dom
|
||||
* @param {EventListener} handler
|
||||
* @param {boolean} capture
|
||||
* @param {boolean} [passive]
|
||||
* @returns {void}
|
||||
*/
|
||||
export function event(event_name, dom, handler, capture, passive) {
|
||||
var options = { capture, passive };
|
||||
|
||||
/**
|
||||
* @this {EventTarget}
|
||||
*/
|
||||
function target_handler(/** @type {Event} */ event) {
|
||||
handle_event_propagation(dom, event);
|
||||
if (!event.cancelBubble) {
|
||||
return handler.call(this, event);
|
||||
}
|
||||
}
|
||||
|
||||
dom.addEventListener(event_name, target_handler, options);
|
||||
|
||||
// @ts-ignore
|
||||
if (dom === document.body || dom === window || dom === document) {
|
||||
render_effect(() => {
|
||||
return () => {
|
||||
dom.removeEventListener(event_name, target_handler, options);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<string>} events
|
||||
* @returns {void}
|
||||
*/
|
||||
export function delegate(events) {
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
all_registered_events.add(events[i]);
|
||||
}
|
||||
|
||||
for (var fn of root_event_handles) {
|
||||
fn(events);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Node} handler_element
|
||||
* @param {Event} event
|
||||
* @returns {void}
|
||||
*/
|
||||
export function handle_event_propagation(handler_element, event) {
|
||||
var owner_document = handler_element.ownerDocument;
|
||||
var event_name = event.type;
|
||||
var path = event.composedPath?.() || [];
|
||||
var current_target = /** @type {null | Element} */ (path[0] || event.target);
|
||||
|
||||
if (event.target !== current_target) {
|
||||
define_property(event, 'target', {
|
||||
configurable: true,
|
||||
value: current_target
|
||||
});
|
||||
}
|
||||
|
||||
// composedPath contains list of nodes the event has propagated through.
|
||||
// We check __root to skip all nodes below it in case this is a
|
||||
// parent of the __root node, which indicates that there's nested
|
||||
// mounted apps. In this case we don't want to trigger events multiple times.
|
||||
var path_idx = 0;
|
||||
|
||||
// @ts-expect-error is added below
|
||||
var handled_at = event.__root;
|
||||
|
||||
if (handled_at) {
|
||||
var at_idx = path.indexOf(handled_at);
|
||||
if (
|
||||
at_idx !== -1 &&
|
||||
(handler_element === document || handler_element === /** @type {any} */ (window))
|
||||
) {
|
||||
// This is the fallback document listener or a window listener, but the event was already handled
|
||||
// -> ignore, but set handle_at to document/window so that we're resetting the event
|
||||
// chain in case someone manually dispatches the same event object again.
|
||||
// @ts-expect-error
|
||||
event.__root = handler_element;
|
||||
return;
|
||||
}
|
||||
|
||||
// We're deliberately not skipping if the index is higher, because
|
||||
// someone could create an event programmatically and emit it multiple times,
|
||||
// in which case we want to handle the whole propagation chain properly each time.
|
||||
// (this will only be a false negative if the event is dispatched multiple times and
|
||||
// the fallback document listener isn't reached in between, but that's super rare)
|
||||
var handler_idx = path.indexOf(handler_element);
|
||||
if (handler_idx === -1) {
|
||||
// handle_idx can theoretically be -1 (happened in some JSDOM testing scenarios with an event listener on the window object)
|
||||
// so guard against that, too, and assume that everything was handled at this point.
|
||||
return;
|
||||
}
|
||||
|
||||
if (at_idx <= handler_idx) {
|
||||
// +1 because at_idx is the element which was already handled, and there can only be one delegated event per element.
|
||||
// Avoids on:click and onclick on the same event resulting in onclick being fired twice.
|
||||
path_idx = at_idx + 1;
|
||||
}
|
||||
}
|
||||
|
||||
current_target = /** @type {Element} */ (path[path_idx] || event.target);
|
||||
|
||||
// Proxy currentTarget to correct target
|
||||
define_property(event, 'currentTarget', {
|
||||
configurable: true,
|
||||
get() {
|
||||
return current_target || owner_document;
|
||||
}
|
||||
});
|
||||
|
||||
while (current_target !== null) {
|
||||
/** @type {null | Element} */
|
||||
var parent_element =
|
||||
current_target.parentNode || /** @type {any} */ (current_target).host || null;
|
||||
var internal_prop_name = '__' + event_name;
|
||||
// @ts-ignore
|
||||
var delegated = current_target[internal_prop_name];
|
||||
|
||||
if (delegated !== undefined && !(/** @type {any} */ (current_target).disabled)) {
|
||||
if (is_array(delegated)) {
|
||||
var [fn, ...data] = delegated;
|
||||
fn.apply(current_target, [event, ...data]);
|
||||
} else {
|
||||
delegated.call(current_target, event);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
event.cancelBubble ||
|
||||
parent_element === handler_element ||
|
||||
current_target === handler_element
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
current_target = parent_element;
|
||||
}
|
||||
|
||||
// @ts-expect-error is used above
|
||||
event.__root = handler_element;
|
||||
// @ts-expect-error is used above
|
||||
current_target = handler_element;
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import { hydrating } from '../../hydration.js';
|
||||
import { render_effect } from '../../reactivity/effects.js';
|
||||
import { current_block } from '../../runtime.js';
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} dom
|
||||
* @param {boolean} value
|
||||
* @returns {void}
|
||||
*/
|
||||
export function autofocus(dom, value) {
|
||||
if (value) {
|
||||
const body = document.body;
|
||||
dom.autofocus = true;
|
||||
render_effect(
|
||||
() => {
|
||||
if (document.activeElement === body) {
|
||||
dom.focus();
|
||||
}
|
||||
},
|
||||
current_block,
|
||||
true,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The child of a textarea actually corresponds to the defaultValue property, so we need
|
||||
* to remove it upon hydration to avoid a bug when someone resets the form value.
|
||||
* @param {HTMLTextAreaElement} dom
|
||||
* @returns {void}
|
||||
*/
|
||||
export function remove_textarea_child(dom) {
|
||||
if (hydrating && dom.firstChild !== null) {
|
||||
dom.textContent = '';
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import { render_effect } from '../../reactivity/effects.js';
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} dom
|
||||
* @param {string} key
|
||||
* @param {string} value
|
||||
* @param {boolean} [important]
|
||||
*/
|
||||
export function style(dom, key, value, important) {
|
||||
const style = dom.style;
|
||||
const prev_value = style.getPropertyValue(key);
|
||||
if (value == null) {
|
||||
if (prev_value !== '') {
|
||||
style.removeProperty(key);
|
||||
}
|
||||
} else if (prev_value !== value) {
|
||||
style.setProperty(key, value, important ? 'important' : '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} dom
|
||||
* @param {string} key
|
||||
* @param {() => string} value
|
||||
* @param {boolean} [important]
|
||||
* @returns {void}
|
||||
*/
|
||||
export function style_effect(dom, key, value, important) {
|
||||
render_effect(() => {
|
||||
const string = value();
|
||||
style(dom, key, string, important);
|
||||
});
|
||||
}
|
Loading…
Reference in new issue