Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implements support for interpolations inside text only nodes li… #138

Merged
merged 10 commits into from
Nov 11, 2024
29 changes: 29 additions & 0 deletions examples/attributes/attributes-element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { TemplateElement, defineElement, html, Directive, defineDirective } from '../../index.js';

const directive = defineDirective(class extends Directive {});

class AttributesElement extends TemplateElement {
properties() {
return {
text: 'Hello',
};
}

template() {
const templateResult = html`
<div foo="bar" class="before:block before:content-[&apos;Festivus&apos;]">no interpolation</div>
<div foo="${'bar'}">single attribute interpolation</div>
<div foo="${'bar'}" bar="${'baz'}">multiple attribute interpolations</div>
<div foo="${'bar'} baz ${'foo'}">multiple interpolations in single attribute</div>
<div ?true-boolean="${true}" ?false-boolean="${false}">boolean attributes</div>
<div .data=${JSON.stringify({ foo: 'bar' })} .list="${JSON.stringify(['foo', 'bar'])}">
property attributes .data and .list
</div>
<div @click="${() => console.log('clicked')}">@event listener attribute</div>
<div no-directive ${directive()}>directive interpolation</div>
`;
console.log('templateResult', templateResult.toString());
return templateResult;
}
}
defineElement('attributes-element', AttributesElement);
13 changes: 13 additions & 0 deletions examples/attributes/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.tailwindcss.com"></script>
<script type="module" src="./attributes-element.js"></script>
</head>
<body>
<div class="container mx-auto p-8 border-4 rounded-lg m-8">
<attributes-element></attributes-element>
</div>
</body>
</html>
13 changes: 13 additions & 0 deletions examples/raw-text-node/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.tailwindcss.com"></script>
<script type="module" src="./raw-text-node-element.js"></script>
</head>
<body>
<div class="container mx-auto p-8 border-4 rounded-lg m-8">
<raw-text-node-element></raw-text-node-element>
</div>
</body>
</html>
16 changes: 16 additions & 0 deletions examples/raw-text-node/raw-text-node-element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TemplateElement, defineElement, html } from '../../index.js';

class RawTextNodeElement extends TemplateElement {
properties() {
return {
text: 'Hello',
};
}

template() {
const templateResult = html`<textarea foo="${'bar'}">${this.text} bar ${'baz'}</textarea>`;
console.log('templateResult', templateResult.toString());
return templateResult;
}
}
defineElement('raw-text-node-element', RawTextNodeElement);
7 changes: 0 additions & 7 deletions src/dom-parts/AttributePart.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import { Part } from './Part.js';
* @return {(function(*): void)|*}
*/
const processBooleanAttribute = (node, name, oldValue) => {
// TODO: It would be better if the ?boolean= attribute would not be there in the first place...
node.removeAttribute(`?${name}`);
return (newValue) => {
const value = !!newValue?.valueOf() && newValue !== 'false';
if (oldValue !== value) {
Expand All @@ -23,8 +21,6 @@ const processBooleanAttribute = (node, name, oldValue) => {
* @return {(function(*): void)|*}
*/
const processPropertyAttribute = (node, name) => {
// TODO: It would be better if the .property= attribute would not be there in the first place...
node.removeAttribute(`.${name}`);
return (value) => {
node[name] = value;
};
Expand All @@ -36,9 +32,6 @@ const processPropertyAttribute = (node, name) => {
* @return {(function(*): void)|*}
*/
const processEventAttribute = (node, name) => {
// TODO: It would be better if the event attribute would not be there in the first place...
node.removeAttribute(name);

let oldValue = undefined;
let type = name.startsWith('@') ? name.slice(1) : name.toLowerCase().slice(2);

Expand Down
64 changes: 64 additions & 0 deletions src/dom-parts/RawTextNodePart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Part } from './Part.js';

/** @type {Map<Element, RawTextNodePart>} */
const rawTextNodePartsCache = new WeakMap();

/**
* For updating a node that can only be updated via node.textContent
* The nodes are: script | style | textarea | title
*/
export class RawTextNodePart extends Part {
/** @type {Node} */
node = undefined;

interpolations = 1;
values = [];
currentValueIndex = 0;
initialValue = undefined;

/**
* @param {Node} node
* @param {string} initialValue
*/
constructor(node, initialValue) {
// If we have multiple raw text node parts for the same node, it means we have multiple
// interpolations inside that node. Instead of creating a new part, we will return the same
// as before and let it defer the update until the last interpolation gets updated
const rawTextNodePart = rawTextNodePartsCache.get(node.nextElementSibling);
if (rawTextNodePart) {
rawTextNodePart.interpolations++;
node.__part = rawTextNodePart; // add Part to comment node for debugging in the browser
return rawTextNodePart;
}

super();
this.initialValue = initialValue;
node.__part = this; // add Part to comment node for debugging in the browser
this.node = node.nextElementSibling;
rawTextNodePartsCache.set(node.nextElementSibling, this);
}

/**
* @param {string} value
*/
update(value) {
// If we only have one sole interpolation, we can just apply the update
if (this.interpolations === 1) {
this.node.textContent = value;
return;
}

// Instead of applying the update immediately, we check if the part has multiple interpolations
// and store the values for each interpolation in a list
this.values[this.currentValueIndex++] = value;

// Only the last call to update (for the last interpolation) will actually trigger the update
// on the DOM processor. Here we can reset everything before the next round of updates
if (this.interpolations === this.currentValueIndex) {
this.currentValueIndex = 0;
let replaceIndex = 0;
// Note: this will coarse the values into strings, but it's probably ok since we are writing to raw text (only) nodes?!
this.node.textContent = this.initialValue.replace(/\x03/g, () => this.values[replaceIndex++]);
}
}
}
4 changes: 4 additions & 0 deletions src/dom-parts/TemplatePart.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TemplateResult } from './TemplateResult.js';
import { AttributePart } from './AttributePart.js';
import { ChildNodePart, processNodePart } from './ChildNodePart.js';
import { NodePart } from './NodePart.js';
import { RawTextNodePart } from './RawTextNodePart.js';

/** @type {Map<TemplateStringsArray, Part[]>} */
const partsCache = new WeakMap();
Expand Down Expand Up @@ -124,6 +125,9 @@ export class TemplatePart extends Part {
if (part.type === 'attribute') {
return new AttributePart(part.node, part.name, part.initialValue);
}
if (part.type === 'raw-text-node') {
return new RawTextNodePart(part.node, part.initialValue);
}
if (part.type === 'directive') {
return new NodePart(part.node, templateResult.values[index]);
}
Expand Down
Loading
Loading