diff --git a/packages/@hec.js/ui/example/lazy/include.html b/packages/@hec.js/ui/example/lazy/include.html new file mode 100644 index 0000000..9b06149 --- /dev/null +++ b/packages/@hec.js/ui/example/lazy/include.html @@ -0,0 +1 @@ +include {{ name }} \ No newline at end of file diff --git a/packages/@hec.js/ui/example/lazy/index.html b/packages/@hec.js/ui/example/lazy/index.html new file mode 100644 index 0000000..672642e --- /dev/null +++ b/packages/@hec.js/ui/example/lazy/index.html @@ -0,0 +1,39 @@ + + + + + + hec.js :: Plugins + + + + + +
The parts will lazy load...
+ +
+
Inline 1 {{ name }}
+
+ + + + + + + diff --git a/packages/@hec.js/ui/example/lazy/lazy.js b/packages/@hec.js/ui/example/lazy/lazy.js new file mode 100644 index 0000000..517a3d7 --- /dev/null +++ b/packages/@hec.js/ui/example/lazy/lazy.js @@ -0,0 +1,24 @@ +import { component, signal, templateByNode, templateByString } from '../../lib/index.js'; + +const p = { + name: 'Lazy...' +} + +const lazy = async () => { + let i = 1; + + for (const lazy of document.querySelectorAll('[hidden]')) { + await new Promise(r => setTimeout(r, 1000)); + + p.name += ` -> ${i++}`; + + lazy.removeAttribute('hidden'); + } + +} + +lazy(); + +templateByNode(document.body, p); + +component('my-test', {}, () => templateByString(new Date().toJSON())); \ No newline at end of file diff --git a/packages/@hec.js/ui/lib/src/component.js b/packages/@hec.js/ui/lib/src/component.js index 66ff145..bb43472 100644 --- a/packages/@hec.js/ui/lib/src/component.js +++ b/packages/@hec.js/ui/lib/src/component.js @@ -1,3 +1,4 @@ +import { notifyVisible } from "./notify/visible.js"; import { f, nodeProps, prop, propsOf } from "./props.js"; import { isSignal, signal } from "./signal.js"; @@ -26,7 +27,13 @@ export function component(name, props, fn) { /** @type {{ [key: string]: AbortController }} */ #aborts = {}; - connectedCallback() { + async connectedCallback() { + const hidden = this.closest('[hidden]'); + + if (this.hasAttribute('data-lazy') && hidden) { + await notifyVisible(this, hidden); + } + const shadow = this.attachShadow({ mode: 'open' }), node = fn(this.#signals); @@ -53,7 +60,7 @@ export function component(name, props, fn) { this.dispatchEvent(new CustomEvent('::mount')); } - node instanceof Promise ? node.then(append) : append(node); + append(node instanceof Promise ? await node : node); } disconnectedCallback() { diff --git a/packages/@hec.js/ui/lib/src/plugins.js b/packages/@hec.js/ui/lib/src/plugins.js index 1529be8..b000045 100644 --- a/packages/@hec.js/ui/lib/src/plugins.js +++ b/packages/@hec.js/ui/lib/src/plugins.js @@ -2,6 +2,7 @@ import { dataBindPlugin } from "./plugins/data-bind.js"; import { dataForPlugin } from "./plugins/data-for.js"; import { dataIfPlugin } from "./plugins/data-if.js"; import { dataIncludePlugin } from "./plugins/data-include.js"; +import { dataLazyPlugin } from "./plugins/data-lazy.js"; import { dataOnPlugin } from "./plugins/data-on.js"; import { dataPreloadPlugin } from "./plugins/data-preload.js"; import { dataRoutePlugin } from "./plugins/data-route.js"; @@ -9,9 +10,10 @@ import { dataRoutePlugin } from "./plugins/data-route.js"; /** * @typedef {{ * select: string, - * run: (arg0: HTMLElement, arg1: {[key: string]: any}) => void + * run: (node: HTMLElement, props: {[key: string]: any}, stopTemplate: () => void) => void * }} Plugin * + * /** @type { Plugin[] } */ export const plugins = [ dataForPlugin, @@ -20,5 +22,6 @@ export const plugins = [ dataIncludePlugin, dataPreloadPlugin, dataOnPlugin, - dataBindPlugin + dataBindPlugin, + dataLazyPlugin, ]; \ No newline at end of file diff --git a/packages/@hec.js/ui/lib/src/plugins/data-for.js b/packages/@hec.js/ui/lib/src/plugins/data-for.js index df6424b..4871450 100644 --- a/packages/@hec.js/ui/lib/src/plugins/data-for.js +++ b/packages/@hec.js/ui/lib/src/plugins/data-for.js @@ -10,7 +10,7 @@ const done = new WeakSet(); export const dataForPlugin = { select: '[data-for]', - run: (node, props) => { + run: (node, props, stopTemplate) => { if (done.has(node)) { return; @@ -62,5 +62,7 @@ export const dataForPlugin = { if (isSignal(list)) { list.subscribe({next: update}); } + + stopTemplate(); } } \ No newline at end of file diff --git a/packages/@hec.js/ui/lib/src/plugins/data-include.js b/packages/@hec.js/ui/lib/src/plugins/data-include.js index b9c17e4..0639daf 100644 --- a/packages/@hec.js/ui/lib/src/plugins/data-include.js +++ b/packages/@hec.js/ui/lib/src/plugins/data-include.js @@ -17,7 +17,7 @@ export const dataIncludePlugin = { loaded.add(node); - const hidden = node.closest('[hidden]'); + const hidden = node.hasAttribute('data-lazy') && node.closest('[hidden]'); const execute = async () => { const response = await fetch(node.dataset.include, { diff --git a/packages/@hec.js/ui/lib/src/plugins/data-lazy.js b/packages/@hec.js/ui/lib/src/plugins/data-lazy.js new file mode 100644 index 0000000..0c6d77d --- /dev/null +++ b/packages/@hec.js/ui/lib/src/plugins/data-lazy.js @@ -0,0 +1,33 @@ +import { notifyVisible } from '../notify/visible.js'; +import { templateByNode } from '../template.js'; + +const loaded = new WeakSet(); + +/** + * @type { import("../plugins.js").Plugin } + */ +export const dataLazyPlugin = { + select: '[data-lazy]', + + run: (node, props, stopTemplate) => { + + if (loaded.has(node) || !node.childNodes.length) { + return; + } + + loaded.add(node); + + const hidden = node.closest('[hidden]'); + + const execute = () => { + for (const child of node.childNodes) { + templateByNode(child, props); + } + } + + if (hidden) { + notifyVisible(node, hidden).then(execute); + stopTemplate(); + } + } +} \ No newline at end of file diff --git a/packages/@hec.js/ui/lib/src/plugins/data-route.js b/packages/@hec.js/ui/lib/src/plugins/data-route.js index b2c1657..2e2f813 100644 --- a/packages/@hec.js/ui/lib/src/plugins/data-route.js +++ b/packages/@hec.js/ui/lib/src/plugins/data-route.js @@ -13,7 +13,7 @@ const joinsRoutes = (node) => { } } - return route + return route.replaceAll(/\/+/g, '/'); } /** diff --git a/packages/@hec.js/ui/lib/src/template.js b/packages/@hec.js/ui/lib/src/template.js index 40cca8c..4f94bf0 100644 --- a/packages/@hec.js/ui/lib/src/template.js +++ b/packages/@hec.js/ui/lib/src/template.js @@ -19,7 +19,7 @@ export function templateByName(name, props = {}) { } /** @type { HTMLTemplateElement } */ - let tmpl = document.querySelector(`template[data-name="${name}"]`); + let tmpl = document.querySelector(`template[id="${name}"]`); if (!tmpl) { /** @type { HTMLMetaElement } */ @@ -28,7 +28,7 @@ export function templateByName(name, props = {}) { templatesLoading[name] ??= new Promise(async (resolve) => { tmpl = document.createElement('template'); - tmpl.dataset.name = name.toString(); + tmpl.setAttribute('id', name.toString()); tmpl.innerHTML = await fetch( meta?.content?.replaceAll('[name]', name.toString()) ?? name ).then((r) => r.text()); @@ -139,7 +139,7 @@ export function templateByNode(template, props = {}) { /** @param { Node } node */ const findExpression = (node) => { - const parentNode = node.parentNode; + let stopFlag = false; if (node.nodeName == '#document-fragment') { nodeProps.set(node, props); @@ -151,25 +151,27 @@ export function templateByNode(template, props = {}) { for (const plugin of plugins) { if (node.matches(plugin.select)) { - plugin.run(node, props); + plugin.run(node, props, () => stopFlag = true); } } - if (parentNode === node.parentNode) { - const attributeNames = node.getAttributeNames(); + if (stopFlag) { + return; + } + + const attributeNames = node.getAttributeNames(); - for (const attributeName of attributeNames) { - const attribute = node.getAttribute(attributeName); - - if (attribute.includes('{{')) { - - bindExpressions(attribute, (text) => { - node.setAttribute(attributeName, text.trim().replace(/ +/, ' ')) - }); + for (const attributeName of attributeNames) { + const attribute = node.getAttribute(attributeName); - } else if (node.localName.includes('-') && props[attribute]) { - node.setAttribute(attributeName, `@parent.${attribute}`); - } + if (attribute.includes('{{')) { + + bindExpressions(attribute, (text) => { + node.setAttribute(attributeName, text.trim().replace(/ +/, ' ')) + }); + + } else if (node.localName.includes('-') && props[attribute]) { + node.setAttribute(attributeName, `@parent.${attribute}`); } } @@ -177,10 +179,8 @@ export function templateByNode(template, props = {}) { bindExpressions(node.textContent, (text) => (node.textContent = text)); } - if (parentNode === node.parentNode) { - for (const child of node.childNodes) { - findExpression(child); - } + for (const child of node.childNodes) { + findExpression(child); } };