Skip to content

Commit 02a013a

Browse files
committed
refactor: refine fallthrough attribute to single root elements across components and fragments.
1 parent 087c10f commit 02a013a

File tree

7 files changed

+99
-62
lines changed

7 files changed

+99
-62
lines changed

packages/runtime-vapor/__tests__/customElement.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -580,22 +580,22 @@ describe('defineVaporCustomElement', () => {
580580
describe('attrs', () => {
581581
const E = defineVaporCustomElement({
582582
setup(_: any, { attrs }: any) {
583-
const n0 = template('<div> </div>')() as any
583+
const n0 = template('<div> </div>', true)() as any
584584
const x0 = txt(n0) as any
585585
renderEffect(() => setText(x0, toDisplayString(attrs.foo)))
586-
return [n0]
586+
return n0
587587
},
588588
})
589589
customElements.define('my-el-attrs', E)
590590

591591
test('attrs via attribute', async () => {
592592
container.innerHTML = `<my-el-attrs foo="hello"></my-el-attrs>`
593593
const e = container.childNodes[0] as VaporElement
594-
expect(e.shadowRoot!.innerHTML).toBe('<div>hello</div>')
594+
expect(e.shadowRoot!.innerHTML).toBe('<div foo="hello">hello</div>')
595595

596596
e.setAttribute('foo', 'changed')
597597
await nextTick()
598-
expect(e.shadowRoot!.innerHTML).toBe('<div>changed</div>')
598+
expect(e.shadowRoot!.innerHTML).toBe('<div foo="changed">changed</div>')
599599
})
600600

601601
test('non-declared properties should not show up in $attrs', () => {

packages/runtime-vapor/src/apiCreateFor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ export const createFor = (
446446

447447
// apply transition for new nodes
448448
if (frag.$transition) {
449-
applyTransitionHooks(block.nodes, frag.$transition, false)
449+
applyTransitionHooks(block.nodes, frag.$transition)
450450
}
451451

452452
if (parent) insert(block.nodes, parent, anchor)

packages/runtime-vapor/src/component.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
currentInstance,
1717
endMeasure,
1818
expose,
19+
getComponentName,
1920
isAsyncWrapper,
2021
isKeepAlive,
2122
nextUid,
@@ -27,7 +28,6 @@ import {
2728
startMeasure,
2829
unregisterHMR,
2930
warn,
30-
// warnExtraneousAttributes,
3131
} from '@vue/runtime-dom'
3232
import {
3333
type Block,
@@ -203,7 +203,10 @@ export function createComponent(
203203
const parentInstance = getParentInstance()
204204

205205
if (
206-
isSingleRoot &&
206+
(isSingleRoot ||
207+
// transition has attrs fallthrough
208+
(parentInstance &&
209+
getComponentName(parentInstance!.type) === 'VaporTransition')) &&
207210
component.inheritAttrs !== false &&
208211
isVaporComponent(parentInstance) &&
209212
parentInstance.hasFallthrough
@@ -403,7 +406,12 @@ export function setupComponent(
403406
component.inheritAttrs !== false &&
404407
Object.keys(instance.attrs).length
405408
) {
406-
renderEffect(() => applyFallthroughProps(instance.block, instance.attrs))
409+
const root = filterSingleRootElement(instance.block)
410+
if (root) {
411+
renderEffect(() => applyFallthroughProps(root, instance.attrs))
412+
} else if (__DEV__ && isArray(instance.block)) {
413+
// TODO warn extraneous attributes
414+
}
407415
}
408416

409417
setActiveSub(prevSub)
@@ -418,17 +426,12 @@ export function setupComponent(
418426
export let isApplyingFallthroughProps = false
419427

420428
export function applyFallthroughProps(
421-
block: Block,
429+
el: Element,
422430
attrs: Record<string, any>,
423431
): void {
424-
const el = getRootElement(block, false)
425-
if (el) {
426-
isApplyingFallthroughProps = true
427-
setDynamicProps(el, [attrs])
428-
isApplyingFallthroughProps = false
429-
} else if (__DEV__) {
430-
// warnExtraneousAttributes(attrs)
431-
}
432+
isApplyingFallthroughProps = true
433+
setDynamicProps(el, [attrs])
434+
isApplyingFallthroughProps = false
432435
}
433436

434437
/**
@@ -869,3 +872,22 @@ export function getRootElement(
869872
return hasComment ? singleRoot : undefined
870873
}
871874
}
875+
876+
export function filterSingleRootElement(block: Block): Element | undefined {
877+
let singleRoot
878+
if (block instanceof Element) {
879+
singleRoot = block
880+
} else if (isArray(block)) {
881+
for (const b of block) {
882+
if (b instanceof Element) {
883+
if (singleRoot) {
884+
// has more than 1 non-comment child
885+
return
886+
} else {
887+
singleRoot = b
888+
}
889+
}
890+
}
891+
}
892+
return singleRoot
893+
}

packages/runtime-vapor/src/components/Teleport.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
MismatchTypes,
44
type TeleportProps,
55
type TeleportTargetElement,
6-
currentInstance,
76
isMismatchAllowed,
87
isTeleportDeferred,
98
isTeleportDisabled,
@@ -16,6 +15,8 @@ import { createComment, createTextNode, querySelector } from '../dom/node'
1615
import {
1716
type LooseRawProps,
1817
type LooseRawSlots,
18+
type VaporComponentInstance,
19+
applyFallthroughProps,
1920
isVaporComponent,
2021
} from '../component'
2122
import { rawPropsProxyHandlers } from '../componentProps'
@@ -32,6 +33,7 @@ import {
3233
setCurrentHydrationNode,
3334
} from '../dom/hydration'
3435
import { applyTransitionHooks } from './Transition'
36+
import { getParentInstance } from '../componentSlots'
3537

3638
export const VaporTeleportImpl = {
3739
name: 'VaporTeleport',
@@ -57,13 +59,12 @@ export class TeleportFragment extends VaporFragment<Block[]> {
5759
placeholder?: Node
5860
mountContainer?: ParentNode | null
5961
mountAnchor?: Node | null
60-
parentComponent: GenericComponentInstance
6162

6263
constructor(props: LooseRawProps, slots: LooseRawSlots) {
6364
super([])
6465
this.rawProps = props
6566
this.rawSlots = slots
66-
this.parentComponent = currentInstance as GenericComponentInstance
67+
this.parentComponent = getParentInstance()
6768
this.anchor = isHydrating
6869
? undefined
6970
: __DEV__
@@ -144,8 +145,19 @@ export class TeleportFragment extends VaporFragment<Block[]> {
144145

145146
private mount(parent: ParentNode, anchor: Node | null): void {
146147
if (this.$transition) {
147-
applyTransitionHooks(this.nodes, this.$transition)
148+
this.$transition = applyTransitionHooks(this.nodes, this.$transition)
148149
}
150+
151+
// fallthrough attrs
152+
const instance = this.parentComponent as VaporComponentInstance
153+
if (
154+
instance.hasFallthrough &&
155+
Object.keys(instance.attrs).length &&
156+
this.nodes instanceof Element
157+
) {
158+
applyFallthroughProps(this.nodes, instance.attrs)
159+
}
160+
149161
insert(
150162
this.nodes,
151163
(this.mountContainer = parent),

packages/runtime-vapor/src/components/Transition.ts

Lines changed: 14 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
applyFallthroughProps,
2727
isVaporComponent,
2828
} from '../component'
29-
import { extend, isArray } from '@vue/shared'
29+
import { isArray } from '@vue/shared'
3030
import { renderEffect } from '../renderEffect'
3131
import { isFragment } from '../fragment'
3232
import {
@@ -85,15 +85,15 @@ export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate(
8585
renderEffect(() => {
8686
resolvedProps = resolveTransitionProps(props)
8787
if (isMounted) {
88-
// only update props for Fragment block, for later reusing
88+
// only update props for Fragment transition, for later reusing
8989
if (isFragment(children)) {
9090
children.$transition!.props = resolvedProps
9191
} else {
9292
const child = findTransitionBlock(children)
9393
if (child) {
9494
// replace existing transition hooks
9595
child.$transition!.props = resolvedProps
96-
applyTransitionHooks(child, child.$transition!, undefined, true)
96+
applyTransitionHooks(child, child.$transition!, true)
9797
}
9898
}
9999
} else {
@@ -102,33 +102,19 @@ export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate(
102102
})
103103

104104
// fallthrough attrs
105-
let fallthroughAttrs = true
106-
if (instance.hasFallthrough) {
107-
renderEffect(() => {
108-
// attrs are accessed in advance
109-
const resolvedAttrs = extend({}, attrs)
110-
const child = findTransitionBlock(children)
111-
if (child) {
112-
// mark single root
113-
;(child as any).$root = true
114-
115-
applyFallthroughProps(child, resolvedAttrs)
116-
// ensure fallthrough attrs are not happened again in
117-
// applyTransitionHooks
118-
fallthroughAttrs = false
119-
}
120-
})
105+
if (
106+
instance.hasFallthrough &&
107+
Object.keys(attrs).length &&
108+
children instanceof Element
109+
) {
110+
renderEffect(() => applyFallthroughProps(children, attrs))
121111
}
122112

123-
const hooks = applyTransitionHooks(
124-
children,
125-
{
126-
state: useTransitionState(),
127-
props: resolvedProps!,
128-
instance: instance,
129-
} as VaporTransitionHooks,
130-
fallthroughAttrs,
131-
)
113+
const hooks = applyTransitionHooks(children, {
114+
state: useTransitionState(),
115+
props: resolvedProps!,
116+
instance: instance,
117+
} as VaporTransitionHooks)
132118

133119
if (resetDisplay && resolvedProps!.appear) {
134120
const child = findTransitionBlock(children)!
@@ -210,7 +196,6 @@ export function resolveTransitionHooks(
210196
export function applyTransitionHooks(
211197
block: Block,
212198
hooks: VaporTransitionHooks,
213-
fallthroughAttrs: boolean = true,
214199
isResolved: boolean = false,
215200
): VaporTransitionHooks {
216201
// filter out comment nodes
@@ -246,13 +231,6 @@ export function applyTransitionHooks(
246231
child.$transition = resolvedHooks
247232
if (isFrag) setTransitionHooksOnFragment(block, resolvedHooks)
248233

249-
// fallthrough attrs
250-
if (fallthroughAttrs && instance.hasFallthrough) {
251-
// mark single root
252-
;(child as any).$root = true
253-
applyFallthroughProps(child, instance.attrs)
254-
}
255-
256234
return resolvedHooks
257235
}
258236

packages/runtime-vapor/src/components/TransitionGroup.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,7 @@ export const VaporTransitionGroup: ObjectVaporComponent = decorate({
154154
const container = createElement(tag)
155155
insert(slottedBlock, container)
156156
// fallthrough attrs
157-
if (instance!.hasFallthrough) {
158-
;(container as any).$root = true
157+
if (instance!.hasFallthrough && Object.keys(instance!.attrs).length) {
159158
renderEffect(() => applyFallthroughProps(container, instance!.attrs))
160159
}
161160
return container

packages/runtime-vapor/src/fragment.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ import {
1111
remove,
1212
} from './block'
1313
import {
14+
type GenericComponentInstance,
1415
type TransitionHooks,
1516
type VNode,
1617
queuePostFlushCb,
18+
setCurrentInstance,
19+
warnExtraneousAttributes,
1720
} from '@vue/runtime-dom'
18-
import type { VaporComponentInstance } from './component'
21+
import { type VaporComponentInstance, applyFallthroughProps } from './component'
1922
import type { NodeRef } from './apiTemplateRef'
2023
import {
2124
applyTransitionHooks,
@@ -28,6 +31,7 @@ import {
2831
locateFragmentEndAnchor,
2932
locateHydrationNode,
3033
} from './dom/hydration'
34+
import { getParentInstance } from './componentSlots'
3135

3236
export class VaporFragment<T extends Block = Block>
3337
implements TransitionOptions
@@ -37,6 +41,7 @@ export class VaporFragment<T extends Block = Block>
3741
nodes: T
3842
vnode?: VNode | null = null
3943
anchor?: Node
44+
parentComponent?: GenericComponentInstance | null
4045
fallback?: BlockFn
4146
insert?: (
4247
parent: ParentNode,
@@ -86,12 +91,14 @@ export class DynamicFragment extends VaporFragment {
8691

8792
constructor(anchorLabel?: string) {
8893
super([])
94+
this.parentComponent = getParentInstance()
8995
if (isHydrating) {
9096
this.anchorLabel = anchorLabel
9197
locateHydrationNode()
9298
} else {
9399
this.anchor =
94100
__DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
101+
if (__DEV__) this.anchorLabel = anchorLabel
95102
}
96103
}
97104

@@ -168,6 +175,8 @@ export class DynamicFragment extends VaporFragment {
168175
transition: VaporTransitionHooks | undefined,
169176
parent: ParentNode | null,
170177
) {
178+
// anchor isConnected indicates the this is an update render
179+
const isUpdate = !!(this.anchor && this.anchor.isConnected)
171180
if (render) {
172181
// try to reuse the kept-alive scope
173182
const scope = this.getScope && this.getScope(this.current)
@@ -177,12 +186,30 @@ export class DynamicFragment extends VaporFragment {
177186
this.scope = new EffectScope()
178187
}
179188

189+
// switch current instance to parent instance during update
190+
let prev
191+
if (this.parentComponent && isUpdate)
192+
prev = setCurrentInstance(this.parentComponent)
180193
this.nodes = this.scope.run(render) || []
194+
if (this.parentComponent && isUpdate) setCurrentInstance(...prev!)
181195

182196
if (transition) {
183197
this.$transition = applyTransitionHooks(this.nodes, transition)
184198
}
185199

200+
// fallthrough attrs
201+
if (
202+
this.parentComponent &&
203+
(this.parentComponent as VaporComponentInstance)!.hasFallthrough &&
204+
Object.keys(this.parentComponent!.attrs).length
205+
) {
206+
if (this.nodes instanceof Element) {
207+
applyFallthroughProps(this.nodes, this.parentComponent!.attrs)
208+
} else if (__DEV__ && this.anchorLabel === 'slot') {
209+
warnExtraneousAttributes(this.parentComponent!.attrs)
210+
}
211+
}
212+
186213
if (this.beforeMount) {
187214
this.beforeMount.forEach(hook =>
188215
hook(this.current, this.nodes, this.scope!),
@@ -191,8 +218,7 @@ export class DynamicFragment extends VaporFragment {
191218

192219
if (parent) {
193220
insert(this.nodes, parent, this.anchor)
194-
// anchor isConnected indicates the this render is updated
195-
if (this.anchor.isConnected && this.updated) {
221+
if (isUpdate && this.updated) {
196222
this.updated.forEach(hook => hook(this.nodes))
197223
}
198224
}

0 commit comments

Comments
 (0)