ripple
Version:
Ripple is an elegant TypeScript UI framework
205 lines (190 loc) • 6.26 kB
JavaScript
/** @import { Block } from '#client' */
import { hydrating } from './hydration.js';
import { HYDRATION } from 'ripple/internal/client/hydration-enabled';
import { TEXT_NODE } from '../../../constants.js';
import { class_name } from '../../../utils/class-name.js';
/**
* Sets the text of an element whose only child is that text. The template
* leaves such an element empty, so the first write creates the text node and
* later writes update it in place; server-rendered text is adopted as is.
* The element has a text node exactly when the value last written did not
* read as empty (an empty write drops the node again), so `prev`, the value
* the render block wrote before, says whether one exists without a DOM read.
* @param {Element} element
* @param {any} value
* @param {any} prev the value of the previous write (`''` before the first)
* @returns {void}
*/
export function set_text_content(element, value, prev) {
var str = value == null ? '' : value + '';
if (HYDRATION && hydrating) {
var text = element.firstChild;
if (text === null) {
if (str !== '') {
element.textContent = str;
}
} else if (text.nodeType !== TEXT_NODE || str === '') {
element.textContent = str;
} else if (text.nodeValue !== str) {
text.nodeValue = str;
}
} else if (prev == null || prev === '') {
if (str !== '') {
element.textContent = str;
}
} else if (str === '') {
element.textContent = '';
} else {
/** @type {Text} */ (element.firstChild).nodeValue = str;
}
}
/**
* @param {Text} text
* @param {any} value
* @returns {void}
*/
export function set_text(text, value) {
var str = value == null ? '' : value + '';
// The compiled render block compares against the value it last wrote, so
// nothing is cached on the node. Only server-rendered text can already
// hold the value; a fresh template text node never does.
if (HYDRATION && hydrating && text.nodeValue === str) {
return;
}
text.nodeValue = str;
}
/**
* Writes a class the compiler proved to be a string (or nullish). The
* compiled render block compares against the class it last applied, so
* nothing is cached on the element. Removing the attribute when the value is
* only an empty string costs more than an empty className, so the class is
* removed only for a nullish value; an element that has no class does not
* receive an empty one either (the server omits an empty class attribute as
* well).
* @param {HTMLElement} dom
* @param {string | null | undefined} value
* @param {string} [hash]
* @param {boolean} [is_html]
* @returns {void}
*/
export function set_class(dom, value, hash, is_html = true) {
/** @type {string | null} */
var class_value;
if (value == null) {
class_value = hash === undefined ? null : hash;
} else {
class_value = hash ? (value === '' ? hash : value + ' ' + hash) : value;
}
if (class_value === null) {
dom.removeAttribute('class');
} else if (is_html) {
if (class_value !== '' || dom.className !== '') {
dom.className = class_value;
}
} else if (class_value !== '' || dom.getAttribute('class')) {
dom.setAttribute('class', class_value);
}
}
/**
* `set_class` for a value of any shape: arrays, objects and numbers compose
* as clsx does. Only a bundle with such a class expression carries the joiner.
* @param {HTMLElement} dom
* @param {any} value
* @param {string} [hash]
* @param {boolean} [is_html]
* @returns {void}
*/
export function set_class_value(dom, value, hash, is_html = true) {
set_class(
dom,
value == null || typeof value === 'string' ? value : class_name(value),
hash,
is_html,
);
}
/**
* @param {HTMLInputElement | HTMLProgressElement | HTMLOptionElement} element
* @param {any} value
* @returns {void}
*/
export function set_value(element, value) {
var attributes = (element.__attributes ??= {});
if (element.nodeName === 'OPTION') {
/** @type {HTMLOptionElement & { __value?: any }} */ (element).__value = value;
}
if (
attributes.value ===
(attributes.value =
// treat null and undefined the same for the initial value
value ?? undefined) ||
// `progress` elements always need their value set when it's `0`
(element.value === value && (value !== 0 || element.nodeName !== 'PROGRESS'))
) {
return;
}
element.value = value ?? '';
}
/**
* @param {HTMLInputElement} element
* @param {boolean} checked
* @returns {void}
*/
export function set_checked(element, checked) {
// Compared against the element itself rather than a record of the last
// write: no expando per checkbox, and a box the user just toggled to the
// value being set needs no write at all.
if (element.checked !== (checked = !!checked)) {
element.checked = checked;
}
}
/**
* @param {HTMLOptionElement} element
* @param {boolean} selected
* @returns {void}
*/
export function set_selected(element, selected) {
if (selected) {
// The selected option could've changed via user selection, and
// setting the value without this check would set it back.
if (!element.hasAttribute('selected')) {
element.setAttribute('selected', '');
}
} else {
element.removeAttribute('selected');
}
}
/**
* Writes an attribute the compiler proved is not a property of its element
* (`dom-setters.js` in the compiler), so no setter is looked for: `null` and
* `undefined` remove it.
* @param {Element} element
* @param {string} attribute
* @param {any} value
* @returns {void}
*/
export function set_attribute_value(element, attribute, value) {
if (value == null) {
element.removeAttribute(attribute);
} else {
element.setAttribute(attribute, value);
}
}
/**
* Writes a name the compiler proved is a settable property of its element:
* a string sets the attribute, any other value the property, and `null` or
* `undefined` remove the attribute, as `set_attribute` decides at runtime
* after discovering the element's setters.
* @param {Element} element
* @param {string} attribute
* @param {any} value
* @returns {void}
*/
export function set_property_value(element, attribute, value) {
if (value == null) {
element.removeAttribute(attribute);
} else if (typeof value !== 'string') {
/** @type {any} */ (element)[attribute] = value;
} else {
element.setAttribute(attribute, value);
}
}