@@ -1018,6 +1018,7 @@ class TemplateEngine {
10181018 static #DEFINED_PREFIX = 'x-element-defined' ;
10191019 static #PROPERTY_PREFIX = 'x-element-property' ;
10201020 static #CONTENT_PREFIX = 'x-element-content' ;
1021+ static #ATTRIBUTE_PADDING = 6 ;
10211022
10221023 // Patterns to find special edges in original html strings.
10231024 static #OPEN_REGEX = / < [ a - z ] [ a - z 0 - 9 - ] * (? = \s ) / g;
@@ -1475,6 +1476,9 @@ class TemplateEngine {
14751476 TemplateEngine . #mapInner( node , startNode , identify , callback , value , 'repeat' ) ;
14761477 }
14771478
1479+ // Walk through each string from our tagged template function “strings” array
1480+ // in a stateful way so that we know what kind of bindings are implied at
1481+ // each interpolated value.
14781482 static #exhaustString( string , state ) {
14791483 if ( ! state . inside ) {
14801484 // We're outside the opening tag.
@@ -1502,6 +1506,29 @@ class TemplateEngine {
15021506 }
15031507 }
15041508
1509+ // Flesh out an html string from our tagged template function “strings” array
1510+ // and add special markers that we can detect later, after instantiation.
1511+ //
1512+ // E.g., the user might have passed this interpolation:
1513+ //
1514+ // <div
1515+ // id="foo-bar-baz"
1516+ // foo="${foo}"
1517+ // bar="${bar}"
1518+ // .baz="${baz}">
1519+ // ${content}
1520+ // </div>
1521+ //
1522+ // … and we would instrument it as follows:
1523+ //
1524+ // <div
1525+ // id="foo-bar-baz"
1526+ // x-element-attribute-000001="foo"
1527+ // x-element-attribute-000002="bar"
1528+ // x-element-property-000003="baz">
1529+ // <!--x-element-content-->
1530+ // </div>
1531+ //
15051532 static #createHtml( type , strings ) {
15061533 const htmlStrings = [ ] ;
15071534 const state = { inside : false , index : 0 } ;
@@ -1527,13 +1554,15 @@ class TemplateEngine {
15271554 case '??' : prefix = TemplateEngine . #DEFINED_PREFIX; syntax = 4 ; break ;
15281555 case '?' : prefix = TemplateEngine . #BOOLEAN_PREFIX; syntax = 3 ; break ;
15291556 }
1530- string = string . slice ( 0 , - syntax - attribute . length ) + `${ prefix } -${ iii } ="${ attribute } ` ;
1557+ const index = String ( iii ) . padStart ( TemplateEngine . #ATTRIBUTE_PADDING, '0' ) ;
1558+ string = string . slice ( 0 , - syntax - attribute . length ) + `${ prefix } -${ index } ="${ attribute } ` ;
15311559 } else {
15321560 // We found a match like this: html`<div .title="${value}"></div>`.
15331561 // The syntax takes up 3 characters: `.${property}="`.
15341562 const syntax = 3 ;
15351563 const prefix = TemplateEngine . #PROPERTY_PREFIX;
1536- string = string . slice ( 0 , - syntax - property . length ) + `${ prefix } -${ iii } ="${ property } ` ;
1564+ const index = String ( iii ) . padStart ( TemplateEngine . #ATTRIBUTE_PADDING, '0' ) ;
1565+ string = string . slice ( 0 , - syntax - property . length ) + `${ prefix } -${ index } ="${ property } ` ;
15371566 }
15381567 state . index = 1 ; // Accounts for an expected quote character next.
15391568 } else {
@@ -1563,8 +1592,35 @@ class TemplateEngine {
15631592 return template . content ;
15641593 }
15651594
1566- static #findLookups( node , nodeType = Node . DOCUMENT_FRAGMENT_NODE , path = [ ] ) {
1567- const lookups = [ ] ;
1595+ // Walk through our fragment that we added special markers to and collect
1596+ // paths to each future target. We use “paths” because each future instance
1597+ // will clone this fragment and so paths are all we can really cache. And,
1598+ // while we go through, clean up our bespoke markers.
1599+ // Note that we are always walking the interpolated strings and the resulting,
1600+ // instantiated DOM _in the same depth-first manner_. This means that the
1601+ // ordering is fairly reliable. The only special handling we need to do is to
1602+ // ensure that we don’t rely on the ordering of NamedNodeMap objects since
1603+ // the spec doesn’t guarantee anything there (though in practice, it would
1604+ // probably work…).
1605+ //
1606+ // For example, we walk this structure:
1607+ //
1608+ // <div
1609+ // id="foo-bar-baz"
1610+ // x-element-attribute-000001="foo"
1611+ // x-element-attribute-000002="bar"
1612+ // x-element-property-000003="baz">
1613+ // <!--x-element-content-->
1614+ // </div>
1615+ //
1616+ // And end up with this (which is ready to be injected into a container):
1617+ //
1618+ // <div id="foo-bar-baz">
1619+ // <!---->
1620+ // <!---->
1621+ // </div>
1622+ //
1623+ static #findLookups( node , nodeType = Node . DOCUMENT_FRAGMENT_NODE , lookups = [ ] , path = [ ] ) {
15681624 // @ts -ignore — TypeScript doesn’t seem to understand the nodeType param.
15691625 if ( nodeType === Node . ELEMENT_NODE ) {
15701626 // Copy the live NamedNodeMap since we need to mutate it during iteration.
@@ -1581,8 +1637,9 @@ class TemplateEngine {
15811637 ? 'defined'
15821638 : null ;
15831639 if ( type ) {
1640+ const index = Number ( name . slice ( - TemplateEngine . #ATTRIBUTE_PADDING) ) ;
15841641 const value = attribute . value ;
1585- lookups . push ( { path, type, name : value } ) ;
1642+ lookups [ index ] = { path, type, name : value } ;
15861643 node . removeAttribute ( name ) ;
15871644 }
15881645 }
@@ -1593,13 +1650,14 @@ class TemplateEngine {
15931650 node . textContent . includes ( TemplateEngine . #CONTENT_PREFIX)
15941651 ) {
15951652 throw new Error ( `Interpolation of "${ localName } " tags is not allowed.` ) ;
1596- } else if (
1597- localName === 'plaintext' ||
1598- localName === 'textarea' ||
1599- localName === 'title'
1600- ) {
1653+ } else if ( localName === 'textarea' || localName === 'title' ) {
16011654 if ( node . textContent . includes ( TemplateEngine . #CONTENT_PREFIX) ) {
1602- lookups . push ( { path, type : 'text' } ) ;
1655+ if ( node . textContent === `<!--${ TemplateEngine . #CONTENT_PREFIX} -->` ) {
1656+ node . textContent = '' ;
1657+ lookups . push ( { path, type : 'text' } ) ;
1658+ } else {
1659+ throw new Error ( `Only basic interpolation of "${ localName } " tags is allowed.` ) ;
1660+ }
16031661 }
16041662 }
16051663 } else if (
@@ -1621,13 +1679,18 @@ class TemplateEngine {
16211679 for ( const childNode of node . childNodes ) {
16221680 const childNodeType = childNode . nodeType ;
16231681 if ( childNodeType === Node . ELEMENT_NODE || Node . COMMENT_NODE ) {
1624- lookups . push ( ... TemplateEngine . #findLookups( childNode , childNodeType , [ ...path , iii ++ ] ) ) ;
1682+ TemplateEngine . #findLookups( childNode , childNodeType , lookups , [ ...path , iii ++ ] ) ;
16251683 }
16261684 }
16271685 }
1628- return lookups ;
1686+ if ( nodeType === Node . DOCUMENT_FRAGMENT_NODE ) {
1687+ return lookups ;
1688+ }
16291689 }
16301690
1691+ // After cloning our common fragment, we use the “lookups” to cache live
1692+ // references to DOM nodes so that we can surgically perform updates later in
1693+ // an efficient manner. Lookups are like directions to find our real targets.
16311694 static #findTargets( fragment , lookups ) {
16321695 const targets = [ ] ;
16331696 const cache = new Map ( ) ;
@@ -1659,6 +1722,7 @@ class TemplateEngine {
16591722 return targets ;
16601723 }
16611724
1725+ // Create and prepare a document fragment to be injected into some container.
16621726 static #ready( result ) {
16631727 if ( result . readied ) {
16641728 throw new Error ( `Unexpected re-injection of template result.` ) ;
@@ -1678,16 +1742,6 @@ class TemplateEngine {
16781742 Object . assign ( result , { fragment, entries } ) ;
16791743 }
16801744
1681- static #inject( result , node , options ) {
1682- const nodes = result . type === 'svg'
1683- ? result . fragment . firstChild . childNodes
1684- : result . fragment . childNodes ;
1685- options ?. before
1686- ? TemplateEngine . #insertAllBefore( node . parentNode , node , nodes )
1687- : TemplateEngine . #insertAllBefore( node , null , nodes ) ;
1688- result . fragment = null ;
1689- }
1690-
16911745 static #assign( result , newResult ) {
16921746 result . lastValues = result . values ;
16931747 result . values = newResult . values ;
@@ -1845,6 +1899,8 @@ class TemplateEngine {
18451899 }
18461900 }
18471901
1902+ // Bind the current values from a result by walking through each target and
1903+ // updating the DOM if things have changed.
18481904 static #commit( result ) {
18491905 result . lastValues ??= result . values . map ( ( ) => TemplateEngine . #UNSET) ;
18501906 const { entries, values, lastValues } = result ;
@@ -1862,6 +1918,18 @@ class TemplateEngine {
18621918 }
18631919 }
18641920
1921+ // Attach a document fragment into some container. Note that all the DOM in
1922+ // the fragment will already have values correctly bound.
1923+ static #inject( result , node , options ) {
1924+ const nodes = result . type === 'svg'
1925+ ? result . fragment . firstChild . childNodes
1926+ : result . fragment . childNodes ;
1927+ options ?. before
1928+ ? TemplateEngine . #insertAllBefore( node . parentNode , node , nodes )
1929+ : TemplateEngine . #insertAllBefore( node , null , nodes ) ;
1930+ result . fragment = null ;
1931+ }
1932+
18651933 static #throwUpdaterError( updater , type ) {
18661934 switch ( updater ) {
18671935 case TemplateEngine . #live:
0 commit comments