-
Notifications
You must be signed in to change notification settings - Fork 1
VIII. Directed Scoped Specifiers (DSS)
"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>