]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: enhance SVG support in dynamic props and attrs handling
authordaiwei <daiwei521@126.com>
Thu, 28 Aug 2025 13:32:04 +0000 (21:32 +0800)
committerdaiwei <daiwei521@126.com>
Thu, 28 Aug 2025 13:32:04 +0000 (21:32 +0800)
packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/vBind.spec.ts
packages/compiler-vapor/src/generators/prop.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transforms/transformElement.ts
packages/runtime-dom/__tests__/patchAttrs.spec.ts
packages/runtime-dom/src/index.ts
packages/runtime-vapor/__tests__/dom/prop.spec.ts
packages/runtime-vapor/src/dom/prop.ts

index edac6aef96ac15838faa5615c87af84e022be2fe..429bc440f1a61fa75d2a8584653d16a49a3fe190 100644 (file)
@@ -471,7 +471,7 @@ const t0 = _template("<svg></svg>", 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("<svg></svg>", 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("<div f=\\"foo1\\" h=\\"1\\"></div>", true)
index 60cd9d986edf0a4396dafa90a6cc56a50c58e19f..d5118632eb911718468f176cbce2edcc2cfd6e3d 100644 (file)
@@ -661,7 +661,15 @@ describe('compiler v-bind', () => {
       <svg :class="cls"/>
     `)
     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(`
+      <svg v-bind="obj"/>
+    `)
+    expect(code).matchSnapshot()
+    expect(code).contains('_setDynamicProps(n0, [_ctx.obj], true, true))')
   })
 
   test('number value', () => {
index 392420613f5ecae79f3890d6d202d27149f4e4d1..5c922c3d2a4da2d769e72ce6f84c0314dc493041 100644 (file)
@@ -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<Record<VaporHelper, HelperConfig>>
 
 // 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) {
index fd4eefd559bd62c127c49227145f5f5e485ba878..246b138f9c1467fdeb3d039bd50cedd01eece9a0 100644 (file)
@@ -111,6 +111,7 @@ export interface SetDynamicPropsIRNode extends BaseIRNode {
   element: number
   props: IRProps[]
   root: boolean
+  tag: string
 }
 
 export interface SetDynamicEventsIRNode extends BaseIRNode {
index 1919ccd2a2bd018068d9ea0ba0cdb578b03d82e8..a02bf80bad435e4bb8bd1c93c759cd7c8c6e0489 100644 (file)
@@ -217,6 +217,7 @@ function transformNativeElement(
         element: context.reference(),
         props: dynamicArgs,
         root: singleRoot,
+        tag,
       },
       getEffectIndex,
     )
index 393b685b0e9b3fbe7b3d7022baea0afb5f71ac33..7ee32c1442b8c83c637636adbe62d96a405ddd8a 100644 (file)
@@ -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)
index 4c43efd4e7f45eea8f6e92842773a7e11375ed05..16f175b74ab17e3f3122076f06836e5c443b824e 100644 (file)
@@ -352,3 +352,7 @@ export {
  * @internal
  */
 export { svgNS, mathmlNS } from './nodeOps'
+/**
+ * @internal
+ */
+export { xlinkNS } from './modules/attrs'
index 9d07b413541932f0f6c58ddc1a630138b15e866e..066634e0295819a923c6fb54f100d08e76bc3cb8 100644 (file)
@@ -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', () => {
index 8c42ad766a51d46d942831a5d81d112db21295e6..b3107e342847b398853dcf3480feae7551abbb49 100644 (file)
@@ -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 <input v-model type="checkbox"> 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 <input v-model type="checkbox">
-    setAttr(el, key, value)
+    setAttr(el, key, value, isSVG)
   }
   return value
 }