Skip to content

Commit

Permalink
Observe shadow root in controller connectedCallback
Browse files Browse the repository at this point in the history
  • Loading branch information
williammartin committed Feb 28, 2023
1 parent 83fa8cb commit 7163497
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 20 deletions.
6 changes: 5 additions & 1 deletion src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {bind, bindShadow} from './bind.js'
import {autoShadowRoot} from './auto-shadow-root.js'
import {defineObservedAttributes, initializeAttrs} from './attr.js'
import type {CustomElementClass} from './custom-element.js'
import {observe} from './lazy-define.js'

const symbol = Symbol.for('catalyst')

Expand Down Expand Up @@ -57,7 +58,10 @@ export class CatalystDelegate {
initializeAttrs(instance)
bind(instance)
connectedCallback?.call(instance)
if (instance.shadowRoot) bindShadow(instance.shadowRoot)
if (instance.shadowRoot) {
bindShadow(instance.shadowRoot)
observe(instance.shadowRoot)
}
}

disconnectedCallback(element: HTMLElement, disconnectedCallback: () => void) {
Expand Down
40 changes: 23 additions & 17 deletions src/lazy-define.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,24 @@ const strategies: Record<string, Strategy> = {
visible
}

const timers = new WeakMap<Element, number>()
function scan(node: Element) {
cancelAnimationFrame(timers.get(node) || 0)
type ElementLike = Element | Document | ShadowRoot

const timers = new WeakMap<ElementLike, number>()
function scan(element: ElementLike) {
cancelAnimationFrame(timers.get(element) || 0)
timers.set(
node,
element,
requestAnimationFrame(() => {
for (const tagName of dynamicElements.keys()) {
const child: Element | null = node.matches(tagName) ? node : node.querySelector(tagName)
const child: Element | null =
element instanceof Element && element.matches(tagName) ? element : element.querySelector(tagName)
if (customElements.get(tagName) || child) {
const strategyName = (child?.getAttribute('data-load-on') || 'ready') as keyof typeof strategies
const strategy = strategyName in strategies ? strategies[strategyName] : strategies.ready
// eslint-disable-next-line github/no-then
for (const cb of dynamicElements.get(tagName) || []) strategy(tagName).then(cb)
dynamicElements.delete(tagName)
timers.delete(node)
timers.delete(element)
}
}
})
Expand All @@ -82,17 +85,20 @@ export function lazyDefine(tagName: string, callback: () => void) {
if (!dynamicElements.has(tagName)) dynamicElements.set(tagName, new Set<() => void>())
dynamicElements.get(tagName)!.add(callback)

scan(document.body)
observe(document)
}

if (!elementLoader) {
elementLoader = new MutationObserver(mutations => {
if (!dynamicElements.size) return
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof Element) scan(node)
}
export function observe(target: ElementLike): void {
elementLoader ||= new MutationObserver(mutations => {
if (!dynamicElements.size) return
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof Element) scan(node)
}
})
elementLoader.observe(document, {subtree: true, childList: true})
}
}
})

scan(target)

elementLoader.observe(target, {subtree: true, childList: true})
}
20 changes: 19 additions & 1 deletion test/controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {expect, fixture, html} from '@open-wc/testing'
import {replace, fake} from 'sinon'
import {replace, fake, spy} from 'sinon'
import {controller} from '../src/controller.js'
import {attr} from '../src/attr.js'
import {lazyDefine} from '../src/lazy-define.js'

describe('controller', () => {
let instance
Expand Down Expand Up @@ -65,6 +66,23 @@ describe('controller', () => {
expect(instance.foo).to.have.callCount(1)
})

it('observes changes on shadowRoots', async () => {
const onDefine = spy()
lazyDefine('nested-shadow-element', onDefine)

@controller
class ControllerObserveShadowElement extends HTMLElement {
connectedCallback() {
const shadowRoot = this.attachShadow({mode: 'open'})
// eslint-disable-next-line github/unescaped-html-literal
shadowRoot.innerHTML = '<div><nested-shadow-element></nested-shadow-element></div>'
}
}
instance = await fixture<ControllerObserveShadowElement>(html`<controller-observe-shadow />`)

expect(onDefine).to.be.callCount(1)
})

it('binds auto shadowRoots', async () => {
@controller
class ControllerBindAutoShadowElement extends HTMLElement {
Expand Down
17 changes: 16 additions & 1 deletion test/lazy-define.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {expect, fixture, html} from '@open-wc/testing'
import {spy} from 'sinon'
import {lazyDefine} from '../src/lazy-define.js'
import {lazyDefine, observe} from '../src/lazy-define.js'

const animationFrame = () => new Promise<unknown>(resolve => requestAnimationFrame(resolve))

Expand Down Expand Up @@ -45,6 +45,21 @@ describe('lazyDefine', () => {

expect(onDefine).to.be.callCount(2)
})

it('lazy loads elements in shadow roots', async () => {
const onDefine = spy()
lazyDefine('nested-shadow-element', onDefine)

const el = await fixture(html` <div></div> `)
const shadowRoot = el.attachShadow({mode: 'open'})
observe(shadowRoot)
// eslint-disable-next-line github/unescaped-html-literal
shadowRoot.innerHTML = '<div><nested-shadow-element></nested-shadow-element></div>'

await animationFrame()

expect(onDefine).to.be.callCount(1)
})
})

describe('firstInteraction strategy', () => {
Expand Down

0 comments on commit 7163497

Please sign in to comment.