VIII. Dependency Scoped Specifiers (DSS) - bahrus/trans-render GitHub Wiki

Dependency Scoped Specifiers (DSS) and DSSArray's

"Dependency 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:

  1. Make it easy to describe a relationship to another DOM element "in its vicinity", including the (custom element) host containing the element without relying on manually defining unique ids.
  2. 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.
  3. It is compatible with HTML that could be emitted from template instantiation built into the browser, that adopts this proposal.

String Pattern

For the TypeScript aficionados, the structure of a DSS string is as follows:

export type ID = `#${string}`;
export type Host = `:host()`;
export type Hostish = ``;
export type Target = ID | Host | Hostish;

export type MindReadProp = ``;
export type PropPath = `?.${string}`;
export type ConstVal = `\`${string}\``;

export type asOptions = 
    | 'number'
    | 'boolean'
    | 'string' 
    | 'object'
    | 'regexp' 
    | 'urlpattern'
    | 'boolean|number'
;
export type MindReadType = ``;
export type TypeQualifier = `-as-${asOptions}`;

export type ValExpression = `${PropPath | ConstVal | MindReadProp}${TypeQualifier | MindReadType}`;

export type MindReadEvent = ``;

export type EventSpecifier = `::${string}`;

export type EventPart = MindReadEvent | EventSpecifier;

export type DSS = `${Target}${ValExpression}${EventPart}`;

By "MindRead" we mean that based on context, defaults will be chosen if not specified. For example, if the element we depend on is an input element, the "mind read" event name will be "input" and the mind read type will be "string" and the mind read prop will be "value".

What do we mean by "Hostish"?

"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:

  1. First, do a "closest" for an element with attribute "itemscope".
    1. If the itemscope attribute specifies a value, it is assumed that it is the name of a registered class or function prototype for this purpose. It ensures that a property is attached to the adorned element with name "ish" (short for itemscope host), that is an instance of the registered class or function prototype. If no such instance is found, it instantiates such an instance.
    2. Alternatively, assuming still that a closest itemscope attributed element is found, ff 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.
  2. 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 --

  1. The value of the elements we are comparing are primitive JS types that are either inferrable, or specified by a property path.
  2. The values of the elements we are comparing change in conjunction with a (user-initiated) event.

DSSArray's

We can combine multiple "atomic" DSS's, as described above, which don't contain any spaces into a "molecule" of DSS expressions via the optional " and " separator. The " and " separator is optional, meaning a single space will also result in an array of DSS's.

Overcoming ID-phobia

DSS builds on top of a powerful auto generated id engine, a kind of polyfill based on this proposal. Some significant differences to that proposal, based on the limitations of what polyfills can provide:

  1. The indicator that we should auto generate id's is not applied at the parent level, but rather it is better to adorn the last streamed element with the indicator.
  2. The indicator is "-id" rather than the "generatedids" attribute as in the current proposal.
  3. The proposal advocates allowing scoped generation at all levels of the DOM. Due to the 2. deviation, the polyfill's scoping is more limited. Scoping is done via the fieldset tag, which allows for good disabled support, as well as the itemscope attribute, which allows for good donut hole scoping (at least semantically). If neither of these two scoping elements is found containing the element with the -id attribute, scoping is done from the root node (i.e. ShadowRoot/document).
  4. Rather than using id={{myName}} we need to use data-id={{myName}}, so as not to set connections that are broken while waiting for resources to download.
  5. To set id's within an element enhancement attribute, the enhancement must use defer-[base], where base is the base attribute of the enhancement, so as to prevent the enhancement from activating too early, and also as a way of simplifying the logic for attributes to search.

The auto generated polyfill explainer provides detailed examples of the rules the polyfill follows

By Example

DSS is used throughout many of the components / enhancements built upon this package. The best way to explain this lingua franca is by example.

fetch-for

The fetch-for web-component uses DSS extensively:

<input data-id="{{op}}" value=integrate>
<input data-id="{{expr}}" value=x^2>
<fetch-for defer-hydration
    for="#{{op}} and #{{expr}}"
    onInput="
        event.href=`https://newton.now.sh/api/v2/${event.forData.op.value}/${event.forData.expr.value}`
    "
    target=#{{json-viewer}}?.object
    onerror=console.error(href)
>
</fetch-for>
...
<json-viewer -id #></json-viewer>

Conditional display with be-switched

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>

This is also an example where a lot of "mind reading" is going on.

Specifying events

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>

<= Custom Element Manifesting => Uniform Storage Path

⚠️ **GitHub.com Fallback** ⚠️