Skip to content

Commit

Permalink
Adds support for defer-hydration
Browse files Browse the repository at this point in the history
  • Loading branch information
zachleat committed Nov 14, 2022
1 parent e9248f9 commit fafe092
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 86 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Features:
* Easy to add to existing components
* Zero dependencies
* Not tightly coupled to a server framework or site generator tool.
* Small footprint (4.86 kB minimized; 1.57 kB with Brotli compression)
* Small footprint (5 kB minimized; 1.73 kB with Brotli compression)
* Server-rendered (SSR) component examples available for SSR-friendly frameworks (Lit, Svelte, Vue, Preact are provided)

Examples for:
Expand Down
62 changes: 62 additions & 0 deletions demo-defer-hydration.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
layout: layout.html
title: Islands
---
<h1><a href="/">is-land</a></h1>

<p>Related to <a href="/issues/13">the test case for GitHub issue #13</a>.</p>

<p><code>defer-hydration</code> solves the problem of <code>&lt;chocolate-web-component&gt;</code> elements outside of <code>is-land</code>s hydrating when <code>chocolate-web-component.js</code> is loaded.</p>

<chocolate-web-component defer-hydration class="demo-component">
<template shadowroot="open">Chocolate shadow root</template>
Chocolate web component
</chocolate-web-component>

<script src="/lib/chocolate-web-component.js" type="module"></script>

<is-land on:interaction import="/lib/chocolate-web-component.js">
<button type="button" onclick="console.log('Click test')">Click test (log to console)</button>
<p>Using <code>defer-hydration</code>.</p>

<chocolate-web-component defer-hydration class="demo-component">
<template shadowroot="open">Chocolate shadow root</template>
Chocolate web component
</chocolate-web-component>
</is-land>

<is-land on:interaction import="/lib/chocolate-web-component.js">
<button type="button" onclick="console.log('Click test')">Click test (log to console)</button>
<p>Using <code>defer-hydration</code>.</p>

<chocolate-web-component defer-hydration class="demo-component">
<template shadowroot="open">Chocolate shadow root</template>
Chocolate web component
</chocolate-web-component>
</is-land>

<!-- <is-land on:interaction import="/lib/chocolate-web-component.js">
<button type="button" onclick="console.log('Click test')">Click test (log to console)</button>
<chocolate-web-component class="demo-component">
<template shadowroot="open">Chocolate shadow root</template>
Chocolate web component
</chocolate-web-component>
</is-land>
<div style="display: block; height: 90vh"></div>
<is-land on:visible import="/lib/chocolate-web-component.js">
<button type="button" onclick="console.log('Click test')">Click test (log to console)</button>
<chocolate-web-component class="demo-component">
<template shadowroot="open">Chocolate shadow root</template>
Chocolate web component
</chocolate-web-component>
</is-land> -->

<!-- <is-land on:visible>
<button type="button" onclick="console.log('Click test')">Click test (log to console)</button>
<some-other-custom-element>Some other custom element</some-other-custom-element>
</is-land> -->
7 changes: 3 additions & 4 deletions demo-styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,9 @@ is-land[ready] {
background-color: rgba(112, 110, 233, 0.2);
outline: 2px solid rgb(97, 82, 173);
}
.test-c-finish:defined {
color: blue;
font-weight: bold;
}
/* .test-c-finish:defined {
font-style: italic;
} */

