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

bug/issue 170 handle support for element properties #184

Merged
merged 2 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 5 additions & 12 deletions src/dom-shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function isShadowRoot(element) {

function deepClone(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== 'object') {
return obj; // Return primitives or functions as-is
return obj;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm, are all the changes here just removing comments?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is correct!

}

if (typeof obj === 'function') {
Expand Down Expand Up @@ -90,7 +90,7 @@ class Node extends EventTarget {
const childNodes = (this.nodeName === 'template' ? this.content : this).childNodes;

if (node.parentNode) {
node.parentNode?.removeChild?.(node); // Remove from current parent
node.parentNode?.removeChild?.(node);
}

if (node.nodeName === 'template') {
Expand Down Expand Up @@ -130,24 +130,21 @@ class Node extends EventTarget {

get textContent() {
if (this.nodeName === '#text') {
return this.value || ''; // Text nodes should return their value
return this.value || '';
}

// Compute textContent for elements by concatenating text of all descendants
return this.childNodes
.map((child) => child.nodeName === '#text' ? child.value : child.textContent)
.join('');
}

set textContent(value) {
// Remove all current child nodes
this.childNodes = [];

if (value) {
// Create a single text node with the given value
const textNode = new Node();
textNode.nodeName = '#text';
textNode.value = value; // Text node content
textNode.value = value;
textNode.parentNode = this;
this.childNodes.push(textNode);
}
Expand All @@ -171,29 +168,25 @@ class Element extends Node {
return this.shadowRoot && serializableShadowRoots && this.shadowRoot.serializable ? this.shadowRoot.innerHTML : '';
}

// Serialize the content of the DocumentFragment when getting innerHTML
get innerHTML() {
const childNodes = (this.nodeName === 'template' ? this.content : this).childNodes;
return childNodes ? serialize({ childNodes }) : '';
}

set innerHTML(html) {
(this.nodeName === 'template' ? this.content : this).childNodes = getParse(html)(html).childNodes; // Replace content's child nodes
(this.nodeName === 'template' ? this.content : this).childNodes = getParse(html)(html).childNodes;
}

hasAttribute(name) {
// Modified attribute handling to work with parse5
return this.attrs.some((attr) => attr.name === name);
}

getAttribute(name) {
// Modified attribute handling to work with parse5
const attr = this.attrs.find((attr) => attr.name === name);
return attr ? attr.value : null;
}

setAttribute(name, value) {
// Modified attribute handling to work with parse5
const attr = this.attrs?.find((attr) => attr.name === name);

if (attr) {
Expand Down
7 changes: 1 addition & 6 deletions src/wcc.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ async function getTagName(moduleURL) {
}

async function initializeCustomElement(elementURL, tagName, node = {}, definitions = [], isEntry, props = {}) {
const { attrs = [], childNodes = [] } = node;

if (!tagName) {
const depth = isEntry ? 1 : 0;
Expand All @@ -161,11 +160,7 @@ async function initializeCustomElement(elementURL, tagName, node = {}, definitio
if (element) {
const elementInstance = new element(data); // eslint-disable-line new-cap

elementInstance.childNodes = childNodes;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

amazing 😊


attrs.forEach((attr) => {
elementInstance.setAttribute(attr.name, attr.value);
});
Object.assign(elementInstance, node);

await elementInstance.connectedCallback();

Expand Down
37 changes: 37 additions & 0 deletions test/cases/element-props/element-props.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Use Case
* Run wcc against a component that passes properties to a child component.
*
* User Result
* Should return the expected HTML output based on the content of the passed property.
*
* User Workspace
* src/
* index.js
* renderer.js
* components/
* prop-passer.js
* prop-receiver.js
*/

import chai from 'chai';
import { JSDOM } from 'jsdom';
import { renderToString } from '../../../src/wcc.js';

const expect = chai.expect;

describe('Run WCC For ', function () {
const LABEL = 'Custom Element w/ Element Properties';
let dom;

before(async function () {
const { html } = await renderToString(new URL('./src/index.js', import.meta.url));
dom = new JSDOM(html);
});

describe(LABEL, function () {
it('should have a prop-receiver component with a heading tag with text content equal to "bar"', function () {
expect(dom.window.document.querySelector('prop-receiver h2').textContent).to.equal('bar');
});
});
});
10 changes: 10 additions & 0 deletions test/cases/element-props/src/components/prop-passer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { html, render } from '../renderer.js';

export default class PropPasser extends HTMLElement {
connectedCallback() {
const data = { foo: 'bar' };
render(html`<prop-receiver .data=${data}></prop-receiver>`, this);
}
}

customElements.define('prop-passer', PropPasser);
9 changes: 9 additions & 0 deletions test/cases/element-props/src/components/prop-receiver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { html, render } from '../renderer.js';

export default class ProperReceiver extends HTMLElement {
connectedCallback() {
render(html`<h2>${this.data.foo}</h2>`, this);
}
}

customElements.define('prop-receiver', ProperReceiver);
10 changes: 10 additions & 0 deletions test/cases/element-props/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import './components/prop-passer.js';
import './components/prop-receiver.js';

export default class ElementProps extends HTMLElement {
connectedCallback() {
this.innerHTML = '<prop-passer></prop-passer>';
}
}

customElements.define('element-props', ElementProps);
90 changes: 90 additions & 0 deletions test/cases/element-props/src/renderer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { parseFragment } from 'parse5';

function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.floor(Math.random() * 16);
const v = c === 'x' ? r : (r % 4) + 8;
return v.toString(16);
});
}

function removeAttribute(element, attribute) {
element.attrs = element.attrs.filter((attr) => attr.name !== attribute);
}

function handlePropertyAttribute(element, attribute, value, deps) {
const propName = attribute.substring(1);
removeAttribute(element, attribute);
if (!element.props) { element.props = {}; }
element[propName] = deps[value] ?? value;
}

function buildStringFromTemplate(template) {
const { strings, values } = template;

if (!strings || !values) {
return { string: '', deps: {} };
}

const stringParts = [];
const deps = {};
let isElement = false;

strings.reduce((acc, stringAtIndex, index) => {
acc.push(stringAtIndex);

isElement =
stringAtIndex.includes('<') || stringAtIndex.includes('>')
? stringAtIndex.lastIndexOf('<') > stringAtIndex.lastIndexOf('>')
: isElement;

const valueAtIndex = values[index];

if (valueAtIndex != null) {
const isPrimitive = typeof valueAtIndex === 'string' || typeof valueAtIndex === 'number';
const valueKey = isPrimitive ? null : generateUUID() + index;
const lastPart = acc[acc.length - 1];
const needsQuotes = isElement && !lastPart.endsWith('"');
acc.push(`${needsQuotes ? '"' : ''}${valueKey !== null ? valueKey : valueAtIndex}${needsQuotes ? '"' : ''}`);

if (valueKey) {
deps[valueKey] = valueAtIndex;
}
}
return acc;
}, stringParts);

return { string: stringParts.join(''), deps };
}

function setAttributes(childNodes, deps) {
childNodes.forEach((element, index)=>{
const { attrs, nodeName } = element;
if (nodeName === '#comment') { return; }
attrs?.forEach(({ name, value }) => {
if (name.startsWith('.')) {
handlePropertyAttribute(childNodes[index], name, value, deps);
}
});
if (element.childNodes) {
setAttributes(element.childNodes, deps);
}
});
}

export function render(content, container) {
const { string, deps } = buildStringFromTemplate(content);
const parsedContent = parseFragment(string);

setAttributes(parsedContent.childNodes, deps);
const template = document.createElement('template');
template.content.childNodes = parsedContent.childNodes;
container.appendChild(template.content.cloneNode(true));
}

export const html = (strings, ...values) => {
return {
strings,
values
};
};
33 changes: 33 additions & 0 deletions test/cases/set-attribute/set-attribute.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Use Case
* Run wcc against a component which sets an attribute of a child heading.
*
* User Result
* Should return a component with a child h2 with the expected attribute and attribute value.
*
* User Workspace
* src/
* index.js
*/

import chai from 'chai';
import { JSDOM } from 'jsdom';
import { renderToString } from '../../../src/wcc.js';

const expect = chai.expect;

describe('Run WCC For ', function () {
const LABEL = 'Custom Element using setAttribute';
let dom;

before(async function () {
const { html } = await renderToString(new URL('./src/index.js', import.meta.url));
dom = new JSDOM(html);
});

describe(LABEL, function () {
it('should have a heading tag with the "foo" attribute equal to "bar"', function () {
expect(dom.window.document.querySelector('set-attribute-element h2').getAttribute('foo')).to.equal('bar');
});
});
});
10 changes: 10 additions & 0 deletions test/cases/set-attribute/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default class SetAttributeElement extends HTMLElement {

connectedCallback() {
const heading = document.createElement('h2');
heading.setAttribute('foo', 'bar');
this.appendChild(heading);
}
}

customElements.define('set-attribute-element', SetAttributeElement);
Loading