From: daiwei Date: Thu, 28 Aug 2025 13:32:04 +0000 (+0800) Subject: feat: enhance SVG support in dynamic props and attrs handling X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=ecef5332f81305427164358e13049e83544bdd66;p=thirdparty%2Fvuejs%2Fcore.git feat: enhance SVG support in dynamic props and attrs handling --- diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap index edac6aef96..429bc440f1 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap @@ -471,7 +471,7 @@ const t0 = _template("", true, 1) export function render(_ctx) { const n0 = t0() - _renderEffect(() => _setAttr(n0, "class", _ctx.cls)) + _renderEffect(() => _setAttr(n0, "class", _ctx.cls, true)) return n0 }" `; @@ -642,6 +642,17 @@ export function render(_ctx) { }" `; +exports[`compiler v-bind > v-bind w/ svg elements 1`] = ` +"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("", true, 1) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setDynamicProps(n0, [_ctx.obj], true, true)) + return n0 +}" +`; + exports[`compiler v-bind > with constant value 1`] = ` "import { setProp as _setProp, template as _template } from 'vue'; const t0 = _template("
", true) diff --git a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts index 60cd9d986e..d5118632eb 100644 --- a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts @@ -661,7 +661,15 @@ describe('compiler v-bind', () => { `) expect(code).matchSnapshot() - expect(code).contains('_setAttr(n0, "class", _ctx.cls))') + expect(code).contains('_setAttr(n0, "class", _ctx.cls, true))') + }) + + test('v-bind w/ svg elements', () => { + const { code } = compileWithVBind(` + + `) + expect(code).matchSnapshot() + expect(code).contains('_setDynamicProps(n0, [_ctx.obj], true, true))') }) test('number value', () => { diff --git a/packages/compiler-vapor/src/generators/prop.ts b/packages/compiler-vapor/src/generators/prop.ts index 392420613f..5c922c3d2a 100644 --- a/packages/compiler-vapor/src/generators/prop.ts +++ b/packages/compiler-vapor/src/generators/prop.ts @@ -23,6 +23,7 @@ import { import { canSetValueDirectly, capitalize, + extend, isSVGTag, shouldSetAsAttr, toHandlerKey, @@ -31,6 +32,7 @@ import { export type HelperConfig = { name: VaporHelper needKey?: boolean + isSVG?: boolean acceptRoot?: boolean } @@ -44,7 +46,6 @@ const helpers = { setAttr: { name: 'setAttr', needKey: true }, setProp: { name: 'setProp', needKey: true }, setDOMProp: { name: 'setDOMProp', needKey: true }, - setDynamicProps: { name: 'setDynamicProps' }, } as const satisfies Partial> // only the static key prop will reach here @@ -66,6 +67,7 @@ export function genSetProp( `n${oper.element}`, resolvedHelper.needKey ? genExpression(key, context) : false, propValue, + resolvedHelper.isSVG && 'true', ), ] } @@ -76,6 +78,7 @@ export function genDynamicProps( context: CodegenContext, ): CodeFragment[] { const { helper } = context + const isSVG = isSVGTag(oper.tag) const values = oper.props.map(props => Array.isArray(props) ? genLiteralObjectProps(props, context) // static and dynamic arg props @@ -90,6 +93,7 @@ export function genDynamicProps( `n${oper.element}`, genMulti(DELIMITERS_ARRAY, ...values), oper.root && 'true', + isSVG && 'true', ), ] } @@ -173,8 +177,7 @@ function getRuntimeHelper( // 1. SVG: always attribute if (isSVG) { - // TODO pass svg flag - return helpers.setAttr + return extend({ isSVG: true }, helpers.setAttr) } if (modifier) { diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts index fd4eefd559..246b138f9c 100644 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@ -111,6 +111,7 @@ export interface SetDynamicPropsIRNode extends BaseIRNode { element: number props: IRProps[] root: boolean + tag: string } export interface SetDynamicEventsIRNode extends BaseIRNode { diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index 1919ccd2a2..a02bf80bad 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -217,6 +217,7 @@ function transformNativeElement( element: context.reference(), props: dynamicArgs, root: singleRoot, + tag, }, getEffectIndex, ) diff --git a/packages/runtime-dom/__tests__/patchAttrs.spec.ts b/packages/runtime-dom/__tests__/patchAttrs.spec.ts index 393b685b0e..7ee32c1442 100644 --- a/packages/runtime-dom/__tests__/patchAttrs.spec.ts +++ b/packages/runtime-dom/__tests__/patchAttrs.spec.ts @@ -10,7 +10,7 @@ describe('runtime-dom: attrs patching', () => { expect(el.getAttributeNS(xlinkNS, 'href')).toBe(null) }) - test('textContent attributes /w svg', () => { + test('textContent attributes w/ svg', () => { const el = document.createElementNS('http://www.w3.org/2000/svg', 'use') patchProp(el, 'textContent', null, 'foo', 'svg') expect(el.attributes.length).toBe(0) diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index 4c43efd4e7..16f175b74a 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -352,3 +352,7 @@ export { * @internal */ export { svgNS, mathmlNS } from './nodeOps' +/** + * @internal + */ +export { xlinkNS } from './modules/attrs' diff --git a/packages/runtime-vapor/__tests__/dom/prop.spec.ts b/packages/runtime-vapor/__tests__/dom/prop.spec.ts index 9d07b41354..066634e029 100644 --- a/packages/runtime-vapor/__tests__/dom/prop.spec.ts +++ b/packages/runtime-vapor/__tests__/dom/prop.spec.ts @@ -12,7 +12,7 @@ import { } from '../../src/dom/prop' import { setStyle } from '../../src/dom/prop' import { VaporComponentInstance } from '../../src/component' -import { ref, setCurrentInstance } from '@vue/runtime-dom' +import { ref, setCurrentInstance, svgNS, xlinkNS } from '@vue/runtime-dom' let removeComponentInstance = NOOP beforeEach(() => { @@ -307,8 +307,9 @@ describe('patchProp', () => { key: string, value: any, el = element.cloneNode(true) as HTMLElement, + isSVG: boolean = false, ) { - _setDynamicProp(el, key, value) + _setDynamicProp(el, key, value, isSVG) return el } @@ -359,7 +360,40 @@ describe('patchProp', () => { expect(res.textContent).toBe('foo') }) - test.todo('should be able to set something on SVG') + test('set class w/ SVG', () => { + const el = document.createElementNS(svgNS, 'svg') as any + setDynamicProp('class', 'foo', el, true) + expect(el.getAttribute('class')).toBe('foo') + }) + + test('set class incremental w/ SVG', () => { + const el = document.createElementNS(svgNS, 'svg') as any + el.setAttribute('class', 'bar') + el.$root = true + setDynamicProp('class', 'foo', el, true) + expect(el.getAttribute('class')).toBe('bar foo') + }) + + test('set xlink attributes w/ SVG', () => { + const el = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'use', + ) as any + setDynamicProp('xlink:href', 'a', el, true) + expect(el.getAttributeNS(xlinkNS, 'href')).toBe('a') + setDynamicProp('xlink:href', null, el, true) + expect(el.getAttributeNS(xlinkNS, 'href')).toBe(null) + }) + + test('set textContent attributes w/ SVG', () => { + const el = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'use', + ) as any + setDynamicProp('textContent', 'foo', el, true) + expect(el.attributes.length).toBe(0) + expect(el.innerHTML).toBe('foo') + }) }) describe('setDynamicProps', () => { diff --git a/packages/runtime-vapor/src/dom/prop.ts b/packages/runtime-vapor/src/dom/prop.ts index 8c42ad766a..b3107e3428 100644 --- a/packages/runtime-vapor/src/dom/prop.ts +++ b/packages/runtime-vapor/src/dom/prop.ts @@ -15,6 +15,7 @@ import { patchStyle, shouldSetAsProp, warn, + xlinkNS, } from '@vue/runtime-dom' import { type VaporComponentInstance, @@ -26,6 +27,7 @@ type TargetElement = Element & { $html?: string $cls?: string $sty?: NormalizedStyle | string | undefined + $svg?: boolean value?: string _value?: any } @@ -42,10 +44,23 @@ export function setProp(el: any, key: string, value: any): void { } } -export function setAttr(el: any, key: string, value: any): void { +export function setAttr( + el: any, + key: string, + value: any, + isSVG: boolean = false, +): void { if (!isApplyingFallthroughProps && el.$root && hasFallthroughKey(key)) { return } + if (isSVG && key.startsWith('xlink:')) { + if (value == null) { + el.removeAttributeNS(xlinkNS, key.slice(6, key.length)) + } else { + el.setAttributeNS(xlinkNS, key, value) + } + return + } // special case for with // :true-value & :false-value @@ -109,11 +124,19 @@ export function setDOMProp(el: any, key: string, value: any): void { needRemove && el.removeAttribute(key) } -export function setClass(el: TargetElement, value: any): void { +export function setClass( + el: TargetElement, + value: any, + isSVG: boolean = false, +): void { if (el.$root) { setClassIncremental(el, value) } else if ((value = normalizeClass(value)) !== el.$cls) { - el.className = el.$cls = value + if (isSVG) { + el.setAttribute('class', (el.$cls = value)) + } else { + el.className = el.$cls = value + } } } @@ -203,21 +226,27 @@ export function setHtml(el: TargetElement, value: any): void { } } -export function setDynamicProps(el: any, args: any[]): void { +export function setDynamicProps( + el: any, + args: any[], + root?: boolean, + isSVG?: boolean, +): void { const props = args.length > 1 ? mergeProps(...args) : args[0] const cacheKey = `$dprops${isApplyingFallthroughProps ? '$' : ''}` const prevKeys = el[cacheKey] as string[] + if (root) el.$root = root if (prevKeys) { for (const key of prevKeys) { if (!(key in props)) { - setDynamicProp(el, key, null) + setDynamicProp(el, key, null, isSVG) } } } for (const key of (el[cacheKey] = Object.keys(props))) { - setDynamicProp(el, key, props[key]) + setDynamicProp(el, key, props[key], isSVG) } } @@ -228,11 +257,10 @@ export function setDynamicProp( el: TargetElement, key: string, value: any, + isSVG: boolean = false, ): void { - // TODO - const isSVG = false if (key === 'class') { - setClass(el, value) + setClass(el, value, isSVG) } else if (key === 'style') { setStyle(el, value) } else if (isOn(key)) { @@ -254,8 +282,7 @@ export function setDynamicProp( setDOMProp(el, key, value) } } else { - // TODO special case for - setAttr(el, key, value) + setAttr(el, key, value, isSVG) } return value }