diff --git a/packages/runtime-dom/__tests__/modules/attrs.spec.ts b/packages/runtime-dom/__tests__/modules/attrs.spec.ts index 9df8d14b..2a3bad0d 100644 --- a/packages/runtime-dom/__tests__/modules/attrs.spec.ts +++ b/packages/runtime-dom/__tests__/modules/attrs.spec.ts @@ -1,7 +1,7 @@ import { patchProp } from '../../src/patchProp' import { xlinkNS } from '../../src/modules/attrs' -describe('attrs', () => { +describe('runtime-dom: attrs patching', () => { test('xlink attributes', () => { const el = document.createElementNS('http://www.w3.org/2000/svg', 'use') patchProp(el, 'xlink:href', null, 'a', true) diff --git a/packages/runtime-dom/__tests__/modules/class.spec.ts b/packages/runtime-dom/__tests__/modules/class.spec.ts index 2cb40951..d6bfdeca 100644 --- a/packages/runtime-dom/__tests__/modules/class.spec.ts +++ b/packages/runtime-dom/__tests__/modules/class.spec.ts @@ -1,188 +1,31 @@ -// https://github.com/vuejs/vue/blob/dev/test/unit/features/directives/class.spec.js +import { patchProp } from '../../src/patchProp' +import { ElementWithTransition } from '../../src/components/Transition' +import { svgNS } from '../../src/nodeOps' -import { h, render, defineComponent } from '../../src' - -type ClassItem = { - value: string | object | string[] -} - -function assertClass(assertions: Array>) { - const root = document.createElement('div') - const dynamic = { value: '' } - const wrapper = () => h('div', { class: ['foo', dynamic.value] }) - - for (const [input, expected] of assertions) { - if (typeof input === 'function') { - input(dynamic.value) - } else { - dynamic.value = input - } - - render(wrapper(), root) - expect(root.children[0].className).toBe(expected) - } -} - -describe('class', () => { - test('plain string', () => { - assertClass([ - ['bar', 'foo bar'], - ['baz qux', 'foo baz qux'], - ['qux', 'foo qux'], - [undefined, 'foo'] - ]) +describe('runtime-dom: class patching', () => { + test('basics', () => { + const el = document.createElement('div') + patchProp(el, 'class', null, 'foo') + expect(el.className).toBe('foo') + patchProp(el, 'class', null, null) + expect(el.className).toBe('') }) - test('object value', () => { - assertClass([ - [{ bar: true, baz: false }, 'foo bar'], - [{ baz: true }, 'foo baz'], - [null, 'foo'], - [{ 'bar baz': true, qux: false }, 'foo bar baz'], - [{ qux: true }, 'foo qux'] - ]) + test('transition class', () => { + const el = document.createElement('div') as ElementWithTransition + el._vtc = new Set(['bar', 'baz']) + patchProp(el, 'class', null, 'foo') + expect(el.className).toBe('foo bar baz') + patchProp(el, 'class', null, null) + expect(el.className).toBe('bar baz') + delete el._vtc + patchProp(el, 'class', null, 'foo') + expect(el.className).toBe('foo') }) - test('array value', () => { - assertClass([ - [['bar', 'baz'], 'foo bar baz'], - [['qux', 'baz'], 'foo qux baz'], - [['w', 'x y z'], 'foo w x y z'], - [undefined, 'foo'], - [['bar'], 'foo bar'], - [(val: Array) => val.push('baz'), 'foo bar baz'] - ]) - }) - - test('array of mixed values', () => { - assertClass([ - [['x', { y: true, z: true }], 'foo x y z'], - [['x', { y: true, z: false }], 'foo x y'], - [['f', { z: true }], 'foo f z'], - [['l', 'f', { n: true, z: true }], 'foo l f n z'], - [['x', {}], 'foo x'], - [undefined, 'foo'] - ]) - }) - - test('class merge between parent and child', () => { - const root = document.createElement('div') - - const childClass: ClassItem = { value: 'd' } - const child = { - render: () => h('div', { class: ['c', childClass.value] }) - } - - const parentClass: ClassItem = { value: 'b' } - const parent = { - render: () => h(child, { class: ['a', parentClass.value] }) - } - - render(h(parent), root) - expect(root.children[0].className).toBe('c d a b') - - parentClass.value = 'e' - // the `foo` here is just for forcing parent to be updated - // (otherwise it's skipped since its props never change) - render(h(parent, { foo: 1 }), root) - expect(root.children[0].className).toBe('c d a e') - - parentClass.value = 'f' - render(h(parent, { foo: 2 }), root) - expect(root.children[0].className).toBe('c d a f') - - parentClass.value = { foo: true } - childClass.value = ['bar', 'baz'] - render(h(parent, { foo: 3 }), root) - expect(root.children[0].className).toBe('c bar baz a foo') - }) - - test('class merge between multiple nested components sharing same element', () => { - const component1 = defineComponent({ - render() { - return this.$slots.default!()[0] - } - }) - - const component2 = defineComponent({ - render() { - return this.$slots.default!()[0] - } - }) - - const component3 = defineComponent({ - render() { - return h( - 'div', - { - class: 'staticClass' - }, - [this.$slots.default!()] - ) - } - }) - - const root = document.createElement('div') - const componentClass1 = { value: 'componentClass1' } - const componentClass2 = { value: 'componentClass2' } - const componentClass3 = { value: 'componentClass3' } - - const wrapper = () => - h(component1, { class: componentClass1.value }, () => [ - h(component2, { class: componentClass2.value }, () => [ - h(component3, { class: componentClass3.value }, () => ['some text']) - ]) - ]) - - render(wrapper(), root) - expect(root.children[0].className).toBe( - 'staticClass componentClass3 componentClass2 componentClass1' - ) - - componentClass1.value = 'c1' - render(wrapper(), root) - expect(root.children[0].className).toBe( - 'staticClass componentClass3 componentClass2 c1' - ) - - componentClass2.value = 'c2' - render(wrapper(), root) - expect(root.children[0].className).toBe('staticClass componentClass3 c2 c1') - - componentClass3.value = 'c3' - render(wrapper(), root) - expect(root.children[0].className).toBe('staticClass c3 c2 c1') - }) - - test('deep update', () => { - const root = document.createElement('div') - const test = { - a: true, - b: false - } - - const wrapper = () => h('div', { class: test }) - render(wrapper(), root) - expect(root.children[0].className).toBe('a') - - test.b = true - render(wrapper(), root) - expect(root.children[0].className).toBe('a b') - }) - - // a vdom patch edge case where the user has several un-keyed elements of the - // same tag next to each other, and toggling them. - test('properly remove staticClass for toggling un-keyed children', () => { - const root = document.createElement('div') - const ok = { value: true } - const wrapper = () => - h('div', [ok.value ? h('div', { class: 'a' }) : h('div')]) - - render(wrapper(), root) - expect(root.children[0].children[0].className).toBe('a') - - ok.value = false - render(wrapper(), root) - expect(root.children[0].children[0].className).toBe('') + test('svg', () => { + const el = document.createElementNS(svgNS, 'svg') + patchProp(el, 'class', null, 'foo', true) + expect(el.getAttribute('class')).toBe('foo') }) }) diff --git a/packages/runtime-dom/__tests__/modules/events.spec.ts b/packages/runtime-dom/__tests__/modules/events.spec.ts index 2aced378..3b365e1c 100644 --- a/packages/runtime-dom/__tests__/modules/events.spec.ts +++ b/packages/runtime-dom/__tests__/modules/events.spec.ts @@ -2,7 +2,7 @@ import { patchProp } from '../../src/patchProp' const timeout = () => new Promise(r => setTimeout(r)) -describe(`events`, () => { +describe(`runtime-dom: events patching`, () => { it('should assign event handler', async () => { const el = document.createElement('div') const event = new Event('click') diff --git a/packages/runtime-dom/__tests__/modules/style.spec.ts b/packages/runtime-dom/__tests__/modules/style.spec.ts index a3174fca..90fd60f0 100644 --- a/packages/runtime-dom/__tests__/modules/style.spec.ts +++ b/packages/runtime-dom/__tests__/modules/style.spec.ts @@ -1,6 +1,6 @@ import { patchProp } from '../../src/patchProp' -describe(`module style`, () => { +describe(`runtime-dom: style patching`, () => { it('string', () => { const el = document.createElement('div') patchProp(el, 'style', {}, 'color:red') diff --git a/packages/runtime-dom/src/modules/class.ts b/packages/runtime-dom/src/modules/class.ts index e81d7fe7..d26867d9 100644 --- a/packages/runtime-dom/src/modules/class.ts +++ b/packages/runtime-dom/src/modules/class.ts @@ -6,15 +6,18 @@ export function patchClass(el: Element, value: string | null, isSVG: boolean) { if (value == null) { value = '' } - // directly setting className should be faster than setAttribute in theory if (isSVG) { el.setAttribute('class', value) } else { + // directly setting className should be faster than setAttribute in theory // if this is an element during a transition, take the temporary transition // classes into account. const transitionClasses = (el as ElementWithTransition)._vtc if (transitionClasses) { - value = [value, ...transitionClasses].join(' ') + value = (value + ? [value, ...transitionClasses] + : [...transitionClasses] + ).join(' ') } el.className = value } diff --git a/packages/runtime-dom/src/nodeOps.ts b/packages/runtime-dom/src/nodeOps.ts index 38b494eb..b147da30 100644 --- a/packages/runtime-dom/src/nodeOps.ts +++ b/packages/runtime-dom/src/nodeOps.ts @@ -1,7 +1,8 @@ import { RendererOptions } from '@vue/runtime-core' +export const svgNS = 'http://www.w3.org/2000/svg' + const doc = (typeof document !== 'undefined' ? document : null) as Document -const svgNS = 'http://www.w3.org/2000/svg' let tempContainer: HTMLElement let tempSVGContainer: SVGElement