JSPM

trans-render

0.0.254
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 685
  • Score
    100M100P100Q113122F
  • License MIT

Instantiate an HTML Template

Package Exports

  • trans-render
  • trans-render/append.js
  • trans-render/appendTag.js
  • trans-render/createTemplate.js
  • trans-render/define.js
  • trans-render/hydrate.js
  • trans-render/init.js
  • trans-render/insertAdjacentTemplate.js
  • trans-render/mergeDeep.js
  • trans-render/plugins/templStamp.js
  • trans-render/prependTag.js
  • trans-render/repeat.js
  • trans-render/repeatInit.js
  • trans-render/replaceElementWithTemplate.js
  • trans-render/replaceTargetWithTag.js
  • trans-render/split.js
  • trans-render/standardPlugins.js
  • trans-render/trans-render
  • trans-render/transform.js
  • trans-render/update.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (trans-render) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

trans-render

Published on webcomponents.org

Actions Status

trans-render provides a methodical way of instantiating a template. It draws inspiration from the (least) popular features of XSLT. Like XSLT, trans-render performs transforms on elements by matching tests on elements. Whereas XSLT uses XPath for its tests, trans-render uses css path tests via the element.matches() and element.querySelector() methods. Unlike XSLT, though, the transform is defined with JavaScript, adhering to JSON-like declarative constraints as much as possible.

XSLT can take pure XML with no formatting instructions as its input. Generally speaking, the XML that XSLT acts on isn't a bunch of semantically meaningless div tags, but rather a nice semantic document, whose intrinsic structure is enough to go on, in order to formulate a "transform" that doesn't feel like a hack.

Likewise, with the advent of custom elements, the template markup will tend to be much more semantic, like XML. trans-render's usefulness grows as a result of the increasingly semantic nature of the template markup, because often the markup semantics provide enough clues on how to fill in the needed "potholes," like textContent and property setting, without the need for custom markup, like binding attributes. But trans-render is completely extensible, so it can certainly accommodate custom binding attributes by using additional, optional helper libraries.

This can leave the template markup quite pristine, but it does mean that the separation between the template and the binding instructions will tend to require looking in two places, rather than one. And if the template document structure changes, separate adjustments may be needed to keep the binding rules in sync. Much like how separate style rules often need adjusting when the document structure changes.

Advantages

By keeping the binding separate, the same template can thus be used to bind with different object structures.

Providing the binding transform in JS form inside the transform function signature has the advantage that one can benefit from TypeScript typing of Custom and Native DOM elements with no additional IDE support. As much as possible, JSON-like structures are also used, allowing most or all of the binding to remain declarative.

Another advantage of separating the binding like this is that one can insert comments, console.log's and/or breakpoints, making it easier to walk through the binding process.

For more musings on the question of what is this good for, please see the rambling section below.

NB It's come to my attention (via template discussions found here) that there are some existing libraries which have explored similar ideas:

  1. pure-js
  2. weld

Workflow

trans-render provides helper functions for either 1) cloning a template, and then walking through the DOM, applying rules in document order, or, 2) using the same syntax, applying changes on a live DOM fragment. Note that the document can grow as processing takes place (due, for example, to cloning sub templates). It's critical, therefore, that the processing occur in a logical order, and that order is down the document tree. That way it is fine to append nodes before continuing processing.

Drilling down to children

For each matching element, after modifying the node, you can instruct the processor which node(s) to consider next.

Most of the time, especially during initial development, you won't need / want to be so precise about where to go next. Generally, the pattern, as we will see, is just to define transform rules that match the HTML Template document structure pretty closely.

So, in the example we will see below, this notation:

const Transform = {
    details: {
        summary: 'Hallå'
    }
};

means "if a node has tag name 'details', then find any direct children of the details tag that has tag name 'summary', and set its textContent property to 'Hallå'." Let's show the full syntax for a minimal working example:

Syntax Example:

