Skip to content

Commit

Permalink
fix(scripts): avoid using inline events for client only
Browse files Browse the repository at this point in the history
Relates to #323
  • Loading branch information
harlan-zw committed Mar 13, 2024
1 parent 3bb8a73 commit 9dac97d
Show file tree
Hide file tree
Showing 8 changed files with 55 additions and 77 deletions.
9 changes: 8 additions & 1 deletion packages/dom/src/renderDOMHead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,14 @@ export async function renderDOMHead<T extends Unhead<any>>(head: T, options: Ren
delete state.elMap[id]
})
}
// add new attributes
// we need to attach event listeners as they can have side effects such as onload
for (const [k, value] of Object.entries(tag._eventHandlers || {})) {
// avoid overriding
;(tag!.tag === 'bodyAttrs' ? dom!.defaultView! : $el)!.addEventListener(
k.replace('on', ''),
value.bind($el),
)
}
Object.entries(tag.props).forEach(([k, value]) => {
const ck = `attr:${k}`
// class attributes have their own side effects to allow for merging
Expand Down
11 changes: 10 additions & 1 deletion packages/unhead/src/composables/useScript.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { NetworkEvents, hashCode } from '@unhead/shared'
import type { DomRenderTagContext, Head, HeadEntryOptions, Script, ScriptInstance, UseScriptInput, UseScriptOptions } from '@unhead/schema'
import type {
DomRenderTagContext,
Head,
HeadEntryOptions,
Script,
ScriptInstance,
UseScriptInput,
UseScriptOptions,
UseScriptResolvedInput,
} from '@unhead/schema'
import { getActiveHead } from './useActiveHead'

const UseScriptDefaults: Script = {
Expand Down
102 changes: 32 additions & 70 deletions packages/unhead/src/plugins/eventHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,7 @@
import type { HeadTag } from '@unhead/schema'
import { NetworkEvents, defineHeadPlugin, hashCode } from '@unhead/shared'

const ValidEventTags = ['script', 'link', 'bodyAttrs']

function stripEventHandlers(tag: HeadTag) {
const props: HeadTag['props'] = {}
const eventHandlers: Record<string, (e: Event) => {}> = {}
Object.entries(tag.props)
.forEach(([key, value]) => {
if (key.startsWith('on') && typeof value === 'function') {
// insert a inline script to set the status of onload and onerror
if (NetworkEvents.includes(key))
props[key] = `this.dataset.${key} = true`
eventHandlers[key] = value
}
else { props[key] = value }
})
return { props, eventHandlers }
}

/**
* Supports DOM event handlers (i.e `onload`) as functions.
*
Expand All @@ -27,65 +10,44 @@ function stripEventHandlers(tag: HeadTag) {
export default defineHeadPlugin(head => ({
hooks: {
'tags:resolve': function (ctx) {
for (const tag of ctx.tags) {
for (const tag of ctx.tags.filter(t => ValidEventTags.includes(t.tag))) {
// must be a valid tag
if (ValidEventTags.includes(tag.tag)) {
const { props, eventHandlers } = stripEventHandlers(tag)
tag.props = props
if (Object.keys(eventHandlers).length) {
// need a key
if (tag.props.src || tag.props.href)
tag.key = tag.key || hashCode(tag.props.src || tag.props.href)
tag._eventHandlers = eventHandlers
}
Object.entries(tag.props)
.forEach(([key, value]) => {
if (key.startsWith('on') && typeof value === 'function') {
// insert a inline script to set the status of onload and onerror
if (head.ssr && NetworkEvents.includes(key)) {
tag.props[key] = `this.dataset.${key} = true`
tag.props['data-unhead-events'] = ''
}
else { delete tag.props[key] }
tag._eventHandlers = tag._eventHandlers || {}
tag._eventHandlers![key] = value
}
})
if (head.ssr && tag._eventHandlers && (tag.props.src || tag.props.href)) {
tag.key = tag.key || hashCode(tag.props.src || tag.props.href)
}
}
},
'dom:renderTag': function (ctx, dom, track) {
if (!ctx.tag._eventHandlers)
'dom:renderTag': function (ctx) {
// this is only handling SSR rendered tags with event handlers
const $el = ctx.$el as HTMLScriptElement
if (!$el?.dataset || !('unheadEvents' in $el.dataset))
return

const $eventListenerTarget: Element | Window | null | undefined = ctx.tag.tag === 'bodyAttrs' ? dom.defaultView : ctx.$el
Object.entries(ctx.tag._eventHandlers).forEach(([k, value]) => {
const sdeKey = `${ctx.tag._d || ctx.tag._p}:${k}`
const eventName = k.slice(2).toLowerCase()
const eventDedupeKey = `data-h-${eventName}`
track(ctx.id, sdeKey, () => {})
if (ctx.$el!.hasAttribute(eventDedupeKey))
return

ctx.$el!.setAttribute(eventDedupeKey, '')

let observer: MutationObserver
const handler = (e: Event) => {
value(e)
observer?.disconnect()
}
if (k in ctx.$el.dataset) {
handler(new Event(k.replace('on', '')))
}
else if (NetworkEvents.includes(k) && typeof MutationObserver !== 'undefined') {
observer = new MutationObserver((e) => {
const hasAttr = e.some(m => m.attributeName === `data-${k}`)
if (hasAttr) {
handler(new Event(k.replace('on', '')))
observer?.disconnect()
}
})
observer.observe(ctx.$el, {
attributes: true,
})
}
else {
// check if $el has the event listener
$eventListenerTarget!.addEventListener(eventName, handler)
}
track(ctx.id, sdeKey, () => {
observer?.disconnect()
$eventListenerTarget!.removeEventListener(eventName, handler)
ctx.$el!.removeAttribute(eventDedupeKey)
delete $el.dataset.unheadEvents
const handler = (k: string) => ctx.tag._eventHandlers?.[k]?.call(ctx.$el, new Event(k.replace('on', '')))
for (const k of Object.keys($el.dataset).filter(k => NetworkEvents.includes(k)))
handler(k)
if (typeof MutationObserver !== 'undefined') {
// we need to handle SSR events, as they are not triggered
const observer = new MutationObserver((e) => {
e.filter(m => m.attributeName && NetworkEvents.includes(m.attributeName!.replace('data-', '')))
.map(m => m.attributeName!.replace('data-', ''))
.map(handler)
})
})
observer.observe(ctx.$el, { attributes: true })
}
},
},
}))
2 changes: 1 addition & 1 deletion test/unhead/dom/eventHandlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('dom event handlers', () => {
expect(await useDelayedSerializedDom()).toMatchInlineSnapshot(`
"<!DOCTYPE html><html><head>
<script src="https://js.stripe.com/v3/" defer="" onload="this.dataset.onload = true" data-h-load=""></script></head>
<script src="https://js.stripe.com/v3/" defer=""></script></head>
<body>
<div>
Expand Down
2 changes: 1 addition & 1 deletion test/unhead/dom/useScript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('dom useScript', () => {
expect(await useDelayedSerializedDom()).toMatchInlineSnapshot(`
"<!DOCTYPE html><html><head>
<script defer="" fetchpriority="low" src="https://cdn.example.com/script.js" onload="this.dataset.onload = true" onerror="this.dataset.onerror = true" onabort="this.dataset.onabort = true" onprogress="this.dataset.onprogress = true" onloadstart="this.dataset.onloadstart = true" data-hid="438d65b" data-h-load="" data-h-error="" data-h-abort="" data-h-progress="" data-h-loadstart=""></script></head>
<script defer="" fetchpriority="low" src="https://cdn.example.com/script.js" data-hid="438d65b"></script></head>
<body>
<div>
Expand Down
2 changes: 1 addition & 1 deletion test/unhead/ssr/eventHandlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('ssr event handlers', () => {
"bodyAttrs": "",
"bodyTags": "",
"bodyTagsOpen": "",
"headTags": "<script src="https://js.stripe.com/v3/" defer onload="this.dataset.onload = true"></script>",
"headTags": "<script src="https://js.stripe.com/v3/" defer onload="this.dataset.onload = true" data-unhead-events=""></script>",
"htmlAttrs": "",
}
`)
Expand Down
2 changes: 1 addition & 1 deletion test/unhead/ssr/useScript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('dom useScript', () => {
"bodyTags": "",
"bodyTagsOpen": "",
"headTags": "<link rel="preconnect" href="https://cdn.example.com" data-hid="7b546a7">
<script defer fetchpriority="low" src="https://cdn.example.com/script.js" onload="this.dataset.onload = true" onerror="this.dataset.onerror = true" onabort="this.dataset.onabort = true" onprogress="this.dataset.onprogress = true" onloadstart="this.dataset.onloadstart = true" data-hid="438d65b"></script>",
<script defer fetchpriority="low" src="https://cdn.example.com/script.js" onload="this.dataset.onload = true" onerror="this.dataset.onerror = true" onabort="this.dataset.onabort = true" onprogress="this.dataset.onprogress = true" onloadstart="this.dataset.onloadstart = true" data-hid="438d65b" data-unhead-events=""></script>",
"htmlAttrs": "",
}
`)
Expand Down
2 changes: 1 addition & 1 deletion test/vue/dom/events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('vue events', () => {
"<!DOCTYPE html><html><head>
</head>
<body data-h-resize="">
<body>
<div>
<h1>hello world</h1>
Expand Down

0 comments on commit 9dac97d

Please sign in to comment.