Skip to content

Commit 914b5c3

Browse files
committed
feat: dom2-mp下支持v-bind="$attrs"
1 parent 1ef8799 commit 914b5c3

File tree

4 files changed

+363
-4
lines changed

4 files changed

+363
-4
lines changed
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import {
2+
type DirectiveNode,
3+
type ElementNode,
4+
NodeTypes,
5+
type SimpleExpressionNode as SimpleExpression,
6+
baseParse as parse,
7+
transform,
8+
} from '@vue/compiler-core'
9+
import { transformVBindAttrs } from '../src/transforms/transformVBindAttrs'
10+
11+
function runTransform(template: string) {
12+
const ast = parse(template)
13+
transform(ast, {
14+
nodeTransforms: [transformVBindAttrs as any],
15+
})
16+
return ast.children[0] as ElementNode
17+
}
18+
19+
function getProp(node: ElementNode, name: string) {
20+
return node.props.find((p) => {
21+
if (
22+
p.type === NodeTypes.DIRECTIVE &&
23+
p.name === 'bind' &&
24+
p.arg?.type === NodeTypes.SIMPLE_EXPRESSION &&
25+
p.arg.isStatic &&
26+
p.arg.content === name
27+
) {
28+
return true
29+
}
30+
return false
31+
}) as DirectiveNode | undefined
32+
}
33+
34+
function getOnProp(node: ElementNode, name: string) {
35+
return node.props.find((p) => {
36+
if (
37+
p.type === NodeTypes.DIRECTIVE &&
38+
p.name === 'on' &&
39+
p.arg?.type === NodeTypes.SIMPLE_EXPRESSION &&
40+
p.arg.isStatic &&
41+
p.arg.content === name
42+
) {
43+
return true
44+
}
45+
return false
46+
}) as DirectiveNode | undefined
47+
}
48+
49+
function countOnProp(node: ElementNode, name: string) {
50+
return node.props.filter((p) => {
51+
return (
52+
p.type === NodeTypes.DIRECTIVE &&
53+
p.name === 'on' &&
54+
p.arg?.type === NodeTypes.SIMPLE_EXPRESSION &&
55+
p.arg.isStatic &&
56+
p.arg.content === name
57+
)
58+
}).length
59+
}
60+
61+
function formatProps(node: ElementNode) {
62+
return node.props.map((p) => {
63+
if (p.type === NodeTypes.DIRECTIVE) {
64+
const arg =
65+
p.arg?.type === NodeTypes.SIMPLE_EXPRESSION && p.arg.isStatic
66+
? p.arg.content
67+
: ''
68+
const exp =
69+
p.exp?.type === NodeTypes.SIMPLE_EXPRESSION ? p.exp.content : ''
70+
if (p.name === 'bind') {
71+
return arg ? `:${arg}=${exp}` : `v-bind=${exp}`
72+
}
73+
if (p.name === 'on') {
74+
return arg ? `@${arg}=${exp}` : `v-on=${exp}`
75+
}
76+
return `v-${p.name}`
77+
}
78+
if (p.type === NodeTypes.ATTRIBUTE) {
79+
return p.value ? `${p.name}=${p.value.content}` : p.name
80+
}
81+
return ''
82+
})
83+
}
84+
85+
describe('compiler: transform v-bind="$attrs"', () => {
86+
test('root node with v-bind="$attrs"', () => {
87+
const node = runTransform(`<view v-bind="$attrs">123</view>`)
88+
89+
// should remove v-bind="$attrs"
90+
expect(
91+
node.props.find(
92+
(p) =>
93+
p.type === NodeTypes.DIRECTIVE &&
94+
p.name === 'bind' &&
95+
!p.arg &&
96+
(p.exp as SimpleExpression)?.content === '$attrs'
97+
)
98+
).toBeUndefined()
99+
100+
// Class
101+
const classProp = getProp(node, 'class')
102+
expect(classProp).toBeDefined()
103+
expect((classProp!.exp as SimpleExpression).content).toBe('$attrs.class')
104+
105+
// Style
106+
const styleProp = getProp(node, 'style')
107+
expect(styleProp).toBeDefined()
108+
expect((styleProp!.exp as SimpleExpression).content).toBe('$attrs.style')
109+
110+
// Id
111+
const idProp = getProp(node, 'id')
112+
expect(idProp).toBeDefined()
113+
expect((idProp!.exp as SimpleExpression).content).toBe('$attrs.id')
114+
115+
// Click
116+
const clickProp = getOnProp(node, 'click')
117+
expect(clickProp).toBeDefined()
118+
expect((clickProp!.exp as SimpleExpression).content).toBe('$attrs.onClick')
119+
120+
expect(formatProps(node)).toEqual([
121+
':class=$attrs.class',
122+
':style=$attrs.style',
123+
'@click=$attrs.onClick',
124+
':id=$attrs.id',
125+
])
126+
})
127+
128+
test('nested node with v-bind="$attrs"', () => {
129+
const ast = parse(`<view><view v-bind="$attrs">123</view></view>`)
130+
transform(ast, {
131+
nodeTransforms: [transformVBindAttrs as any],
132+
})
133+
const node = (ast.children[0] as ElementNode).children[0] as ElementNode
134+
expect(getProp(node, 'class')).toBeDefined()
135+
})
136+
137+
test('root node with v-bind="$attrs" and existing style', () => {
138+
// transformVBindAttrs only merges with DYNAMIC style/class. Static style is kept separate (Vue behavior).
139+
// The plugin adds :style="$attrs.style".
140+
const node = runTransform(
141+
`<view v-bind="$attrs" style="color:red">123</view>`
142+
)
143+
const styleProp = getProp(node, 'style')
144+
expect(styleProp).toBeDefined()
145+
expect((styleProp!.exp as SimpleExpression).content).toBe('$attrs.style')
146+
})
147+
148+
test('root node with v-bind="$attrs" and existing dynamic style', () => {
149+
const node = runTransform(
150+
`<view v-bind="$attrs" :style="{ color: 'red' }">123</view>`
151+
)
152+
const styleProp = getProp(node, 'style')
153+
expect(styleProp).toBeDefined()
154+
// Should merge: [original, $attrs.style]
155+
expect((styleProp!.exp as SimpleExpression).content).toBe(
156+
`[{ color: 'red' }, $attrs.style]`
157+
)
158+
expect(formatProps(node)).toEqual([
159+
':class=$attrs.class',
160+
'@click=$attrs.onClick',
161+
':id=$attrs.id',
162+
":style=[{ color: 'red' }, $attrs.style]",
163+
])
164+
})
165+
166+
test('should merge with existing dynamic class', () => {
167+
const node = runTransform(`<view :class="foo" v-bind="$attrs"/>`)
168+
const classProp = getProp(node, 'class')
169+
expect(classProp).toBeDefined()
170+
expect((classProp!.exp as SimpleExpression).content).toBe(
171+
`[foo, $attrs.class]`
172+
)
173+
expect(formatProps(node)).toEqual([
174+
':class=[foo, $attrs.class]',
175+
':style=$attrs.style',
176+
'@click=$attrs.onClick',
177+
':id=$attrs.id',
178+
])
179+
})
180+
181+
test('should skip id if already defined after v-bind', () => {
182+
const node = runTransform(`<view v-bind="$attrs" id="foo"/>`)
183+
// id="foo" is static attribute, not bound.
184+
// transformVBindAttrs checks for both Attribute 'id' and Directive 'bind' 'id'.
185+
// If found AFTER v-bind, it skips adding :id.
186+
expect(getProp(node, 'id')).toBeUndefined()
187+
expect(formatProps(node)).toEqual([
188+
':class=$attrs.class',
189+
':style=$attrs.style',
190+
'@click=$attrs.onClick',
191+
'id=foo',
192+
])
193+
})
194+
195+
test('should add id if defined before v-bind', () => {
196+
const node = runTransform(`<view id="foo" v-bind="$attrs"/>`)
197+
// id defined BEFORE. The check slice(i+1) won't find it.
198+
// So it adds :id="$attrs.id"
199+
expect(getProp(node, 'id')).toBeDefined()
200+
expect(formatProps(node)).toEqual([
201+
'id=foo',
202+
':class=$attrs.class',
203+
':style=$attrs.style',
204+
'@click=$attrs.onClick',
205+
':id=$attrs.id',
206+
])
207+
})
208+
209+
test('should not transform v-bind with argument', () => {
210+
const node = runTransform(`<view v-bind:class="$attrs"/>`)
211+
const classProp = getProp(node, 'class')
212+
expect(classProp).toBeDefined()
213+
expect((classProp!.exp as SimpleExpression).content).toBe('$attrs')
214+
expect(getProp(node, 'style')).toBeUndefined()
215+
expect(getProp(node, 'id')).toBeUndefined()
216+
expect(getOnProp(node, 'click')).toBeUndefined()
217+
expect(formatProps(node)).toEqual([':class=$attrs'])
218+
})
219+
220+
test('should not add click if already defined', () => {
221+
const node = runTransform(`<view v-bind="$attrs" @click="foo">123</view>`)
222+
const clickProp = getOnProp(node, 'click')
223+
expect(clickProp).toBeDefined()
224+
expect((clickProp!.exp as SimpleExpression).content).toBe('foo')
225+
expect(countOnProp(node, 'click')).toBe(1)
226+
expect(formatProps(node)).toEqual([
227+
':class=$attrs.class',
228+
':style=$attrs.style',
229+
':id=$attrs.id',
230+
'@click=foo',
231+
])
232+
})
233+
})

