Skip to content

Commit

Permalink
class inheritence support with td-extends, arguments, use shadowRoot …
Browse files Browse the repository at this point in the history
…as shadowRootInit in attachShadow, build range at runtime
  • Loading branch information
JRJurman committed Jun 2, 2024
1 parent 99c1756 commit 9914eee
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 34 deletions.
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,21 @@ isolation.
### Component API
These attributes can be used to provide logic for different life cycle events of your component. They follow the
standard API for Web Components.
These attributes can be used to provide or inherit logic for different life cycle events of your component. They follow
the standard API for Web Components.
<dl>
<dt><code>td-extends="tag-name"</code></dt>
<dd>
Attribute to be used on the top-level definition tag. The class associated with the `tag-name` will be used as a parent
class when building this Web Component definition. That tag must be already registered in the `customElements` registry.
</dd>
<dt><code>td-property="propertyName"</code></dt>
<dd>
Attribute to be used on a `script` tag in your component definition. This assigned property name will be attached to the
Attribute to be used on a `script` tag in your component definition. The assigned property name will be attached to the
element as a static property, and can be useful for adding `observedAttributes`, `formAssociated`, `disableInternals`,
or `disableShadow`. You can also define custom static properties for your element.
Expand All @@ -168,6 +175,11 @@ element, and can be useful for adding to the `constructor`, or setting other Web
`connectedCallback`, `disconnectedCallback`, `adoptedCallback`, or `attributeChangedCallback`. You can also define
custom methods for your element.
> [!tip]
>
> If you are using `attributeChangedCallback` you can access the parameters (`name`, `oldValue`, and `newValue`) using
> the `arguments` keyword. See the example below to see how this works!
</dd>
</dl>
Expand Down Expand Up @@ -198,8 +210,11 @@ custom methods for your element.

<!-- when the count updates, update the template -->
<script td-method="attributeChangedCallback">
const span = this.shadowRoot.querySelector('span');
span.textContent = this.getAttribute('count');
const [name, oldValue, newValue] = arguments;
if (name === 'count') {
const span = this.shadowRoot.querySelector('span');
span.textContent = newValue;
}
</script>
</my-counter>
</template>
Expand Down
64 changes: 61 additions & 3 deletions example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ <h1>
</script>

<script td-method="attributeChangedCallback">
this.collapse.textContent = this.hasAttribute('collapsed') ? 'expand' : 'collapse';
const [name, oldValue, newValue] = arguments;
if (name === 'collapsed') {
this.collapse.textContent = this.hasAttribute('collapsed') ? 'expand' : 'collapse';
}
</script>
</callout-alert>

Expand Down Expand Up @@ -102,10 +105,61 @@ <h1>
</script>

<script td-method="attributeChangedCallback">
const span = this.shadowRoot.querySelector('span');
span.textContent = this.getAttribute('count');
const [name, oldValue, newValue] = arguments;
if (name === 'count') {
const span = this.shadowRoot.querySelector('span');
span.textContent = newValue;
}
</script>
</my-counter>

<!-- extended counter, does nothing addition -->
<my-copied-counter td-extends="my-counter"></my-copied-counter>

<!-- extended counter, changes the dom layout -->
<my-red-counter td-extends="my-counter">
<template shadowrootmode="open" shadowrootdelegatesfocus>
<style>
button {
cursor: pointer;
color: red;
}
</style>
<button>TOTAL CLICKS - <span>0</span></button>
</template>
</my-red-counter>

<!-- extended counter, goes down instead of up -->
<my-decrementing-counter td-extends="my-counter">
<script td-method="connectedCallback">
const button = this.shadowRoot.querySelector('button');
button.addEventListener('click', (event) => {
const newCount = parseInt(this.getAttribute('count')) - 1;
this.setAttribute('count', newCount);
});
</script>
</my-decrementing-counter>

<!-- button that extends an action-only interface -->
<removable-button>
<script td-method="connectedCallback">
this.button = this.shadowRoot.querySelector('button');
this.button.addEventListener('click', () => {
this.remove();
});
</script>
</removable-button>