/* List logos */
.examples {
Expand Down
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ <h1><a href="/">is-land</a></h1>
<li><a href="demo-lit.html"><img src="https://v1.indieweb-avatar.11ty.dev/https%3A%2F%2Flit.dev%2F/" alt="IndieWeb avatar for https://lit.dev/" width="28" height="28" decoding="async" loading="lazy">Lit</a> (and SSR)</li>
<li><a href="demo-alpine.html"><img src="https://v1.indieweb-avatar.11ty.dev/https%3A%2F%2Falpinejs.dev%2F/" alt="IndieWeb avatar for https://alpinejs.dev/" width="28" height="28" decoding="async" loading="lazy">Alpine.js</a></li>
<li><a href="demo-markdown.html">Embedded in <img src="https://v1.indieweb-avatar.11ty.dev/https%3A%2F%2Fwww.markdownguide.org%2F/" alt="IndieWeb avatar for https://www.markdownguide.org/" width="28" height="28" decoding="async" loading="lazy">Markdown</a></li>
<li><em>Experimental:</em> <a href="demo-defer-hydration.html"><code>defer-hydration</code> Component Attribute Support</a></li>
<li><em>Experimental:</em> <a href="demo-image-loading.html">Image Loading</a></li>
<li><em>Experimental:</em> <a href="demo-importmaps.html">Using import maps to simplify import URLs</a>. This is for-future-reference when <a href="https://caniuse.com/import-maps">browser support broadens</a>.</li>
<li><em>Test:</em> <a href="demo-stress-test.html">Stress test of 10000 islands</a></li>
Expand Down
37 changes: 20 additions & 17 deletions is-land.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@ const islandOnceCache = new Map();

class Island extends HTMLElement {
static tagName = "is-land";
static prefix = "is-land--"
static prefix = "is-land--";
static attr = {
autoInitType: "autoinit",
import: "import",
template: "data-island",
ready: "ready",
defer: "defer-hydration",
};

static fallback = {
":scope:not([skip-fallback]) :not(:defined)": (readyPromise, node, prefix) => {
":scope :not(:defined):not([defer-hydration])": (readyPromise, node, prefix) => {
// remove from document to prevent web component init
let cloned = document.createElement(prefix + node.localName);
for(let attr of node.getAttributeNames()) {
Expand All @@ -22,6 +29,7 @@ class Island extends HTMLElement {
shadowroot.appendChild(tmpl.content.cloneNode(true));
}
}

// cheers to https://gist.github.com/developit/45c85e9be01e8c3f1a0ec073d600d01e
if(shadowroot) {
cloned.attachShadow({ mode: shadowroot.mode }).append(...shadowroot.childNodes);
Expand Down Expand Up @@ -63,17 +71,9 @@ class Island extends HTMLElement {
constructor() {
super();

this.attrs = {
autoInitType: "autoinit",
import: "import",
template: "data-island",
ready: "ready",
};

// Internal promises
this.ready = new Promise((resolve, reject) => {
this.ready = new Promise(resolve => {
this.readyResolve = resolve;
this.readyReject = reject;
});
}

Expand Down Expand Up @@ -145,7 +145,7 @@ class Island extends HTMLElement {
}

getTemplates() {
return this.querySelectorAll(`:scope template[${this.attrs.template}]`);
return this.querySelectorAll(`:scope template[${Island.attr.template}]`);
}

replaceTemplates(templates) {
Expand All @@ -156,7 +156,7 @@ class Island extends HTMLElement {
continue;
}

let value = node.getAttribute(this.attrs.template);
let value = node.getAttribute(Island.attr.template);
// get rid of the rest of the content on the island
if(value === "replace") {
let children = Array.from(this.childNodes);
Expand Down Expand Up @@ -202,26 +202,29 @@ class Island extends HTMLElement {

let mod;
// [dependency="my-component-code.js"]
let importScript = this.getAttribute(this.attrs.import);
let importScript = this.getAttribute(Island.attr.import);
if(importScript) {
// we could resolve import maps here manually but you’d still have to use the full URL in your script’s import anyway
mod = await import(importScript);
}

if(mod) {
// Use `import=""` for when import maps are available e.g. `import="petite-vue"`
let fn = Island.autoinit[this.getAttribute(this.attrs.autoInitType) || importScript];
let fn = Island.autoinit[this.getAttribute(Island.attr.autoInitType) || importScript];

if(fn) {
await fn.call(this, mod);
}
}

this.readyResolve({
import: mod
// import: mod
});

this.setAttribute(this.attrs.ready, "");
this.setAttribute(Island.attr.ready, "");

// Remove [defer-hydration]
this.querySelectorAll(`:scope [${Island.attr.defer}]`).forEach(node => node.removeAttribute(Island.attr.defer));
}
}

Expand Down
61 changes: 3 additions & 58 deletions issues/13.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,9 @@ <h1><a href="/">is-land</a></h1>

<p>For <a href="https://github.com/11ty/is-land/issues/13">GitHub issue #13</a>.</p>

<!-- <is-land import="/lib/details-utils.js">
<button type="button" onclick="console.log('Click test')">Click test (log to console)</button>
<p>This island will load without fallback because it’s eager (no loading conditions).</p>
<details-utils close-click-outside>
<details open>
<summary>Toggle Menu (Default open: click outside to close after island initializes)</summary>
<p>Note that interacting with content inside of the menu works without closing the menu.</p>
</details>
</details-utils>
</is-land> -->
<p>Note that the Declarative Shadow DOM polyfill will not be applied outside of `&lt;is-land&gt;`.</p>

<p>Note that if `chocolate-web-component.js` is not loaded, any `&lt;chocolate-web-component&gt;` elements outside of `&lt;is-land&gt;` will be initialized when the file is loaded. Note also that the Declarative Shadow DOM polyfill will not be applied outside of `&lt;is-land&gt;`.</p>
<p>Note that if <code>chocolate-web-component.js</code> is not available on page load, any <code>&lt;chocolate-web-component&gt;</code> elements outside of <code>&lt;is-land&gt;</code> will be initialized when the file is loaded. This is better solved by implementing <a href="/demo-defer-hydration/"><code>defer-hydration</code> in your component code</a>.</p>

<chocolate-web-component class="demo-component">
<template shadowroot="open">Chocolate shadow root</template>
Expand All @@ -33,53 +24,7 @@ <h1><a href="/">is-land</a></h1>
</chocolate-web-component>
</is-land>

<!-- <is-land on:idle import="/lib/details-utils.js">
<button type="button" onclick="console.log('Click test')">Click test (log to console)</button>
<details-utils close-click-outside>
<details open>
<summary>Toggle Menu (Default open: click outside to close after island initializes)</summary>
<p>Note that interacting with content inside of the menu works without closing the menu.</p>
</details>
</details-utils>
</is-land> -->

<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div style="display: block; height: 90vh"></div>

<is-land on:visible import="/lib/chocolate-web-component.js">
<button type="button" onclick="console.log('Click test')">Click test (log to console)</button>
Expand Down
24 changes: 18 additions & 6 deletions lib/chocolate-web-component.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
// Programmatic usage
// import { ready } from "/is-land.js";

class ChocolateWebComponent extends HTMLElement {
async connectedCallback() {
// Programmatic API: waiting to lazy load
// await ready(this);
this.init();
}

init() {
if(this.hasAttribute("defer-hydration")) {
return;
}

// ready to go
this.classList.add("test-c-finish");
}

// when this *defines* it triggers an attribute change on defer-hydration from null => ""
static get observedAttributes() {
return ["defer-hydration"];
}

attributeChangedCallback(name, previousValue, newValue) {
if(name ==="defer-hydration" && newValue === null) {
this.init();
}
}
}

if("customElements" in window) {
Expand Down

0 comments on commit fafe092

Please sign in to comment.