<template id="sourceTemplate">
    <details>
        ...
        <summary>E pluribus unum</summary>
        ...
    </details>
</template>
<div id="target"></div>
<script type="module">
    import { transform } from 'trans-render/transform.js';
    const model = {
        summaryText: 'hello'
    }
    const Transform = {
        details: {
            summary: model.summaryText
        }
    };
    transform(sourceTemplate, { Transform }, target);
</script>

Produces

<div id="target">
    <details>
        ...
        <summary>hello</summary>
        ...
    </details>
</div>

Not limited to declarative syntax

The transform example so far has been declarative (according to my definition of that subjective term). This library is striving to make the syntax as declarative as possible as more functionality is added. The goal being that ultimately much of what one needs with normal UI development could in fact be encoded into a separate JSON import.

However, bear in mind that the transform rules also provide support for 100% no-holds-barred non-declarative code:

const Transform = {
    details:{
        summary: ({target}: RenderContext<HTMLSummaryElement>) => {
            ///knock yourself out
            target.appendChild(document.body);
        }
    }
}

Template Stamping

If using trans-rendering for a web component, you will very likely find it convenient to "stamp" individual elements of interest. The quickest way to do this is via the templStamp plugin:

import {templStampSym} from 'trans-render/plugins/templStamp.js';
...
const uiRefs = {mySpecialElement: Symbol()};
const Transform = {
    ':host': [templStampSym, uiRefs]
}

This will search the template / dom fragment / matching target for all elements whose id or "part" attribute is "mySpecialElement". Only one element will get stamped for later use. In other words, stamping is only useful if you have exactly one element whose id or part is equal to "mySpecialElement."

Syntax summary

We'll be walking through a number of more complex scenarios, but for reference, the rules are available below in summary form, if you expand the section.

They will make more sense after walking through examples, and, of course, real-world practice.

Rules Summary

Terminology:

Each transform rule has a left hand side (lhs) "key" and a right hand side (rhs) expression.

For example, in:

const Transform = {
    details: {
        summary: model.summaryText
    }
};

details, summary are lhs keys of two nested transform rules. model.summaryText is a rhs expression, as is the open and closed curly braces section containing it.

In the example above, the details and summary keys are really strings, and could be equivalently written:

const Transform = {
    'details': {
        'summary': model.summaryText
    }
};

Due to the basic rules of object literals in JavaScript, keys can only be strings or ES6 symbols (or numbers, which aren't used). trans-render uses both strings and ES6 symbols as keys.

LHS Key Scenarios

  • If the key is a string that starts with a lower case letter, then it is a "css match" expression.
    • If the key starting with a lower case letter ends with the word "Part", then it maps to a css match expression: '[part="{{first part of the key before Part}}"]' [TODO]
  • If the key is a string that starts with double quote, then it is also a "css match" expression, but the css expression comes from the nearest previous sibling key which doesn't start with a double quote.
  • If the key is a string that starts with a capital letter, then it is part of a "Next Step" expression that indicates where to jump down to next in the DOM tree.
  • If the key is a string that starts with ^ then it matches if the tag name starts with the rest of the string [TODO: do we need with introduction of Part notation above?]
  • If the key is a string that ends with $ then it matches if the tag name ends with the rest of the string [TODO: do we need with introduction of Part notation above?]
  • If the key is "debug" then set a breakpoint in the code at that point.
  • If the key is an ES6 symbol, it is a shortcut to grab a reference to a DOM element previously stored either in context.host or context.cache, where context.host is a custom element instance.

CSS Match Rules

  • If the rhs expression is null or undefined, do nothing.
  • If the rhs expression evaluates to a string, then set the textContent property of matching target element to that string.
  • If the rhs expression evaluates to the boolean "false", then remove the matching elements from the DOM Tree.
  • If the rhs expression evaluates to a symbol, create a reference to the matching target element with that symbol as a key, in the ctx.host (custom element instance) or ctx.cache property.
  • If the rhs expression evaluates to a function, then
    • that function is invoked, where the context object is passed in
    • The evaluated function replaces the rhs expression.
  • If the rhs expression evaluates to an array, then
    • Arrays are treated as "tuples" for common requirements.
    • The first element of the tuple indicates what type of tuple to expect.
    • If the first element of the tuple is undefined, just ignore the rest of the array and move on.
    • If the first element of the tuple is a non-array, non HTMLTemplateElement object, then the tuple is treated as a "PEATS" tuple -- property / event / attribute / transform / symbol reference setting:
      • First optional parameter is a property object, that gets shallow-merged into the matching element (target).
        • Shallow-merging goes one level deeper with style and dataset properties.
      • Second optional parameter is an event object, that binds to events of the matching target element.
      • Third optional parameter is an attribute object, that sets the attributes. "null" values remove the attributes.
      • Fourth optional parameter is a sub-transform, which recursively performs a transform within the light children of the matching target element.
      • Fifth optional parameter is of type symbol, to allow future referencing to the matching target element.
    • If the first element of the tuple itself is an array, then the tuple represents a declarative loop associated with the array of items in the first tuple element.
      • The acronym to remember for a loop array is "ATRIUMS".
      • First element of the tuple is the array of items to loop over.
      • Second element is either:
        • A template reference that should be repeated, or
        • A tag of type string, that turns into a DOM element using document.createElement(tag)
        • A toTagOrTemplate function that returns a string or template.
          • If the function returns a string, it is used to generate a (custom element) with the name of the string.
          • If the function returns a template, it is the template that should be repeated.
      • Third optional parameter is an optional range of indexes from the item array to render [TODO].
      • Fourth optional parameter is either:
        • the init transform for each item, which recursively uses the transform syntax described here, or
        • a symbol, which is a key of ctx, and the key points to a function that returns a transform.
      • Fifth optional parameter is the update transform for each item.
      • Sixth optional parameter is metadata associated with the array we are looping over -- how to extract the identifier for each item, for example.
      • Seventh optional parameter is a symbol to allow future referencing to the matching target element.
      • If the first element of the tuple is a function, then evaluate the function, passing in the render context, and the returned value of the function replaces the first element.[TODO]
    • If the first element of the tuple is a boolean, then this represents a conditional display rule.
      • The acronym to remember for a conditional array is "CATMINTS".
      • The first element is the condition.
      • The second element is where it looks for the affirmative template.
      • The third element contains metadata instructions.
      • The fourth element is where it looks for the negative template.
      • If the first element is true, then the affirmative template is cloned into the target.
      • If the first element is false, then the negative template is cloned into the target.
      • The third element of the array allows for "metadata instructions". Currently it supports stamping the target with a "yesSymbol" and a "noSymbol", depending on the value of the condition, which later transform steps can then act on.
      • A major TODO item here is if using a conditional expression as part of an update transform, how to deal with previously cloned content, out of sync with the current value of the condition.
    • If the first element of the tuple is an ES6 symbol, then this represents a directive / plugin.
      • The syntax only does anything if:
        • that symbol, say mySymbol, is a key of the ctx object, and
        • ctx[mySymbol] is of type function.
      • If that is the case, then that function is passed ctx, and the remaining items in the tuple.
      • The benefits of a directive over an using an arrow function combined with an imperative function call are:
        • Arrow functions are a bit cumbersome
        • Arrow functions are not really as declarative (kind of a subjective call), possibly less capable of being encoded as JSON.
        • I am under some foggy assumption here that global Symbol.for([guid string])'s can be represented in JSON somehow, based on some special notation, like what is done for dates.
    • If the first element of the tuple is a string, then this is the name of a DOM tag that should be inserted.
      • The second optional element is the position of where to place the new element relative to the target -- afterEnd, beforeEnd, afterBegin, beforeBegin, or replace [TODO: no test coverage]
      • The third optional element is expected to be a PEATS object if it is defined.