<red-removable-button td-extends="removable-button">
<template shadowrootmode="open">
<style>
button {
color: red;
}
</style>
<button>Click Me</button>
</template>
</red-removable-button>
</template>

<script>
Expand All @@ -122,6 +176,10 @@ <h1>
<custom-title>Tram-Deco is Cool!</custom-title>
<my-counter id="a" count="0"></my-counter>
<my-counter id="b" count="12">Special</my-counter>
<my-copied-counter id="c" count="15">Copied Counter</my-copied-counter>
<my-red-counter id="d" count="10"></my-red-counter>
<my-decrementing-counter id="e" count="5">Decrementing Counter</my-decrementing-counter>
<red-removable-button id="r"></red-removable-button>
<callout-alert collapsed>
<span slot="title">Import Alert</span>
<span>The following spoiler only works on browsers that support <code>setHTMLUnsafe</code></span>
Expand Down
21 changes: 21 additions & 0 deletions example/spec.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,26 @@ describe('Tram-Deco Example Components', () => {
cy.get('spoiler-tag').shadow().find('[aria-hidden="true"]').should('exist');
cy.get('spoiler-tag').click();
cy.get('spoiler-tag').shadow().find('[aria-hidden="false"]').should('exist');

/* validate that button that implements a shadow DOM from a parent with none works as expected */
cy.get('red-removable-button#r').should('exist');
cy.get('red-removable-button#r').click();
cy.get('red-removable-button#r').should('not.exist');

/* validate that extended counters with different shadow DOM work as expected */
cy.get('my-red-counter#d').should('have.attr', 'count', '10');
cy.get('my-red-counter#d').click();
cy.get('my-red-counter#d').should('have.attr', 'count', '11');

/* THE FOLLOWING TWO TESTS DO NOT WORK IN CHROMIUM BROWSERS */
/* validate that extended counters with nothing different work as expected */
cy.get('my-copied-counter#c').should('have.attr', 'count', '15');
cy.get('my-copied-counter#c').shadow().find('button').click();
cy.get('my-copied-counter#c').should('have.attr', 'count', '16');

/* validate that extended counters with different callbacks work as expected */
cy.get('my-decrementing-counter#e').should('have.attr', 'count', '5');
cy.get('my-decrementing-counter#e').click();
cy.get('my-decrementing-counter#e').should('have.attr', 'count', '4');
});
});
2 changes: 1 addition & 1 deletion example/spoiler-tag.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

