Skip to content

Commit a40a7f9

Browse files
keithamuskoddsson
authored andcommitted
add custom-element-loader
1 parent 7815333 commit a40a7f9

File tree

6 files changed

+134
-57
lines changed

6 files changed

+134
-57
lines changed

.eleventy.js

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,34 @@ const buildJS = (config = {}) => {
1313
return build({
1414
minify: process.NODE_ENV === "development" ? false : true,
1515
bundle: true,
16+
splitting: true,
1617
write: true,
18+
format: "esm",
19+
metafile: true,
1720
outdir: "_site/script",
1821
plugins: [
1922
{
2023
name: "css",
2124
setup: (plugin) => {
22-
console.log('==================>')
23-
plugin.onResolve({filter: /^.*\.css$/}, ({path, importer, resolveDir, kind}) => {
24-
return {path, namespace: 'css', pluginData: {importer, resolveDir, kind}}
25+
plugin.onResolve({ filter: /^.*\.css$/ }, ({ path, importer, resolveDir, kind }) => {
26+
return {
27+
path,
28+
namespace: "css",
29+
pluginData: { importer, resolveDir, kind },
30+
}
2531
})
26-
plugin.onLoad({filter: /^.*\.css$/, namespace: 'css'}, async (ctx) => {
27-
const {default: stringToTemplateLiteral} = await import('string-to-template-literal')
28-
let contents = await fs.readFile(path.resolve(ctx.pluginData.resolveDir, ctx.path), 'utf8')
32+
plugin.onLoad({ filter: /^.*\.css$/, namespace: "css" }, async (ctx) => {
33+
const { default: stringToTemplateLiteral } = await import("string-to-template-literal")
34+
let contents = await fs.readFile(path.resolve(ctx.pluginData.resolveDir, ctx.path), "utf8")
2935

30-
contents = `const c = new CSSStyleSheet(); c.replaceSync(${stringToTemplateLiteral(contents)}); export default c;`
36+
contents = `const c = new CSSStyleSheet(); c.replaceSync(${stringToTemplateLiteral(
37+
contents
38+
)}); export default c;`
3139

32-
return {contents, resolveDir: ctx.pluginData.resolveDir}
40+
return { contents, resolveDir: ctx.pluginData.resolveDir }
3341
})
34-
}
35-
}
42+
},
43+
},
3644
],
3745
...config,
3846
})
@@ -44,12 +52,12 @@ module.exports = (eleventyConfig) => {
4452

4553
const entryPoints = glob.sync("script/*.[tj]s")
4654
eleventyConfig.addWatchTarget("script/*.[tj]s")
47-
55+
4856
buildJS({ entryPoints })
4957

5058
eleventyConfig.on("beforeWatch", (changedFiles) => {
5159
// Run me before --watch or --serve re-runs
52-
if (changedFiles.some((watchPath) => watchPath.endsWith('.css') || entryPoints.includes(watchPath))) {
60+
if (changedFiles.some((watchPath) => watchPath.endsWith(".css") || entryPoints.includes(watchPath))) {
5361
buildJS({ entryPoints })
5462
}
5563
})

_includes/main.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
<link href="/feed.xml" rel="alternate" type="application/atom+xml" title="Web Components Guide Blog" />
3838

3939
<script type="module" defer src="/script/polyfills.js"></script>
40+
<script type="module" defer src="/script/custom-element-loader.js"></script>
4041
{% for file in script %}
4142
<script type="module" defer src="/script/{{ file }}"></script>
4243
{% endfor %}

learn/components/naming-your-components.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
---
22
title: Naming your components
33
order: 3
4-
script: ["tag-name-input.js"]
54
---
65

76
_Custom Element tag names_ must have at least one dash (`-`) in them. As such you probably want to name your element

script/custom-element-loader.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
const dynamicElements = new Map()
2+
3+
const ready = new Promise((resolve) => {
4+
if (document.readyState !== "loading") {
5+
resolve()
6+
} else {
7+
document.addEventListener("readystatechange", () => resolve(), {
8+
once: true,
9+
})
10+
}
11+
})
12+
13+
const timers = new WeakMap()
14+
function scan(node) {
15+
cancelAnimationFrame(timers.get(node) || 0)
16+
timers.set(
17+
node,
18+
requestAnimationFrame(async () => {
19+
for (const [tagName, cb] of dynamicElements) {
20+
const child = node.matches(tagName) ? node : node.querySelector(tagName)
21+
if (!(customElements.get(tagName) || child?.matches(":defined"))) {
22+
const module = await cb()
23+
let ElementConstructor
24+
if (module[Symbol.toStringTag] === "Module" && module.default) {
25+
ElementConstructor = module.default
26+
} else if ("prototype" in module && module.prototype instanceof HTMLElement) {
27+
ElementConstructor = module
28+
} else {
29+
throw new Error(`invalid module for custom element ${tagName}`)
30+
}
31+
await ready
32+
if (typeof ElementConstructor.define === "function") {
33+
ElementConstructor.define(tagName)
34+
} else {
35+
customElements.define(tagName, ElementConstructor)
36+
}
37+
}
38+
dynamicElements.delete(tagName)
39+
timers.delete(node)
40+
}
41+
})
42+
)
43+
}
44+
45+
let elementLoader
46+
export function lazyDefine(tagName, callback) {
47+
if (dynamicElements.has(tagName)) {
48+
throw new Error(`cannot define already defined element ${tagName}`)
49+
}
50+
dynamicElements.set(tagName, callback)
51+
52+
scan(document.body)
53+
54+
if (!elementLoader) {
55+
elementLoader = new MutationObserver((mutations) => {
56+
if (!dynamicElements.size) return
57+
for (const mutation of mutations) {
58+
for (const node of mutation.addedNodes) {
59+
if (node instanceof Element) scan(node)
60+
}
61+
}
62+
})
63+
elementLoader.observe(document, { subtree: true, childList: true })
64+
}
65+
}
66+
67+
lazyDefine("code-interactive", () => import("./code-interactive.js"))
68+
lazyDefine("relative-time", () => import("./relative-time.js"))
69+
lazyDefine("tag-name-input", () => import("./tag-name-input.js"))

script/relative-time.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
import "@github/relative-time-element"
1+
import RelativeTimeElement from "@github/relative-time-element/relative-time"
2+
export default RelativeTimeElement

script/tag-name-input.js

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,51 @@
1-
import { validTagName, reservedTags, PCENChar, builtInTagMap } from "eslint-plugin-custom-elements/lib/tag-names.js"
1+
import { builtInTagMap, PCENChar, reservedTags, validTagName } from "eslint-plugin-custom-elements/lib/tag-names.js"
22

33
const validChar = new RegExp(`^${PCENChar}$`)
44

5-
customElements.define(
6-
"tag-name-input",
7-
class extends HTMLInputElement {
8-
connectedCallback() {
9-
this.addEventListener("input", this)
10-
this.handleEvent()
11-
}
5+
export default class extends HTMLInputElement {
6+
static define(tag = "tag-name-input") {
7+
customElements.define(tag, this, { extends: "input" })
8+
}
9+
connectedCallback() {
10+
this.addEventListener("input", this)
11+
this.handleEvent()
12+
}
1213

13-
handleEvent() {
14-
const { value } = this
15-
this.setCustomValidity("")
16-
let hint = ""
17-
if (value) {
18-
if (/[A-Z]/.test(value)) {
19-
this.setCustomValidity(`${value} is not valid, it cannot contain capital letters`)
20-
} else if (reservedTags.has(value)) {
21-
this.setCustomValidity(`${value} is not valid, it's a reserved tag`)
22-
}
23-
if (!value.includes("-")) {
24-
this.setCustomValidity(`${value} is not valid, it must include a dash (-)`)
25-
} else if (value.startsWith("-")) {
26-
this.setCustomValidity(`${value} is not valid, it must not start with a dash (-)`)
27-
} else if (!/^[a-z]/.test(value)) {
28-
this.setCustomValidity(`${value} is not valid, it must start with a letter (a-z)`)
29-
} else if (!validTagName(value)) {
30-
const chars = new Set()
31-
for (const char of value) {
32-
if (!validChar.test(char)) chars.add(`'${char}'`)
33-
}
34-
this.setCustomValidity(`${value} is not a valid tag name, cannot contain ${[...chars].join(", ")}`)
14+
handleEvent() {
15+
const { value } = this
16+
this.setCustomValidity("")
17+
let hint = ""
18+
if (value) {
19+
if (/[A-Z]/.test(value)) {
20+
this.setCustomValidity(`${value} is not valid, it cannot contain capital letters`)
21+
} else if (reservedTags.has(value)) {
22+
this.setCustomValidity(`${value} is not valid, it's a reserved tag`)
23+
}
24+
if (!value.includes("-")) {
25+
this.setCustomValidity(`${value} is not valid, it must include a dash (-)`)
26+
} else if (value.startsWith("-")) {
27+
this.setCustomValidity(`${value} is not valid, it must not start with a dash (-)`)
28+
} else if (!/^[a-z]/.test(value)) {
29+
this.setCustomValidity(`${value} is not valid, it must start with a letter (a-z)`)
30+
} else if (!validTagName(value)) {
31+
const chars = new Set()
32+
for (const char of value) {
33+
if (!validChar.test(char)) chars.add(`'${char}'`)
3534
}
35+
this.setCustomValidity(`${value} is not a valid tag name, cannot contain ${[...chars].join(", ")}`)
36+
}
3637

37-
const parts = value.split(/-/g)
38-
for (const part in parts) {
39-
if (part in builtInTagMap) {
40-
hint = `${value} is similar to the built-in ${builtInTagMap[part]}`
41-
}
38+
const parts = value.split(/-/g)
39+
for (const part in parts) {
40+
if (part in builtInTagMap) {
41+
hint = `${value} is similar to the built-in ${builtInTagMap[part]}`
4242
}
4343
}
44-
this.reportValidity()
45-
const errorEl = this.parentElement.querySelector(".error span")
46-
errorEl.textContent = this.validationMessage
47-
const hintEl = this.parentElement.querySelector(".hint span")
48-
hintEl.textContent = hint
4944
}
50-
},
51-
{ extends: "input" }
52-
)
45+
this.reportValidity()
46+
const errorEl = this.parentElement.querySelector(".error span")
47+
errorEl.textContent = this.validationMessage
48+
const hintEl = this.parentElement.querySelector(".hint span")
49+
hintEl.textContent = hint
50+
}
51+
}

0 commit comments

Comments
 (0)