refactor: new directive API

This commit is contained in:
Amour1688 2020-07-13 23:44:52 +08:00
parent d495546883
commit 672f27d258
5 changed files with 197 additions and 128 deletions

View File

@ -137,6 +137,28 @@ export default {
}
```
* 或者使用这种实现
```jsx
<input vModel={[val, ['trim']]} />
```
```jsx
<A vModel={[val, 'foo', ['bar']]} />
```
会变编译成:
```js
h(A, {
'foo': val,
"fooModifiers": {
"bar": true
},
"onUpdate:foo": $event => val = $event
})
```
自定义指令
```jsx
@ -145,17 +167,15 @@ const App = {
setup() {
return () => (
<a
vCustom={{
value: 123,
modifiers: { modifier: true },
arg: 'arg',
}}
vCustom={[val, 'arg', ['a', 'b']]}
/>
);
},
}
```
> 注意:如果想要使用 `arg`, 第二个参数需要为字符串
### 插槽
```jsx

View File

@ -120,7 +120,7 @@ const App = {
v-model
* You should use underscore (`_`) instead of dot (`.`) for modifiers (`vModel_trim={this.test}`)
* You can use underscore (`_`) instead of dot (`.`) for modifiers (`vModel_trim={this.test}`)
```jsx
export default {
@ -138,6 +138,28 @@ export default {
}
```
* Or you can use this proposal.
```jsx
<input vModel={[val, ['trim']]} />
```
```jsx
<A vModel={[val, 'foo', ['bar']]} />
```
Will compile to:
```js
h(A, {
'foo': val,
"fooModifiers": {
"bar": true
},
"onUpdate:foo": $event => val = $event
})
```
custom directive
```jsx
@ -146,17 +168,15 @@ const App = {
setup() {
return () => (
<a
vCustom={{
value: 123,
modifiers: { modifier: true },
arg: 'arg',
}}
vCustom={[val, 'arg', ['a', 'b']]}
/>
);
},
}
```
> Note: You should pass the second param as string for using `arg`.
### Slot
```jsx

View File

@ -0,0 +1,131 @@
import * as t from '@babel/types';
import { NodePath } from '@babel/traverse';
import { createIdentifier } from './utils';
import { State, ExcludesBoolean } from './';
/**
* Get JSX element type
*
* @param path Path<JSXOpeningElement>
*/
const getType = (path: NodePath<t.JSXOpeningElement>) => {
const typePath = path
.get('attributes')
.find((attribute) => {
if (!t.isJSXAttribute(attribute)) {
return false;
}
return t.isJSXIdentifier(attribute.get('name'))
&& (attribute.get('name') as NodePath<t.JSXIdentifier>).get('name') === 'type'
&& t.isStringLiteral(attribute.get('value'))
},
);
return typePath ? typePath.get('value.value') : '';
};
const parseModifiers = (value: t.Expression) => {
let modifiers: string[] = [];
if (t.isArrayExpression(value)) {
modifiers = (value as t.ArrayExpression).elements.map(el => t.isStringLiteral(el) ? el.value : '').filter(Boolean)
}
return modifiers;
}
const parseDirectives = (args: {
name: string,
path: NodePath<t.JSXAttribute>,
value: t.StringLiteral | t.Expression | null,
state: State,
tag: t.Identifier | t.MemberExpression | t.StringLiteral | t.CallExpression,
isComponent: boolean
}) => {
const {
name, path, value, state, tag, isComponent,
} = args
let modifiers: string[] = name.split('_');
let arg;
let val;
const directiveName: string = modifiers.shift()
?.replace(/^v/, '')
.replace(/^-/, '')
.replace(/^\S/, (s: string) => s.toLowerCase()) || '';
if (directiveName === 'model' && !t.isJSXExpressionContainer(path.get('value'))) {
throw new Error('You have to use JSX Expression inside your v-model');
}
const hasDirective = directiveName !== 'model' || (directiveName === 'model' && !isComponent);
if (t.isArrayExpression(value)) {
const { elements } = value as t.ArrayExpression;
const [first, second, third] = elements;
if (t.isStringLiteral(second)) {
arg = second;
modifiers = parseModifiers(third as t.Expression);
} else if (second) {
modifiers = parseModifiers(second as t.Expression);
}
val = first;
}
const modifiersSet = new Set(modifiers);
return {
directiveName,
modifiers: modifiersSet,
value: val || value,
arg,
directive: hasDirective ? [
resolveDirective(path, state, tag, directiveName),
val as t.Identifier,
!!modifiersSet.size && t.unaryExpression('void', t.numericLiteral(0), true),
!!modifiersSet.size && t.objectExpression(
[...modifiersSet].map(
(modifier) => t.objectProperty(
t.identifier(modifier as string),
t.booleanLiteral(true),
),
),
),
].filter(Boolean as any as ExcludesBoolean) : undefined,
};
};
const resolveDirective = (path: NodePath<t.JSXAttribute>, state: State, tag: any, directiveName: string) => {
if (directiveName === 'show') {
return createIdentifier(state, 'vShow');
}
if (directiveName === 'model') {
let modelToUse;
const type = getType(path.parentPath as NodePath<t.JSXOpeningElement>);
switch (tag.value) {
case 'select':
modelToUse = createIdentifier(state, 'vModelSelect');
break;
case 'textarea':
modelToUse = createIdentifier(state, 'vModelText');
break;
default:
switch (type) {
case 'checkbox':
modelToUse = createIdentifier(state, 'vModelCheckbox');
break;
case 'radio':
modelToUse = createIdentifier(state, 'vModelRadio');
break;
default:
modelToUse = createIdentifier(state, 'vModelText');
}
}
return modelToUse;
}
return t.callExpression(
createIdentifier(state, 'resolveDirective'), [
t.stringLiteral(directiveName),
],
);
};
export default parseDirectives;

View File

@ -10,10 +10,10 @@ import {
getJSXAttributeName,
transformJSXText,
transformJSXExpressionContainer,
parseDirectives,
isFragment,
walksScope,
} from './utils';
import parseDirectives from './parseDirectives';
import { PatchFlags, PatchFlagNames } from './patchFlags';
import { State, ExcludesBoolean } from './';
@ -198,7 +198,7 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
return;
}
if (isDirective(name)) {
const { directive, modifiers, directiveName } = parseDirectives({
const { directive, modifiers, value, arg, directiveName } = parseDirectives({
tag,
isComponent,
name,
@ -206,6 +206,8 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
state,
value: attributeValue,
});
const argVal = (arg as t.StringLiteral)?.value;
const propName = argVal || 'modelValue';
if (directiveName === 'slots') {
slots = attributeValue;
@ -215,16 +217,16 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
} else {
// must be v-model and is a component
properties.push(t.objectProperty(
t.stringLiteral('modelValue'),
arg || t.stringLiteral('modelValue'),
// @ts-ignore
attributeValue,
value,
));
dynamicPropNames.add('modelValue');
dynamicPropNames.add(propName);
if (modifiers.size) {
properties.push(t.objectProperty(
t.stringLiteral('modelModifiers'),
t.stringLiteral(`${argVal || 'model'}Modifiers`),
t.objectExpression(
[...modifiers].map((modifier) => (
t.objectProperty(
@ -237,17 +239,19 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
}
}
if (directiveName === 'model' && attributeValue) {
console.log(value)
if (directiveName === 'model' && value) {
properties.push(t.objectProperty(
t.stringLiteral('onUpdate:modelValue'),
t.stringLiteral(`onUpdate:${propName}`),
t.arrowFunctionExpression(
[t.identifier('$event')],
// @ts-ignore
t.assignmentExpression('=', attributeValue, t.identifier('$event')),
t.assignmentExpression('=', value, t.identifier('$event')),
),
));
dynamicPropNames.add('onUpdate:modelValue');
dynamicPropNames.add(`onUpdate:${propName}`);
}
return;
}

View File

@ -2,7 +2,7 @@ import * as t from '@babel/types';
import htmlTags from 'html-tags';
import svgTags from 'svg-tags';
import { NodePath } from '@babel/traverse';
import { State, ExcludesBoolean } from './';
import { State } from './';
/**
* create Identifier
@ -169,111 +169,6 @@ const transformJSXSpreadChild = (
path: NodePath<t.JSXSpreadChild>
): t.SpreadElement => t.spreadElement(path.get('expression').node);
/**
* Get JSX element type
*
* @param path Path<JSXOpeningElement>
*/
const getType = (path: NodePath<t.JSXOpeningElement>) => {
const typePath = path
.get('attributes')
.find((attribute) => {
if (!t.isJSXAttribute(attribute)) {
return false;
}
return t.isJSXIdentifier(attribute.get('name'))
&& (attribute.get('name') as NodePath<t.JSXIdentifier>).get('name') === 'type'
&& t.isStringLiteral(attribute.get('value'))
},
);
return typePath ? typePath.get('value.value') : '';
};
const resolveDirective = (path: NodePath<t.JSXAttribute>, state: State, tag: any, directiveName: string) => {
if (directiveName === 'show') {
return createIdentifier(state, 'vShow');
} if (directiveName === 'model') {
let modelToUse;
const type = getType(path.parentPath as NodePath<t.JSXOpeningElement>);
switch (tag.value) {
case 'select':
modelToUse = createIdentifier(state, 'vModelSelect');
break;
case 'textarea':
modelToUse = createIdentifier(state, 'vModelText');
break;
default:
switch (type) {
case 'checkbox':
modelToUse = createIdentifier(state, 'vModelCheckbox');
break;
case 'radio':
modelToUse = createIdentifier(state, 'vModelRadio');
break;
default:
modelToUse = createIdentifier(state, 'vModelText');
}
}
return modelToUse;
}
return t.callExpression(
createIdentifier(state, 'resolveDirective'), [
t.stringLiteral(directiveName),
],
);
};
/**
* Parse directives metadata
*
* @param path JSXAttribute
* @returns null | Object<{ modifiers: Set<string>, valuePath: Path<Expression>}>
*/
const parseDirectives = (args: {
name: string,
path: NodePath<t.JSXAttribute>,
value: t.StringLiteral | t.Expression | null,
state: State,
tag: t.Identifier | t.MemberExpression | t.StringLiteral | t.CallExpression,
isComponent: boolean
}) => {
const {
name, path, value, state, tag, isComponent,
} = args
const modifiers: string[] = name.split('_');
const directiveName: string = modifiers.shift()
?.replace(/^v/, '')
.replace(/^-/, '')
.replace(/^\S/, (s: string) => s.toLowerCase()) || '';
if (directiveName === 'model' && !t.isJSXExpressionContainer(path.get('value'))) {
throw new Error('You have to use JSX Expression inside your v-model');
}
const modifiersSet = new Set(modifiers);
const hasDirective = directiveName !== 'model' || (directiveName === 'model' && !isComponent);
return {
directiveName,
modifiers: modifiersSet,
directive: hasDirective ? [
resolveDirective(path, state, tag, directiveName),
value,
!!modifiersSet.size && t.unaryExpression('void', t.numericLiteral(0), true),
!!modifiersSet.size && t.objectExpression(
[...modifiersSet].map(
(modifier) => t.objectProperty(
t.identifier(modifier),
t.booleanLiteral(true),
),
),
),
].filter(Boolean as any as ExcludesBoolean) : undefined,
};
};
const walksScope = (path: NodePath, name: string) => {
if (path.scope.hasBinding(name) && path.parentPath) {
path.parentPath.setData('optimize', false);
@ -291,7 +186,6 @@ export {
transformJSXText,
transformJSXSpreadChild,
transformJSXExpressionContainer,
parseDirectives,
isFragment,
walksScope,
};