VIII. Directed Scoped Specifiers (DSS) - bahrus/trans-render GitHub Wiki
"Directed Scoped Specifiers" (DSS) is a string pattern specification that is inspired by CSS selectors, but whose goal is far more targeted: It provides a syntax to:
- Make it easy to describe a relationship to another DOM element "in its vicinity", including the (custom element) host containing the element.
- Included in that information can be highly useful information including the name of the property to bind to, and the event name to listen for.
- It is compatible with HTML that could be emitted from template instantiation built into the browser, that adopts this proposal.
- It nudges the developer to name things in a way that will be semantically meaningful.
At the core of DSS are some some special symbols used in order to keep the statements small.
Symbol | Meaning | Notes |
---|---|---|
/propName | "Hostish" | Attaches listeners to "propagator" EventTarget. |
@propName | Name attribute | Listens for input events by default. |
|propName | Itemprop attribute | If contenteditible, listens for input events by default. Otherwise, uses be-value-added. |
#propName | Id attribute | Listens for input events by default. |
%propName | first match based on part attribute | Listens for input events by default. |
%%propName | all matches based on part attribute | [TODO] |
-prop-name | Marker indicates prop | Attaches listeners to "propagator" EventTarget. |
~elementName | match based on element name | Listens for input events by default. |
$0 | adorned element | |
::eventName | name of event to listen for | |
: | optional chaining operator |
"Host" is a generic term used to represent the custom element that "contains" the element in question. But that element in question may be inside a ShadowDOM element, or it might not be. We may even want to be able to easily switch back and forth between using ShadowDOM and not using ShadowDOM, without having to re-architect everything. But if there is no Shadow Root, how do we identify which element to look for? Here's the approach we take:
-
First, do a "closest" for an element with attribute "itemscope".
- If found element's tag name has a dash in it, that's basically what we are looking for. Wait for the element to upgrade and return that.
- Alternatively, assuming still that a closest itemscope attributed element is found, if the itemscope attribute specifies a value, it is assumed that it will be a valid custom element name. It awaits customElements.whenDefined([name specified by itemscope attribute]). Once that happens, it ensures that a property is attached to the adorned element with name "ish" (short for itemscope host), that is an instance of the custom element. If no such instance is found, it instantiates such an instance via document.createElement, and it attaches the instance to the adorned element via oAdornedElement.ish = oCustomElement in a gingerly fashion. It calls and awaits method attachedCallback. The custom element may choose to add itself to the DOM somewhere, but the algorithm described here doesn't do that.
-
If no closest itemscope match is found, use getRootNode().host.
We are often (but not always in the case of 2. below) making some assumptions about the elements we are comparing --
- The value of the elements we are comparing are primitive JS types that are either inferrable, or specified by a property path.
- The values of the elements we are comparing change in conjunction with a (user-initiated) event.
Symbol | Meaning | Notes |
---|---|---|
^{...} | Single closest match | |
^^{...} | Recursive closest match | Keeps going up a level until it finds a matching DSS fulfilling element inside [TODO] |
^{(...)} | UpSearch | Checks previous siblings as well as parent, previous elements of parent, etc. |
Y{...} | Single file downward match. | Doesn't check inside each downward element [TODO] |
Y{(...)} | Thorough downward match. | Checks stuff inside each downward element [TODO] |
|{} | Sibling matches | [TODO] |
%[aria-rowindex] | "Modulo" row index | Find closest ancestor with aria-rowindex attribute. Finds other elements within closest table or role=grid, or element with aria-totalrows, etc with the same aria-rowindex |
We can combine multiple "atomic" DSS's, as described above, which generally don't contain any spaces, into a "molecule" of DSS expressions via some key words that do have spaces around them. We call such expressions "DSSArray's".
The key words are:
Keyword | Meaning | Notes |
---|---|---|
and | Allows for an array of specifiers | The word is actually optional |
as | Specify type conversion | number | boolean | \string | \regex | \url | object |
w/i | Within |
DSS is used throughout many of the components / enhancements built upon this package. The best way to explain this lingua franca is by example
The fetch-for web-component uses DSS extensively:
<input name=op value=integrate>
<input name=expr value=x^2>
<fetch-for
for="@op and @expr"
onInput="
event.href=`https://newton.now.sh/api/v2/${event.forData.op.value}/${event.forData.expr.value}`
"
target=-object
onerror=console.error(href)
>
</fetch-for>
...
<json-viewer -object></json-viewer>
@op and @expr is saying "find elements within the nearest "form" element, or rootNode with name attributes "op" and "expr". Leave whatever default events and approaches of extracting the value from these elements up to the individual library to determine, that is outside the scope of DSS".
Likewise, the marker "-object" is saying "find element with attribute -object" and pass whatever this library wants to pass to it (say myStuff), via the local property oJsonViewer.object = myStuff".
be-switched is a custom enhancement that can lazy load HTML content when conditions are met. It uses DSS syntax to specify dependencies on nearby elements (or the host). For example:
<label for=lhs>LHS:</label>
<input id=lhs>
<label for=rhs>RHS:</label>
<input id=rhs>
<template be-switched='on when #lhs equals #rhs.'>
<div>LHS === RHS</div>
</template>
To specify more nuanced locations, use the "upstream" ^ operator and w/i keyword:
These should be ignored:
<div>
<label for=lhs>LHS:</label>
<input id=lhs name=lhs>
<label for=rhs>RHS:</label>
<input id=rhs name=rhs>
</div>
These should be active:
<section>
<label>
LHS:
<input name=lhs>
</label>
<label>RHS:
<input name=rhs>
</label>
<template be-switched="on when @lhs eq @rhs w/i ^{section}.">
<div>LHS === RHS</div>
</template>
</section>
We can also specify "modulo" upstream queries, tied to the aria-rowindex attribute:
<table>
<tbody>
<tr aria-rowindex=10><td><input name=lhs></td></tr>
<tr aria-rowindex=10>
<td>
<template 🎚️="on when @lhs eq @rhs w/i %[aria-rowindex].">
<div>lhs == rhs</div>
</template>
</td>
</tr>
<tr aria-rowindex=10><td><input name=rhs></td></tr>
<tr aria-rowindex=11><td><input name=lhs></td></tr>
<tr aria-rowindex=11>
<td>
<template 🎚️="on when @lhs eq @rhs w/i %[aria-rowindex].">
<div>lhs == rhs</div>
</template>
</td>
</tr>
<tr aria-rowindex=11><td><input name=rhs></td></tr>
</tbody>
</table>
be-bound has an example where we specify the property name, and the event name to listen for:
<input id=alternativeRating type=number>
<form be-bound='between rating:value::change and #alternativeRating.'>
<div part=rating-stars class="rating__stars">
<input id="rating-1" class="rating__input rating__input-1" type="radio" name="rating" value="1">
<input id="rating-2" class="rating__input rating__input-2" type="radio" name="rating" value="2">
<input id="rating-3" class="rating__input rating__input-3" type="radio" name="rating" value="3">
<input id="rating-4" class="rating__input rating__input-4" type="radio" name="rating" value="4">
<input id="rating-5" class="rating__input rating__input-5" type="radio" name="rating" value="5">
</div>
</form>
We finish our dissertation with a rather dull topic. This package contains some utilities that help manage the peculiarities of the DOM.
Many DOM elements have a concept of having an intrinsic parsed (class instance) value, a corresponding string "value" and a "text display". For example, the hyperlink's "value" is the href, and the text display is the text, and we may want to work with the URL object associated with it.
The time tag has a datetime string property for the ISO value, and the textContent that should display to the user. And if working with the time tag in coordination with a Date object, we would probably want to associate the actual Date Object somewhere. So we have no fewer than three key things we need to juggle associated with time element if that is what we are targeting.
The DOM ASMR attempts to create a uniform interface to work with across the framework of components and enhancements that build on this package.
So if we are sharing values from the host custom element, say, to these DOM elements we can use the interface:
const sharingObj = ASMR.getSO(oTimeElement, [options]);
sharingObj.setValue(oTimeElement, new Date());
and the Sharing Obj worries about all the "under the hood" things that should happen with the time element -- in this case set the datetime attribute to the ISO string, and the textContent to the locale specific text. We are guessing what the developer wants to do, hence the term "mind reading."
However, the getSO method allows for a second "options" parameter to be passed in, which can range in how detailed it can be, where the developer specifies how the setValue should go about processing the values passed in via setValue. The more specific the options object is, the less "mind reading" that needs to happen.
We need a uniform way to "upgrade" the shared object associated with the time element, based on custom attributes.
If rather than sharing, we want to go in the opposite direction, and absorb changes from a remote element into our local DOM element, we need to account for default event types we would want to listen for, or property setters to observe, or attribute changes to monitor. Again, the goal of the ASMR library is to shield the developer from having to articulate explicitly how to do this while they drift off.
const absObj = ASMR.getAO(oTimeElement, [options]);
absObj.addEventListener('value', e => {
this.dteObj = absObj.getValue(oTimeElement);
})
You are now fully certified to use this library.
Good night. Sleep tight.