packages/uni-mp-compiler/src/compile.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { transformTag } from './transforms/transformTag'
2424
import { transformHtml } from './transforms/vHtml'
2525
import { transformText } from './transforms/vText'
2626
import { transformAttr } from './transforms/transformAttr'
27+
import { transformVBindAttrs } from './transforms/transformVBindAttrs'
2728
import { FILTER_MODULE_NAME } from './transforms/utils'
2829

2930
export type TransformPreset = [
@@ -41,6 +42,7 @@ export function getBaseTransformPreset({
4142
// order is important
4243
const nodeTransforms = [
4344
transformRoot,
45+
transformVBindAttrs,
4446
transformAttr,
4547
transformTag,
4648
transformHtml,

packages/uni-mp-compiler/src/transforms/transformElement.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,15 @@ export function processProps(
217217
createMPCompilerError(MPErrorCodes.X_V_ON_NO_ARGUMENT, loc)
218218
)
219219
}
220-
if (isVBind && (!isComponent || isPluginComponent)) {
221-
context.onError(
222-
createMPCompilerError(MPErrorCodes.X_V_BIND_NO_ARGUMENT, loc)
223-
)
220+
if (isVBind) {
221+
const isVBindAttrs =
222+
prop.exp?.type === NodeTypes.SIMPLE_EXPRESSION &&
223+
prop.exp.content === '$attrs'
224+
if (!isVBindAttrs) {
225+
context.onError(
226+
createMPCompilerError(MPErrorCodes.X_V_BIND_NO_ARGUMENT, loc)
227+
)
228+
}
224229
}
225230
continue
226231
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import {
2+
type DirectiveNode,
3+
NodeTypes,
4+
createSimpleExpression,
5+
isStaticArgOf,
6+
} from '@vue/compiler-core'
7+
import { isElementNode } from '@dcloudio/uni-cli-shared'
8+
import type { NodeTransform } from '../transform'
9+
10+
export const transformVBindAttrs: NodeTransform = (node, context) => {
11+
if (!isElementNode(node)) {
12+
return
13+
}
14+
const props = node.props
15+
for (let i = 0; i < props.length; i++) {
16+
const prop = props[i]
17+
if (
18+
prop.type === NodeTypes.DIRECTIVE &&
19+
prop.name === 'bind' &&
20+
!prop.arg &&
21+
prop.modifiers.length === 0
22+
) {
23+
if (
24+
prop.exp &&
25+
prop.exp.type === NodeTypes.SIMPLE_EXPRESSION &&
26+
prop.exp.content === '$attrs'
27+
) {
28+
const content = prop.exp.content
29+
const newProps: DirectiveNode[] = []
30+
// :class
31+
const classProp = props.find(
32+
(p) =>
33+
p.type === NodeTypes.DIRECTIVE &&
34+
p.name === 'bind' &&
35+
isStaticArgOf(p.arg, 'class')
36+
) as DirectiveNode
37+
if (classProp && classProp.exp?.type === NodeTypes.SIMPLE_EXPRESSION) {
38+
classProp.exp.content = `[${classProp.exp.content}, ${content}.class]`
39+
} else {
40+
newProps.push(
41+
createBindDirective('class', `${content}.class`, prop.loc)
42+
)
43+
}
44+
// :style
45+
const styleProp = props.find(
46+
(p) =>
47+
p.type === NodeTypes.DIRECTIVE &&
48+
p.name === 'bind' &&
49+
isStaticArgOf(p.arg, 'style')
50+
) as DirectiveNode
51+
if (styleProp && styleProp.exp?.type === NodeTypes.SIMPLE_EXPRESSION) {
52+
styleProp.exp.content = `[${styleProp.exp.content}, ${content}.style]`
53+
} else {
54+
newProps.push(
55+
createBindDirective('style', `${content}.style`, prop.loc)
56+
)
57+
}
58+
// @click (only add if not already defined)
59+
const hasClick = props.some(
60+
(p) =>
61+
p.type === NodeTypes.DIRECTIVE &&
62+
p.name === 'on' &&
63+
isStaticArgOf(p.arg, 'click')
64+
)
65+
if (!hasClick) {
66+
newProps.push(
67+
createOnDirective('click', `${content}.onClick`, prop.loc)
68+
)
69+
}
70+
// :id
71+
// 查找后面是否还有 id,如果有,则忽略当前 id
72+
const hasId = props
73+
.slice(i + 1)
74+
.some(
75+
(p) =>
76+
(p.type === NodeTypes.ATTRIBUTE && p.name === 'id') ||
77+
(p.type === NodeTypes.DIRECTIVE &&
78+
p.name === 'bind' &&
79+
isStaticArgOf(p.arg, 'id'))
80+
)
81+
if (!hasId) {
82+
newProps.push(createBindDirective('id', `${content}.id`, prop.loc))
83+
}
84+
props.splice(i, 1, ...newProps)
85+
i += newProps.length - 1
86+
}
87+
}
88+
}
89+
}
90+
91+
function createBindDirective(
92+
name: string,
93+
value: string,
94+
loc: any
95+
): DirectiveNode {
96+
return {
97+
type: NodeTypes.DIRECTIVE,
98+
name: 'bind',
99+
modifiers: [],
100+
loc,
101+
arg: createSimpleExpression(name, true, loc),
102+
exp: createSimpleExpression(value, false, loc),
103+
}
104+
}
105+
106+
function createOnDirective(
107+
name: string,
108+
value: string,
109+
loc: any
110+
): DirectiveNode {
111+
return {
112+
type: NodeTypes.DIRECTIVE,
113+
name: 'on',
114+
modifiers: [],
115+
loc,
116+
arg: createSimpleExpression(name, true, loc),
117+
exp: createSimpleExpression(value, false, loc),
118+
}
119+
}

0 commit comments

Comments
 (0)