This repository considers the possibility of introducing new DOM API methods to efficiently set multiple HTML element properties at once, on either a single element or a collection of elements identified with id
s.
Example:
const div = document.createElement('div');
div.applyProperties({
attributes: {
role: 'main'
},
style: {
color: 'red'
},
textContent: 'Hello, world.'
});
The above would be exactly equivalent to:
const div = document.createElement('div');
div.setAttribute('role', 'main');
div.style.color = 'red';
div.textContent = 'Hello, world.';
In either case, the result would be the same:
<div role="main" style="color: red;">Hello, world.<div>
- Generally facilitate population and manipulation of DOM from JavaScript, e.g., in Functional-Reactive Programming (FRP) architectures. Rather that introducing new capabilities, this simply formalizes very common JavaScript patterns for creating and updating DOM elements.
- Support fast updates by allowing the browser to update multiple properties and elements in a single DOM call.
- Allow web components to efficiently reflect state changes in their shadow trees.
The applyProperties
method would be added to Element.prototype
. This method applies a dictionary of properties to the indicated element.
The Element
prototype exposes several properties that expose a collection that can be modified but not set directly: attributes
, childNodes
, classList
, and (for HTMLElement
and SVGElement
) style
. Although these properties cannot be directly set (you can't write element.attributes = newAttributes
), we can define simple and useful write semantics for updating these properties in the context of the applyProperties
method.
In each case, we define the write semantics in terms of existing DOM write methods:
-
attributes
property: Sets multiple attributes at once. This takes a subdictionary in which eachname: value
is equivalent to callingsetAttribute(key, value)
. Passing a nullishvalue
acts likeremoveAttribute(key)
. Known Boolean attributes (e.g.,disabled
) are slightly different: a truthyvalue
has the effects ofsetAttribute(key, '')
, and a falsyvalue
acts likeremoveAttribute(key)
. -
childNodes
property: Sets an element'schildNodes
. This takes aNodeList
or array ofNode
elements. This is equivalent to callingremoveChild()
on any nodes not in the supplied value, then callingappendChild()
for each node in the supplied value. -
classList
property: Sets/clears multiple classes at once. This takes a subdictionary in which eachname: value
is equivalent to callingclassList.toggle(name, value)
. -
style
property: Sets multiple style values at once. This takes a subdictionary in which eachname: value
is equivalent to callingstyle[name] = value
. Attempting to updatestyle
on something other than aHTMLElement
orSVGElement
throws an exception.
Note that applyProperties
is updating the indicated properties, leaving in place any other existing element attributes, classes, or styles not specifically referenced in the dictionary:
const div = document.createElement('div');
div.classList.add('foo bar');
div.applyProperties({
classList: {
bar: false, // Removes bar
baz: true // Adds baz
}
});
div.classList.value // "foo baz"
The ability to update childNodes
facilitates construction of DOM through code that, for example, maps an array of model objects to an array of DOM elements that should be added to the DOM.
const objects = [...];
const elements = objects.map(object =>
document.createElement('div', { properties })
);
document.body.applyProperties({
childNodes: elements
});
All other property dictionary keys result in invoking the property with the corresponding name. E.g.,
element.applyProperties({ foo: 'bar' });
is equivalent to
element.foo = 'bar';
This can be used to set both native HTML element properties as well as custom element properties.
A related method, applyPropertiesById
, allows the application of properties to a set of elements identified by id
. This method would be exposed on Document
and DocumentFragment
.
<body>
<div id="foo">
<div id="bar"></div>
</div>
</body>
document.applyPropertiesById({
foo: {
style: {
color: 'red'
}
},
bar: {
textContent: 'Hello, world.'
}
});
For each key: value
in the supplied dictionary, applyPropertiesById
takes the key
as an id
, and finds the corresponding element in the relevant tree via getElementById(key)
. If the element is found, it passes the value
as a property dictionary to applyProperties(element, value)
.
The above code is exactly equivalent to:
const foo = document.getElementById('foo');
foo.style.color = 'red';
const bar = document.getElementById('bar');
bar.textContent = 'Hello, world.';
In both cases, the result is:
<body>
<div id="foo" style="color: red;">
<div id="bar">Hello, world.</div>
</div>
</body>
The use of applyPropertiesById
is particularly useful in web components that want to reflect component state in their shadow tree. E.g., when component state changes, it might invoke
const changes = {
element1: { /* changes for element1 */ },
element2: { /* changes for element2 */ },
...
}
this.shadowRoot.applyPropertiesById(changes);
It would be useful to accept the same properties dictionary as an options parameter to createElement
/createElementNS
. The example at the top of this document could be written more concisely as:
const element = document.createElement('div', {
attributes: {
role: 'main'
},
style: {
color: 'red'
},
textContent: 'Hello, world.'
});
Note: createElement
/createElementNS
currently accept an additional argument with options
, which at the moment is just the standard (but not universally supported) is
option. There are likely several ways the existing options parameter could be reconciled with the suggestion above.