(() => {
class TramDeco{static processTemplate(e){[...e.content.children].forEach(e=>{TramDeco.define(e)})}static define(newElement){const tagName=newElement.tagName.toLowerCase(),shadowRoot=newElement.shadowRoot,{mode,delegatesFocus}=shadowRoot,range=document.createRange();range.selectNodeContents(shadowRoot);class BaseTDElement extends HTMLElement{constructor(){super(),this.attachShadow({mode:mode,delegatesFocus:delegatesFocus}),this.shadowRoot.append(range.cloneContents())}}const modifiedConstructor=newElement.querySelector('script[td-method="constructor"]');class TDElement extends BaseTDElement{constructor(){super(),eval(modifiedConstructor?.textContent||"")}}newElement.querySelectorAll("script[td-method]").forEach(lifecycleScript=>{const methodName=lifecycleScript.getAttribute("td-method");TDElement.prototype[methodName]=function(){eval(lifecycleScript.textContent)}}),newElement.querySelectorAll("script[td-property]").forEach(propertyScript=>{const propertyName=propertyScript.getAttribute("td-property");Object.defineProperty(TDElement,propertyName,{get:function(){return eval(propertyScript.textContent)}})}),customElements.define(tagName,TDElement)}}
class TramDeco{static processTemplate(e){[...e.content.children].forEach(e=>{TramDeco.define(e)})}static define(templateElement){const tagName=templateElement.tagName.toLowerCase();class BaseTDElement extends HTMLElement{constructor(e){var t;super(),e&&(this.attachShadow(e),(t=document.createRange()).selectNodeContents(e),this.shadowRoot.append(t.cloneContents()))}}const parentClassName=templateElement.getAttribute("td-extends"),parentClass=customElements.get(parentClassName)||BaseTDElement,modifiedConstructor=templateElement.querySelector('script[td-method="constructor"]');class TDElement extends parentClass{constructor(overrideShadowRoot){super(overrideShadowRoot||templateElement.shadowRoot),eval(modifiedConstructor?.textContent||"")}}templateElement.querySelectorAll("script[td-method]").forEach(lifecycleScript=>{const methodName=lifecycleScript.getAttribute("td-method");TDElement.prototype[methodName]=function(){eval(lifecycleScript.textContent)}}),templateElement.querySelectorAll("script[td-property]").forEach(propertyScript=>{const propertyName=propertyScript.getAttribute("td-property");Object.defineProperty(TDElement,propertyName,{get:function(){return eval(propertyScript.textContent)}})}),customElements.define(tagName,TDElement)}}

const importTemplate = document.createElement('template')
importTemplate.setHTMLUnsafe(`<spoiler-tag>
Expand Down
49 changes: 24 additions & 25 deletions tram-deco.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,57 @@ class TramDeco {
// function to process template tags that have component definitions
static processTemplate(template) {
// for each definition in the template, define a web component
[...template.content.children].forEach((newElement) => {
TramDeco.define(newElement);
[...template.content.children].forEach((templateElement) => {
TramDeco.define(templateElement);
});
}

static define(newElement) {
static define(templateElement) {
// set the tag name to the element name in the template
const tagName = newElement.tagName.toLowerCase();

// pull the shadow root (we expect this to have been built by DSD)
const shadowRoot = newElement.shadowRoot;

// we have to manually pull out the attributes from the shadowRoot
// since there's no native way to just clone the shadowRoot fragment -.-
// we do this using a Range document fragment
const { mode, delegatesFocus } = shadowRoot;
const range = document.createRange();
range.selectNodeContents(shadowRoot);
const tagName = templateElement.tagName.toLowerCase();

// TDElement class, which has the core functionality that all Tram-Deco
// Web Components will need
class BaseTDElement extends HTMLElement {
constructor() {
constructor(shadowRoot) {
super();

// attach the shadow root, with the options used in the DSD
this.attachShadow({ mode, delegatesFocus });
if (shadowRoot) {
// attach the shadow root, with the options used in the created declarative shadow DOM
this.attachShadow(shadowRoot);

// clone the shadow root content
this.shadowRoot.append(range.cloneContents());
// clone the shadow root content using a document range
const shadowRootRange = document.createRange();
shadowRootRange.selectNodeContents(shadowRoot);
this.shadowRoot.append(shadowRootRange.cloneContents());
}
}
}

const parentClassName = templateElement.getAttribute(`td-extends`);
const parentClass = customElements.get(parentClassName) || BaseTDElement;

// we need to pull the constructor method separately
const modifiedConstructor = newElement.querySelector(`script[td-method="constructor"]`);
class TDElement extends BaseTDElement {
constructor() {
super();
const modifiedConstructor = templateElement.querySelector(`script[td-method="constructor"]`);
class TDElement extends parentClass {
// overrideShadowRoot will be a templateElement.shadowRoot of a sub-class (if we have one)
// otherwise, we'll default to using the shadowRoot of this element.
constructor(overrideShadowRoot) {
super(overrideShadowRoot || templateElement.shadowRoot);
eval(modifiedConstructor?.textContent || '');
}
}

// pull all other script tags for methods, and add them to the prototype
newElement.querySelectorAll(`script[td-method]`).forEach((lifecycleScript) => {
templateElement.querySelectorAll(`script[td-method]`).forEach((lifecycleScript) => {
const methodName = lifecycleScript.getAttribute('td-method');
TDElement.prototype[methodName] = function () {
eval(lifecycleScript.textContent);
};
});

// pull script tags for properties, and add them to the class as getters
newElement.querySelectorAll(`script[td-property]`).forEach((propertyScript) => {
templateElement.querySelectorAll(`script[td-property]`).forEach((propertyScript) => {
const propertyName = propertyScript.getAttribute('td-property');
Object.defineProperty(TDElement, propertyName, {
get: function () {
Expand Down

0 comments on commit 9914eee

Please sign in to comment.