Created
October 6, 2024 20:20
-
-
Save oscarmarina/63c161ac4ab72d23fa7bb063b9bdf15f to your computer and use it in GitHub Desktop.
Deeply inspired by Konnor Rogers' approach to - https://www.konnorrogers.com/posts/2024/making-lit-components-morphable
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<title>Demo - morphable-element</title> | |
<meta charset="utf-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1" /> | |
<style> | |
:root { | |
font-family: sans-serif; | |
} | |
section { | |
max-width: 50rem; | |
margin: 1rem auto; | |
} | |
section div { | |
display: grid; | |
grid-template-columns: repeat(2, minmax(12.5rem, 1fr)); | |
gap: 0.5rem; | |
} | |
</style> | |
</head> | |
<body> | |
<section> | |
<p>Default</p> | |
<div> | |
<morphable-element count="5" active message="hello"> | |
<code></code> | |
</morphable-element> | |
<morphable-element message="hello123"> | |
<code></code> | |
</morphable-element> | |
</div> | |
</section> | |
<section> | |
<p>Extend</p> | |
<div> | |
<morphable-element-extend><code></code></morphable-element-extend> | |
<morphable-element-extend count="10"><code></code></morphable-element-extend> | |
</div> | |
</section> | |
<script type="module" src="./morphable-element.js"></script> | |
<script type="module" src="./morphable-element-extend.js"></script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {MorphableElement} from './morphable-element.js'; | |
export class MorphableElementExtend extends MorphableElement { | |
constructor() { | |
super(); | |
this.message = undefined; | |
this.count = undefined; | |
} | |
} | |
window.customElements.define('morphable-element-extend', MorphableElementExtend); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {css} from 'lit'; | |
export const styles = css` | |
:host { | |
--spacing: 8px; | |
display: flex; | |
flex-direction: column; | |
align-items: start; | |
gap: var(--spacing); | |
padding: var(--spacing); | |
border-radius: calc(var(--spacing) / 2); | |
border: 1px dashed rgb(204, 204, 204); | |
margin-bottom: 1rem; | |
background-color: rgba(204, 204, 204, 0.2); | |
} | |
:host([hidden]), | |
[hidden] { | |
display: none !important; | |
} | |
*, | |
*::before, | |
*::after { | |
box-sizing: inherit; | |
} | |
button { | |
vertical-align: middle; | |
padding: 0.4em; | |
} | |
`; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {html, LitElement} from 'lit'; | |
import {until} from 'lit/directives/until.js'; | |
import {MorphableMixin} from './Morphable.js'; | |
import {styles} from './morphable-element-styles.css.js'; | |
export class MorphableElement extends MorphableMixin(LitElement) { | |
static styles = styles; | |
static properties = { | |
message: {type: String, reflect: true}, | |
count: {type: Number, reflect: true}, | |
active: {type: Boolean, reflect: true}, | |
}; | |
constructor() { | |
super(); | |
this.message = 'Hiya'; | |
this.count = 11; | |
this.active = false; | |
} | |
connectedCallback() { | |
super.connectedCallback?.(); | |
this.codeNode = this.querySelector('code'); | |
} | |
render() { | |
return html` | |
<div><b>Change Properties</b></div> | |
<div> | |
message: | |
<input | |
.value=${this.message != null ? this.message : ''} | |
@input="${({target}) => { | |
this.message = target.value; | |
}}" /> | |
</div> | |
<div> | |
active: | |
<input | |
type="checkbox" | |
.checked=${this.active} | |
@input="${({target}) => { | |
this.active = target.checked; | |
}}" /> | |
</div> | |
<div> | |
count: | |
<button | |
@click=${() => { | |
this.count = (this.count ?? 0) + 1; | |
}}> | |
${this.count} | |
</button> | |
</div> | |
<div><b>Update Attributes</b></div> | |
<div> | |
<button | |
@click="${() => { | |
if (this.count != null) { | |
this.setAttribute('count', String(this.count)); | |
} | |
if (this.message != null) { | |
this.setAttribute('message', this.message); | |
} | |
if (this.active) { | |
this.setAttribute('active', ''); | |
} | |
}}"> | |
set | |
</button> | |
<button | |
@click="${() => { | |
this.removeAttribute('message'); | |
this.removeAttribute('count'); | |
this.removeAttribute('active'); | |
}}"> | |
remove | |
</button> | |
</div> | |
<div><b>Results</b></div> | |
<ul> | |
<li> | |
message: attr = ${until(this.updateComplete.then(() => this.getAttribute('message')))}, | |
prop = ${this.message} | |
</li> | |
<li> | |
count: attr = ${until(this.updateComplete.then(() => this.getAttribute('count')))}, prop = | |
${this.count} | |
</li> | |
<li> | |
active: attr = ${until(this.updateComplete.then(() => this.getAttribute('active')))}, prop | |
= ${this.active} | |
</li> | |
</ul> | |
<slot></slot> | |
`; | |
} | |
updated() { | |
if (this.codeNode) { | |
this.codeNode.textContent = ''; | |
this.codeNode.textContent = this.outerHTML; | |
} | |
} | |
} | |
window.customElements.define('morphable-element', MorphableElement); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {dedupeMixin} from '@open-wc/dedupe-mixin'; | |
/** | |
*  | |
* | |
* Deeply inspired by Konnor Rogers' approach to [making Lit components morphable](https://www.konnorrogers.com/posts/2024/making-lit-components-morphable). | |
* | |
* This mixin ensures that the property is reset to its initial value when the attribute is removed. | |
* It applies to properties that have `reflect: true` and works even with those initialized as undefined, null, or false. | |
* | |
* ## Key Use Cases: | |
* | |
* 1. **Attributes**: The `attributeChangedCallback` is called first: | |
* | |
* ```html | |
* <morphable-element message="Hello" active count="5">some light-dom</morphable-element> | |
* ``` | |
* | |
* 2. **No Attributes - properties with values**: The `attributeChangedCallback` is called first: | |
* | |
* ```html | |
* <morphable-element>some light-dom</morphable-element> | |
* ``` | |
* | |
* ```js | |
* // Initial properties: | |
* constructor() { | |
* super(); | |
* this.message = 'Hiya'; | |
* this.count = 11; | |
* this.active = false; | |
* } | |
* ``` | |
* | |
* 3. **No attributes, properties are undefined**: The `connectedCallback` is called first: | |
* | |
* ```html | |
* <morphable-element>some light-dom</morphable-element> | |
* ``` | |
* | |
* ```js | |
* // Initial properties: | |
* constructor() { | |
* super(); | |
* this.message = undefined; | |
* this.count = undefined; | |
* this.active = false; | |
* } | |
* ``` | |
* | |
* 4. **Direct property setting**: If the property is set directly rather than through an attribute, such as: | |
* | |
* ```html | |
* <morphable-element .message="${message}" .active="${false}">some light-dom</morphable-element> | |
* ``` | |
* | |
* There is no straightforward way to determine the value used in the constructor. This is because | |
* the property is set directly on the instance after it has been constructed, bypassing the attribute | |
* reflection mechanism. | |
*/ | |
const MorphableBase = (Base) => | |
class Morphable extends Base { | |
#initialProperties = new Map(); | |
#setupInitialProperties() { | |
const {elementProperties} = this.constructor; | |
elementProperties.forEach((v, k) => { | |
if (v.reflect === true) { | |
const initialProperty = { | |
...v, | |
initialValue: this[k], | |
}; | |
this.#initialProperties.set(k, initialProperty); | |
} | |
}); | |
} | |
#ensureInitialProperties() { | |
if (!this.#initialProperties.size) { | |
this.#setupInitialProperties(); | |
} | |
} | |
connectedCallback() { | |
this.#ensureInitialProperties(); | |
super.connectedCallback?.(); | |
} | |
/** | |
* @param {string} name | |
* @param {string|null} old | |
* @param {string|null} value | |
*/ | |
async attributeChangedCallback(name, old, value) { | |
this.#ensureInitialProperties(); | |
if (value != null) { | |
super.attributeChangedCallback?.(name, old, value); | |
return; | |
} | |
const propertyDetails = this.#initialProperties.get(name) ?? {}; | |
const {initialValue, type} = propertyDetails; | |
if (!(type === Boolean && !this[name])) { | |
await this.updateComplete; | |
if (initialValue != null && initialValue !== false) { | |
this.setAttribute(name, initialValue); | |
} else { | |
this[name] = initialValue; | |
} | |
} else { | |
super.attributeChangedCallback?.(name, old, value); | |
} | |
} | |
}; | |
export const MorphableMixin = dedupeMixin(MorphableBase); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"type": "module", | |
"dependencies": { | |
"@open-wc/dedupe-mixin": "^1.4.0", | |
"lit": "^3.2.0", | |
"@lit/reactive-element": "^2.0.4", | |
"lit-element": "^4.1.0", | |
"lit-html": "^3.2.0" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"files": { | |
"Morphable.js": { | |
"position": 0 | |
}, | |
"index.html": { | |
"position": 1 | |
}, | |
"morphable-element.js": { | |
"position": 2 | |
}, | |
"morphable-element-extend.js": { | |
"position": 3 | |
}, | |
"morphable-element-styles.css.js": { | |
"position": 4, | |
"hidden": true | |
}, | |
"package.json": { | |
"position": 5, | |
"hidden": true | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment