Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial pass at enabling dom diffing for custom element templates con… #7

Merged
merged 10 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
registry-url: https://registry.npmjs.org/

- name: Install
run: npm ci
run: npm i

- name: Test
run: npm test
Expand All @@ -49,7 +49,7 @@ jobs:
registry-url: https://registry.npmjs.org/

- name: Install
run: npm ci
run: npm i

- name: Test
run: npm test
Expand Down
100 changes: 72 additions & 28 deletions index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,62 @@
const CustomElementMixin = (superclass) => class extends superclass {
constructor() {
super()

this.expandSlots = this.expandSlots.bind(this)
this.removeStyleTags = this.removeStyleTags.bind(this)
this.removeScriptTags = this.removeScriptTags.bind(this)
this.expandTemplate = this.expandTemplate.bind(this)
this.scrubTemplate = this.scrubTemplate.bind(this)
// Has this element been server side rendered
const enhanced = this.hasAttribute('enhanced')
this.enhanced = this.hasAttribute('enhanced')
// Expands the Custom Element with the template content
this.hasSlots = Boolean(this.template.content.querySelectorAll('slot')?.length)
this.scrubTemplate(this.template.content)
this.expandTemplate()
}

scrubTemplate(el) {
this.removeStyleTags(el)
this.removeScriptTags(el)
return el
}

expandTemplate() {
// If the Custom Element was already expanded by SSR it will have the "enhanced" attribute so do not replaceChildren
if (!this.enhanced && !this.hasSlots) {
this.replaceChildren(this.scrubTemplate(this.template.content.cloneNode(true)))
// If this Custom Element was added dynamically with JavaScript then use the template contents to expand the element
} else if (!this.enhanced && this.hasSlots) {
this.innerHTML = this.expandSlots(this.innerHTML, this.template.innerHTML)
}
}

removeScriptTags(el) {
// Removes script tags as they are already appended to the body by SSR
// TODO: If only added dynamically in the browser we need to insert the script tag after running the script transform on it. As well as handle deduplication.
el.querySelectorAll('script')
.forEach((tag) => { el.content.removeChild(tag) })
}

removeStyleTags(el) {
// Handle style tags
if (enhanced) {
if (this.enhanced) {
// Removes style tags as they are already inserted into the head by SSR
this.template.content.querySelectorAll('style')
.forEach((tag) => { this.template.content.removeChild(tag) })
el.querySelectorAll('style')
.forEach((tag) => { el.removeChild(tag) })
} else {
let tagName = this.tagName
this.template.content.querySelectorAll('style')
el.querySelectorAll('style')
.forEach((tag) => {
let sheet = this.styleTransform({ tag, tagName, scope: tag.getAttribute('scope') })
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]
this.template.content.removeChild(tag)
el.removeChild(tag)
})
}
}

// Removes script tags as they are already appended to the body by SSR
// TODO: If only added dynamically in the browser we need to insert the script tag after running the script transform on it. As well as handle deduplication.
this.template.content.querySelectorAll('script')
.forEach((tag) => { this.template.content.removeChild(tag) })

// Expands the Custom Element with the template content
const hasSlots = this.template.content.querySelectorAll('slot')?.length

// If the Custom Element was already expanded by SSR it will have the "enhanced" attribute so do not replaceChildren
// If this Custom Element was added dynamically with JavaScript then use the template contents to expand the element
if (!enhanced && !hasSlots) {
this.replaceChildren(this.template.content.cloneNode(true))
} else if (!enhanced && hasSlots) {
this.innerHTML = this.expandSlots(this)
}
removeSlotTags(el) {
el.querySelectorAll('slot')
.forEach((tag) => { el.removeChild(tag) })
}

toKebabCase(str) {
Expand All @@ -46,7 +68,9 @@ const CustomElementMixin = (superclass) => class extends superclass {
const styles = this.parseCSS(tag.textContent)

if (scope === 'global') {
return styles
const sheet = new CSSStyleSheet();
sheet.replaceSync(tag.textContent)
return sheet
}

const rules = styles.cssRules
Expand Down Expand Up @@ -110,11 +134,13 @@ const CustomElementMixin = (superclass) => class extends superclass {
}


expandSlots(here) {
expandSlots(str, templateStr) {
const fragment = document.createElement('div')
fragment.innerHTML = here.innerHTML
fragment.innerHTML = str
const template = document.createElement('template')
template.innerHTML = templateStr
fragment.attachShadow({ mode: 'open' }).appendChild(
here.template.content.cloneNode(true)
template.content.cloneNode(true)
)

const children = Array.from(fragment.childNodes)
Expand All @@ -126,10 +152,28 @@ const CustomElementMixin = (superclass) => class extends superclass {
if (slot) {
if (slot.name) {
if (!namedSlots[slot.name]) namedSlots[slot.name] = { slotNode: slot, contentToSlot: [] }
namedSlots[slot.name].contentToSlot.push(child)
if (child['dataset']) {
child.dataset.slotted = true
kristoferjoseph marked this conversation as resolved.
Show resolved Hide resolved
namedSlots[slot.name].contentToSlot.push(child)
}
else {
const wrapperSpan = document.createElement('span')
wrapperSpan.dataset.slotted = true
wrapperSpan.appendChild(child)
namedSlots[slot.name].contentToSlot.push(wrapperSpan)
}
} else {
if (!unnamedSlot["slotNode"]) unnamedSlot = { slotNode: slot, contentToSlot: [] }
unnamedSlot.contentToSlot.push(child)
if (child['dataset']) {
child.dataset.slotted = true
unnamedSlot.contentToSlot.push(child)
}
else {
const wrapperSpan = document.createElement('span')
kristoferjoseph marked this conversation as resolved.
Show resolved Hide resolved
wrapperSpan.dataset.slotted = true
wrapperSpan.appendChild(child)
unnamedSlot.contentToSlot.push(wrapperSpan)
}
}
}
})
Expand Down
Loading
Loading