From 9cb07e6c69813048603085e14c3b291dffa6f7b0 Mon Sep 17 00:00:00 2001 From: Alexandre von Brasche Figueiredo Date: Fri, 21 Mar 2025 11:26:39 +0100 Subject: [PATCH 1/4] fix(runtime-dom): check for camel-case element prop close #13055 --- packages/runtime-dom/src/patchProp.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/runtime-dom/src/patchProp.ts b/packages/runtime-dom/src/patchProp.ts index b6af8997112..110774441d0 100644 --- a/packages/runtime-dom/src/patchProp.ts +++ b/packages/runtime-dom/src/patchProp.ts @@ -47,7 +47,9 @@ export const patchProp: DOMRendererOptions['patchProp'] = ( ? ((key = key.slice(1)), false) : shouldSetAsProp(el, key, nextValue, isSVG) ) { - patchDOMProp(el, key, nextValue, parentComponent) + const camelKey = camelize(key) + const propKey = camelKey in el ? camelKey : key + patchDOMProp(el, propKey, nextValue, parentComponent, key) // #6007 also set form state as attributes so they work with // or libs / extensions that expect attributes // #11163 custom elements may use value as an prop and set it as object @@ -140,5 +142,5 @@ function shouldSetAsProp( return false } - return key in el + return camelize(key) in el || key in el } From 5b211295e299820fd49eb3dfad8ddbfc61acdf2f Mon Sep 17 00:00:00 2001 From: Alexandre von Brasche Figueiredo Date: Fri, 21 Mar 2025 11:29:47 +0100 Subject: [PATCH 2/4] test: add kebab to camel prop tests --- .../runtime-dom/__tests__/patchProps.spec.ts | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/packages/runtime-dom/__tests__/patchProps.spec.ts b/packages/runtime-dom/__tests__/patchProps.spec.ts index 304d9f9c136..51713c0b572 100644 --- a/packages/runtime-dom/__tests__/patchProps.spec.ts +++ b/packages/runtime-dom/__tests__/patchProps.spec.ts @@ -7,6 +7,8 @@ import { vModelCheckbox, withDirectives, } from '../src' +import { defineComponent } from '@vue/runtime-test' +import { render as domRender } from 'vue' describe('runtime-dom: props patching', () => { test('basic', () => { @@ -395,3 +397,114 @@ describe('runtime-dom: props patching', () => { expect(fn).toBeCalledTimes(0) }) }) + +// #13055 +test('should lookup camelCase keys in element properties', async () => { + class TestElement extends HTMLElement { + _complexData = {} + + get primitiveValue(): string | null { + return this.getAttribute('primitive-value') + } + + set primitiveValue(value: string | undefined) { + if (value) { + this.setAttribute('primitive-value', value) + } else { + this.removeAttribute('primitive-value') + } + } + + get complexData() { + return this._complexData + } + + set complexData(data) { + this._complexData = data + } + } + + window.customElements.define('test-element', TestElement) + const el = document.createElement('test-element') as TestElement + + patchProp(el, 'primitive-value', null, 'foo') + expect(el.primitiveValue).toBe('foo') + + patchProp(el, 'complex-data', null, { foo: 'bar' }) + expect(el.hasAttribute('complex-data')).toBe(false) + expect(el.getAttribute('complex-data')).not.toBe('[object Object]') + expect(el.complexData).toStrictEqual({ foo: 'bar' }) +}) + +// #13055 +test('should handle kebab-case prop bindings', async () => { + class HelloWorld extends HTMLElement { + #testProp = '' + #output: HTMLDivElement + + get testProp() { + return this.#testProp + } + + set testProp(value: string) { + this.#testProp = value + this.#update() + } + + constructor() { + super() + + this.attachShadow({ mode: 'open' }) + this.shadowRoot!.innerHTML = `
` + this.#output = this.shadowRoot?.querySelector('.output') as HTMLDivElement + } + + connectedCallback() { + console.log('UPDATING!') + this.#update() + } + + #update() { + this.#output.innerHTML = `this.testProp = ${this.#testProp}` + } + } + + window.customElements.define('hello-world', HelloWorld) + + const Comp = defineComponent({ + setup() { + const testProp = ref('Hello, world! from App.vue') + return { + testProp, + } + }, + template: ` + + + + `, + }) + + const root = document.createElement('div') + + // Note this one is using the main Vue render so it can compile template + // on the fly + domRender(h(Comp), root) + + expect(root.innerHTML).toBe( + ``, + ) + + const [child1, child2, child3] = Array.from( + root.querySelectorAll('hello-world'), + ) + expect(child3.shadowRoot?.innerHTML).toBe( + '
this.testProp = Hello, world! from App.vue
', + ) + expect(child2.shadowRoot?.innerHTML).toBe( + '
this.testProp = Hello, world! from App.vue
', + ) + expect(child1.shadowRoot?.innerHTML).toBe( + '
this.testProp = Hello, world! from App.vue
', + ) +}) From 489d2857fa0181ce9e1378501e271a1c71dc026a Mon Sep 17 00:00:00 2001 From: Alexandre von Brasche Figueiredo Date: Tue, 25 Mar 2025 10:43:50 +0100 Subject: [PATCH 3/4] perf(runtime-dom): cache element prop key --- packages/runtime-dom/src/patchProp.ts | 29 ++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/runtime-dom/src/patchProp.ts b/packages/runtime-dom/src/patchProp.ts index 110774441d0..38978e17736 100644 --- a/packages/runtime-dom/src/patchProp.ts +++ b/packages/runtime-dom/src/patchProp.ts @@ -47,8 +47,10 @@ export const patchProp: DOMRendererOptions['patchProp'] = ( ? ((key = key.slice(1)), false) : shouldSetAsProp(el, key, nextValue, isSVG) ) { - const camelKey = camelize(key) - const propKey = camelKey in el ? camelKey : key + let camelKey = '' + const propKey = + getElementPropKey(el, key) ?? + ((camelKey = camelize(key)) in el ? camelKey : key) patchDOMProp(el, propKey, nextValue, parentComponent, key) // #6007 also set form state as attributes so they work with // or libs / extensions that expect attributes @@ -89,10 +91,12 @@ function shouldSetAsProp( // most keys must be set as attribute on svg elements to work // ...except innerHTML & textContent if (key === 'innerHTML' || key === 'textContent') { + setElementPropKey(el, key) return true } // or native onclick with function values if (key in el && isNativeOn(key) && isFunction(value)) { + setElementPropKey(el, key) return true } return false @@ -142,5 +146,24 @@ function shouldSetAsProp( return false } - return camelize(key) in el || key in el + const camelKey = camelize(key) + if (camelKey in el) { + setElementPropKey(el, key, camelKey) + return true + } + + if (key in el) { + setElementPropKey(el, key) + return true + } + + return false +} + +function getElementPropKey(el: Element, key: string) { + return (el as any)[`$_v_prop_${key}`] +} + +function setElementPropKey(el: Element, key: string, propKey = key) { + ;(el as any)[`$_v_prop_${key}`] = propKey } From d639feb9dc804957cdd9c199aca596d34ad1c858 Mon Sep 17 00:00:00 2001 From: Alexandre von Brasche Figueiredo Date: Thu, 27 Mar 2025 09:59:32 +0100 Subject: [PATCH 4/4] Revert "perf(runtime-dom): cache element prop key" This reverts commit 489d2857fa0181ce9e1378501e271a1c71dc026a. --- packages/runtime-dom/src/patchProp.ts | 29 +++------------------------ 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/packages/runtime-dom/src/patchProp.ts b/packages/runtime-dom/src/patchProp.ts index 38978e17736..110774441d0 100644 --- a/packages/runtime-dom/src/patchProp.ts +++ b/packages/runtime-dom/src/patchProp.ts @@ -47,10 +47,8 @@ export const patchProp: DOMRendererOptions['patchProp'] = ( ? ((key = key.slice(1)), false) : shouldSetAsProp(el, key, nextValue, isSVG) ) { - let camelKey = '' - const propKey = - getElementPropKey(el, key) ?? - ((camelKey = camelize(key)) in el ? camelKey : key) + const camelKey = camelize(key) + const propKey = camelKey in el ? camelKey : key patchDOMProp(el, propKey, nextValue, parentComponent, key) // #6007 also set form state as attributes so they work with // or libs / extensions that expect attributes @@ -91,12 +89,10 @@ function shouldSetAsProp( // most keys must be set as attribute on svg elements to work // ...except innerHTML & textContent if (key === 'innerHTML' || key === 'textContent') { - setElementPropKey(el, key) return true } // or native onclick with function values if (key in el && isNativeOn(key) && isFunction(value)) { - setElementPropKey(el, key) return true } return false @@ -146,24 +142,5 @@ function shouldSetAsProp( return false } - const camelKey = camelize(key) - if (camelKey in el) { - setElementPropKey(el, key, camelKey) - return true - } - - if (key in el) { - setElementPropKey(el, key) - return true - } - - return false -} - -function getElementPropKey(el: Element, key: string) { - return (el as any)[`$_v_prop_${key}`] -} - -function setElementPropKey(el: Element, key: string, propKey = key) { - ;(el as any)[`$_v_prop_${key}`] = propKey + return camelize(key) in el || key in el }