Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/runtime-core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1270,6 +1270,14 @@ export interface ComponentCustomElementInterface {
shouldReflect?: boolean,
shouldUpdate?: boolean,
): void
/**
* @internal
*/
_beginPatch(): void
/**
* @internal
*/
_endPatch(): void
/**
* @internal attached by the nested Teleport when shadowRoot is false.
*/
Expand Down
27 changes: 18 additions & 9 deletions packages/runtime-core/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,15 +621,24 @@ function baseCreateRenderer(
optimized,
)
} else {
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
if (n1.el && (n1.el as VueElement)._isVueCE) {
;(n1.el as VueElement)._beginPatch()
}
try {
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} finally {
if (n1.el && (n1.el as VueElement)._isVueCE) {
;(n1.el as VueElement)._endPatch()
}
}
}
}

Expand Down
184 changes: 184 additions & 0 deletions packages/runtime-dom/__tests__/customElement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,190 @@ describe('defineCustomElement', () => {
'<div><span>1 is number</span><span>true is boolean</span></div>',
)
})

test('should patch all props together', async () => {
let prop1Calls = 0
let prop2Calls = 0
const E = defineCustomElement({
props: {
prop1: {
type: String,
default: 'default1',
},
prop2: {
type: String,
default: 'default2',
},
},
data() {
return {
data1: 'defaultData1',
data2: 'defaultData2',
}
},
watch: {
prop1(_) {
prop1Calls++
this.data2 = this.prop2
},
prop2(_) {
prop2Calls++
this.data1 = this.prop1
},
},
render() {
return h('div', [
h('h1', this.prop1),
h('h1', this.prop2),
h('h2', this.data1),
h('h2', this.data2),
])
},
})
customElements.define('my-watch-element', E)

render(h('my-watch-element'), container)
const e = container.childNodes[0] as VueElement
expect(e).toBeInstanceOf(E)
expect(e._instance).toBeTruthy()
expect(e.shadowRoot!.innerHTML).toBe(
`<div><h1>default1</h1><h1>default2</h1><h2>defaultData1</h2><h2>defaultData2</h2></div>`,
)
expect(prop1Calls).toBe(0)
expect(prop2Calls).toBe(0)

// patch props
render(
h('my-watch-element', { prop1: 'newValue1', prop2: 'newValue2' }),
container,
)
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe(
`<div><h1>newValue1</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
)
expect(prop1Calls).toBe(1)
expect(prop2Calls).toBe(1)

// same prop values
render(
h('my-watch-element', { prop1: 'newValue1', prop2: 'newValue2' }),
container,
)
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe(
`<div><h1>newValue1</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
)
expect(prop1Calls).toBe(1)
expect(prop2Calls).toBe(1)

// update only prop1
render(
h('my-watch-element', { prop1: 'newValue3', prop2: 'newValue2' }),
container,
)
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe(
`<div><h1>newValue3</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
)
expect(prop1Calls).toBe(2)
expect(prop2Calls).toBe(1)
})

test('should patch all props together (async)', async () => {
let prop1Calls = 0
let prop2Calls = 0
const E = defineCustomElement(
defineAsyncComponent(() =>
Promise.resolve(
defineComponent({
props: {
prop1: {
type: String,
default: 'default1',
},
prop2: {
type: String,
default: 'default2',
},
},
data() {
return {
data1: 'defaultData1',
data2: 'defaultData2',
}
},
watch: {
prop1(_) {
prop1Calls++
this.data2 = this.prop2
},
prop2(_) {
prop2Calls++
this.data1 = this.prop1
},
},
render() {
return h('div', [
h('h1', this.prop1),
h('h1', this.prop2),
h('h2', this.data1),
h('h2', this.data2),
])
},
}),
),
),
)
customElements.define('my-async-watch-element', E)

render(h('my-async-watch-element'), container)

await new Promise(r => setTimeout(r))
const e = container.childNodes[0] as VueElement
expect(e).toBeInstanceOf(E)
expect(e._instance).toBeTruthy()
expect(e.shadowRoot!.innerHTML).toBe(
`<div><h1>default1</h1><h1>default2</h1><h2>defaultData1</h2><h2>defaultData2</h2></div>`,
)
expect(prop1Calls).toBe(0)
expect(prop2Calls).toBe(0)

// patch props
render(
h('my-async-watch-element', { prop1: 'newValue1', prop2: 'newValue2' }),
container,
)
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe(
`<div><h1>newValue1</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
)
expect(prop1Calls).toBe(1)
expect(prop2Calls).toBe(1)

// same prop values
render(
h('my-async-watch-element', { prop1: 'newValue1', prop2: 'newValue2' }),
container,
)
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe(
`<div><h1>newValue1</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
)
expect(prop1Calls).toBe(1)
expect(prop2Calls).toBe(1)

// update only prop1
render(
h('my-async-watch-element', { prop1: 'newValue3', prop2: 'newValue2' }),
container,
)
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe(
`<div><h1>newValue3</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
)
expect(prop1Calls).toBe(2)
expect(prop2Calls).toBe(1)
})
})

describe('attrs', () => {
Expand Down
27 changes: 24 additions & 3 deletions packages/runtime-dom/src/apiCustomElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ export class VueElement

private _connected = false
private _resolved = false
private _patching = false
private _dirty = false
private _numberProps: Record<string, true> | null = null
private _styleChildren = new WeakSet()
private _pendingResolve: Promise<void> | undefined
Expand Down Expand Up @@ -457,11 +459,11 @@ export class VueElement
// defining getter/setters on prototype
for (const key of declaredPropKeys.map(camelize)) {
Object.defineProperty(this, key, {
get() {
get(this: VueElement) {
return this._getProp(key)
},
set(val) {
this._setProp(key, val, true, true)
set(this: VueElement, val) {
this._setProp(key, val, true, !this._patching)
},
})
}
Expand Down Expand Up @@ -495,6 +497,7 @@ export class VueElement
shouldUpdate = false,
): void {
if (val !== this._props[key]) {
this._dirty = true
if (val === REMOVAL) {
delete this._props[key]
} else {
Expand Down Expand Up @@ -670,6 +673,24 @@ export class VueElement
this._applyStyles(comp.styles, comp)
}

/**
* @internal
*/
_beginPatch(): void {
this._patching = true
this._dirty = false
}

/**
* @internal
*/
_endPatch(): void {
this._patching = false
if (this._dirty && this._instance) {
this._update()
}
}

/**
* @internal
*/
Expand Down