mirror of
https://github.com/vuejs/babel-plugin-jsx.git
synced 2024-11-10 09:39:14 +08:00
feat: should support passing object slots via JSX children (#204)
* feat: should support passing object slots via JSX children * feat: add unit test * feat: remove `cloneDeep` of `isSlot` * chore(deps): add `@babel/template`
This commit is contained in:
parent
b443f847ca
commit
5b6323f22a
@ -11,11 +11,10 @@
|
||||
"url": "git+https://github.com/vuejs/jsx-next.git"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "npm run build && webpack-dev-server",
|
||||
"build": "tsc",
|
||||
"lint": "eslint 'src/*.ts'",
|
||||
"test": "npm run build && jest --coverage",
|
||||
"prepublishOnly": "npm run build"
|
||||
"test": "yarn build && jest --coverage",
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/vuejs/jsx-next/issues"
|
||||
@ -26,6 +25,7 @@
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.0.0",
|
||||
"@babel/plugin-syntax-jsx": "^7.0.0",
|
||||
"@babel/template": "^7.0.0",
|
||||
"@babel/traverse": "^7.0.0",
|
||||
"@babel/types": "^7.0.0",
|
||||
"@vue/babel-helper-vue-transform-on": "^1.0.0-rc.2",
|
||||
|
@ -1,5 +1,6 @@
|
||||
import * as t from '@babel/types';
|
||||
import * as BabelCore from '@babel/core';
|
||||
import template from '@babel/template';
|
||||
import syntaxJsx from '@babel/plugin-syntax-jsx';
|
||||
import { addNamed, isModule, addNamespace } from '@babel/helper-module-imports';
|
||||
import { NodePath } from '@babel/traverse';
|
||||
@ -23,7 +24,6 @@ export type ExcludesBoolean = <T>(x: T | false | true) => x is T;
|
||||
|
||||
const hasJSX = (parentPath: NodePath) => {
|
||||
let fileHasJSX = false;
|
||||
|
||||
parentPath.traverse({
|
||||
JSXElement(path) { // skip ts error
|
||||
fileHasJSX = true;
|
||||
@ -62,6 +62,7 @@ export default ({ types }: typeof BabelCore) => ({
|
||||
'resolveDirective',
|
||||
'mergeProps',
|
||||
'createTextVNode',
|
||||
'isVNode',
|
||||
];
|
||||
if (isModule(path)) {
|
||||
// import { createVNode } from "vue";
|
||||
@ -83,6 +84,24 @@ export default ({ types }: typeof BabelCore) => ({
|
||||
return identifier;
|
||||
});
|
||||
});
|
||||
state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => {
|
||||
if (importMap.runtimeIsSlot) {
|
||||
return importMap.runtimeIsSlot;
|
||||
}
|
||||
const { name: isVNodeName } = state.get('isVNode')();
|
||||
const isSlot = path.scope.generateUidIdentifier('isSlot');
|
||||
const ast = template.ast`
|
||||
function ${isSlot.name}(s) {
|
||||
return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${isVNodeName}(s));
|
||||
}
|
||||
`;
|
||||
const lastImport = (path.get('body') as NodePath[]).filter((p) => p.isImportDeclaration()).pop();
|
||||
if (lastImport) {
|
||||
lastImport.insertAfter(ast);
|
||||
}
|
||||
importMap.runtimeIsSlot = isSlot;
|
||||
return isSlot;
|
||||
});
|
||||
} else {
|
||||
// var _vue = require('vue');
|
||||
let sourceName = '';
|
||||
|
@ -83,12 +83,10 @@ const transformJSXElement = (
|
||||
|
||||
const slotFlag = path.getData('slotFlag') || SlotFlags.STABLE;
|
||||
|
||||
const createVNode = t.callExpression(createIdentifier(state, 'createVNode'), [
|
||||
tag,
|
||||
props,
|
||||
(children.length || slots) ? (
|
||||
isComponent
|
||||
? t.objectExpression([
|
||||
let VNodeChild;
|
||||
|
||||
if (children.length > 1 || slots) {
|
||||
VNodeChild = isComponent ? t.objectExpression([
|
||||
!!children.length && t.objectProperty(
|
||||
t.identifier('default'),
|
||||
t.arrowFunctionExpression([], t.arrayExpression(buildIIFE(path, children))),
|
||||
@ -102,9 +100,82 @@ const transformJSXElement = (
|
||||
t.identifier('_'),
|
||||
t.numericLiteral(slotFlag),
|
||||
),
|
||||
].filter(Boolean as any))
|
||||
: t.arrayExpression(children)
|
||||
) : t.nullLiteral(),
|
||||
].filter(Boolean as any)) : t.arrayExpression(children);
|
||||
} else if (children.length === 1) {
|
||||
const child = children[0];
|
||||
if (t.isIdentifier(child)) {
|
||||
VNodeChild = t.conditionalExpression(
|
||||
t.callExpression(state.get('@vue/babel-plugin-jsx/runtimeIsSlot')(), [child]),
|
||||
child,
|
||||
t.objectExpression([
|
||||
t.objectProperty(
|
||||
t.identifier('default'),
|
||||
t.arrowFunctionExpression([], t.arrayExpression(buildIIFE(path, [child]))),
|
||||
),
|
||||
optimize && t.objectProperty(
|
||||
t.identifier('_'),
|
||||
t.numericLiteral(slotFlag),
|
||||
) as any,
|
||||
].filter(Boolean)),
|
||||
);
|
||||
} else if (
|
||||
t.isCallExpression(child) && child.loc && isComponent
|
||||
) { // the element was generated and doesn't have location information
|
||||
const slotId = path.scope.generateUidIdentifier('slot');
|
||||
const scope = path.hub.getScope();
|
||||
if (scope) {
|
||||
scope.push({
|
||||
id: slotId,
|
||||
kind: 'let',
|
||||
});
|
||||
}
|
||||
|
||||
VNodeChild = t.conditionalExpression(
|
||||
t.callExpression(
|
||||
state.get('@vue/babel-plugin-jsx/runtimeIsSlot')(),
|
||||
[t.assignmentExpression('=', slotId, child)],
|
||||
),
|
||||
slotId,
|
||||
t.objectExpression([
|
||||
t.objectProperty(
|
||||
t.identifier('default'),
|
||||
t.arrowFunctionExpression([], t.arrayExpression(buildIIFE(path, [slotId]))),
|
||||
),
|
||||
optimize && t.objectProperty(
|
||||
t.identifier('_'),
|
||||
t.numericLiteral(slotFlag),
|
||||
) as any,
|
||||
].filter(Boolean)),
|
||||
);
|
||||
} else if (t.isFunctionExpression(child) || t.isArrowFunctionExpression(child)) {
|
||||
VNodeChild = t.objectExpression([
|
||||
t.objectProperty(
|
||||
t.identifier('default'),
|
||||
child,
|
||||
),
|
||||
]);
|
||||
} else if (t.isObjectExpression(child)) {
|
||||
VNodeChild = t.objectExpression([
|
||||
...child.properties,
|
||||
optimize && t.objectProperty(
|
||||
t.identifier('_'),
|
||||
t.numericLiteral(slotFlag),
|
||||
),
|
||||
].filter(Boolean as any));
|
||||
} else {
|
||||
VNodeChild = isComponent ? t.objectExpression([
|
||||
t.objectProperty(
|
||||
t.identifier('default'),
|
||||
t.arrowFunctionExpression([], t.arrayExpression([child])),
|
||||
),
|
||||
]) : t.arrayExpression([child]);
|
||||
}
|
||||
}
|
||||
|
||||
const createVNode = t.callExpression(createIdentifier(state, 'createVNode'), [
|
||||
tag,
|
||||
props,
|
||||
VNodeChild || t.nullLiteral(),
|
||||
!!patchFlag && optimize && t.numericLiteral(patchFlag),
|
||||
!!dynamicPropNames.size && optimize
|
||||
&& t.arrayExpression(
|
||||
|
@ -127,9 +127,53 @@ exports[`override props single: single 1`] = `
|
||||
_createVNode(\\"div\\", a, null);"
|
||||
`;
|
||||
|
||||
exports[`reassign variable as component: reassign variable as component 1`] = `
|
||||
exports[`passing object slots via JSX children multiple expressions: multiple expressions 1`] = `
|
||||
"import { createVNode as _createVNode } from \\"vue\\";
|
||||
import { resolveComponent as _resolveComponent } from \\"vue\\";
|
||||
|
||||
_createVNode(_resolveComponent(\\"A\\"), null, {
|
||||
default: () => [foo, bar],
|
||||
_: 1
|
||||
});"
|
||||
`;
|
||||
|
||||
exports[`passing object slots via JSX children single expression, function expression: single expression, function expression 1`] = `
|
||||
"import { createVNode as _createVNode } from \\"vue\\";
|
||||
import { resolveComponent as _resolveComponent } from \\"vue\\";
|
||||
|
||||
_createVNode(_resolveComponent(\\"A\\"), null, {
|
||||
default: () => \\"foo\\"
|
||||
});"
|
||||
`;
|
||||
|
||||
exports[`passing object slots via JSX children single expression, non-literal value: runtime check: single expression, non-literal value: runtime check 1`] = `
|
||||
"import { createVNode as _createVNode } from \\"vue\\";
|
||||
import { isVNode as _isVNode } from \\"vue\\";
|
||||
import { resolveComponent as _resolveComponent } from \\"vue\\";
|
||||
|
||||
let _slot;
|
||||
|
||||
function _isSlot(s) {
|
||||
return typeof s === 'function' || Object.prototype.toString.call(s) === '[object Object]' && !_isVNode(s);
|
||||
}
|
||||
|
||||
const foo = () => 1;
|
||||
|
||||
_createVNode(_resolveComponent(\\"A\\"), null, _isSlot(_slot = foo()) ? _slot : {
|
||||
default: () => [_slot],
|
||||
_: 1
|
||||
});"
|
||||
`;
|
||||
|
||||
exports[`reassign variable as component: reassign variable as component 1`] = `
|
||||
"import { isVNode as _isVNode } from \\"vue\\";
|
||||
import { createVNode as _createVNode } from \\"vue\\";
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
function _isSlot(s) {
|
||||
return typeof s === 'function' || Object.prototype.toString.call(s) === '[object Object]' && !_isVNode(s);
|
||||
}
|
||||
|
||||
let a = 1;
|
||||
const A = defineComponent({
|
||||
setup(_, {
|
||||
@ -146,7 +190,7 @@ const _a = function () {
|
||||
return a;
|
||||
}();
|
||||
|
||||
a = _createVNode(A, null, {
|
||||
a = _createVNode(A, null, _isSlot(a) ? a : {
|
||||
default: () => [_a],
|
||||
_: 2
|
||||
});"
|
||||
|
@ -1,5 +1,9 @@
|
||||
import {
|
||||
reactive, ref, defineComponent, CSSProperties, ComponentPublicInstance,
|
||||
reactive,
|
||||
ref,
|
||||
defineComponent,
|
||||
CSSProperties,
|
||||
ComponentPublicInstance,
|
||||
} from 'vue';
|
||||
import { shallowMount, mount, VueWrapper } from '@vue/test-utils';
|
||||
|
||||
@ -66,9 +70,7 @@ describe('Transform JSX', () => {
|
||||
|
||||
const wrapper = mount({
|
||||
render() {
|
||||
return (
|
||||
<Child class="parent" foo={1} />
|
||||
);
|
||||
return <Child class="parent" foo={1} />;
|
||||
},
|
||||
});
|
||||
expect(wrapper.classes()).toStrictEqual([]);
|
||||
@ -123,7 +125,7 @@ describe('Transform JSX', () => {
|
||||
const wrapper = shallowMount({
|
||||
setup() {
|
||||
// @ts-ignore
|
||||
return () => <div class="a" {...{ class: 'b' } } />;
|
||||
return () => <div class="a" {...{ class: 'b' }} />;
|
||||
},
|
||||
});
|
||||
expect(wrapper.classes().sort()).toEqual(['a', 'b'].sort());
|
||||
@ -145,10 +147,12 @@ describe('Transform JSX', () => {
|
||||
const wrapper = shallowMount({
|
||||
setup() {
|
||||
// @ts-ignore
|
||||
return () => <div { ...propsA } { ...propsB } />;
|
||||
return () => <div {...propsA} {...propsB} />;
|
||||
},
|
||||
});
|
||||
expect(wrapper.html()).toBe('<div style="color: blue; width: 300px; height: 300px;"></div>');
|
||||
expect(wrapper.html()).toBe(
|
||||
'<div style="color: blue; width: 300px; height: 300px;"></div>',
|
||||
);
|
||||
});
|
||||
|
||||
test('JSXSpreadChild', () => {
|
||||
@ -259,7 +263,7 @@ describe('directive', () => {
|
||||
calls.push(1);
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(({
|
||||
const wrapper = shallowMount({
|
||||
directives: { custom: customDirective },
|
||||
setup() {
|
||||
return () => (
|
||||
@ -272,28 +276,28 @@ describe('directive', () => {
|
||||
/>
|
||||
);
|
||||
},
|
||||
}));
|
||||
});
|
||||
const node = wrapper.vm.$.subTree;
|
||||
expect(calls).toEqual(expect.arrayContaining([1]));
|
||||
expect(node.dirs).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('vHtml', () => {
|
||||
const wrapper = shallowMount(({
|
||||
const wrapper = shallowMount({
|
||||
setup() {
|
||||
return () => <h1 v-html="<div>foo</div>"></h1>;
|
||||
},
|
||||
}));
|
||||
});
|
||||
expect(wrapper.html()).toBe('<h1><div>foo</div></h1>');
|
||||
});
|
||||
|
||||
test('vText', () => {
|
||||
const text = 'foo';
|
||||
const wrapper = shallowMount(({
|
||||
const wrapper = shallowMount({
|
||||
setup() {
|
||||
return () => <div v-text={text}></div>;
|
||||
},
|
||||
}));
|
||||
});
|
||||
expect(wrapper.html()).toBe('<div>foo</div>');
|
||||
});
|
||||
});
|
||||
@ -315,7 +319,11 @@ describe('slots', () => {
|
||||
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () => <A v-slots={{ foo: (val: string) => val }}><span>default</span></A>;
|
||||
return () => (
|
||||
<A v-slots={{ foo: (val: string) => val }}>
|
||||
<span>default</span>
|
||||
</A>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@ -325,11 +333,7 @@ describe('slots', () => {
|
||||
test('without default', () => {
|
||||
const A = defineComponent({
|
||||
setup(_, { slots }) {
|
||||
return () => (
|
||||
<div>
|
||||
{slots.foo?.('foo')}
|
||||
</div>
|
||||
);
|
||||
return () => <div>{slots.foo?.('foo')}</div>;
|
||||
},
|
||||
});
|
||||
|
||||
@ -362,7 +366,11 @@ describe('PatchFlags', () => {
|
||||
const onClick = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
return () => <div v-show={visible.value} onClick={onClick}>NEED_PATCH</div>;
|
||||
return () => (
|
||||
<div v-show={visible.value} onClick={onClick}>
|
||||
NEED_PATCH
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@ -380,7 +388,9 @@ describe('PatchFlags', () => {
|
||||
};
|
||||
|
||||
return () => (
|
||||
<div {...bindProps} class="static" onClick={onClick}>full props</div>
|
||||
<div {...bindProps} class="static" onClick={onClick}>
|
||||
full props
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -455,9 +465,7 @@ describe('variables outside slots', () => {
|
||||
const textarea = <textarea id="textarea" {...attrs} />;
|
||||
return (
|
||||
<A inc={this.inc}>
|
||||
<div>
|
||||
{textarea}
|
||||
</div>
|
||||
<div>{textarea}</div>
|
||||
<button id="button" onClick={this.inc}>+1</button>
|
||||
</A>
|
||||
);
|
||||
@ -480,7 +488,6 @@ test('reassign variable as component should work', () => {
|
||||
});
|
||||
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
const _a2 = 2;
|
||||
a = _a2;
|
||||
/* eslint-enable */
|
||||
@ -495,3 +502,72 @@ test('reassign variable as component should work', () => {
|
||||
|
||||
expect(wrapper.html()).toBe('<span>2</span>');
|
||||
});
|
||||
|
||||
describe('should support passing object slots via JSX children', () => {
|
||||
const A = defineComponent({
|
||||
setup(_, { slots }) {
|
||||
return () => (
|
||||
<span>
|
||||
{slots.default?.()}
|
||||
{slots.foo?.()}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
test('single expression, variable', () => {
|
||||
const slots = { default: () => 1, foo: () => 2 };
|
||||
|
||||
const wrapper = mount({
|
||||
render() {
|
||||
return <A>{slots}</A>;
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.html()).toBe('<span>12</span>');
|
||||
});
|
||||
|
||||
test('single expression, object literal', () => {
|
||||
const wrapper = mount({
|
||||
render() {
|
||||
return <A>{{ default: () => 1, foo: () => 2 }}</A>;
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.html()).toBe('<span>12</span>');
|
||||
});
|
||||
|
||||
test('single expression, object literal', () => {
|
||||
const wrapper = mount({
|
||||
render() {
|
||||
return <A>{{ default: () => 1, foo: () => 2 }}</A>;
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.html()).toBe('<span>12</span>');
|
||||
});
|
||||
|
||||
test('single expression, non-literal value', () => {
|
||||
const foo = () => 1;
|
||||
|
||||
const wrapper = mount({
|
||||
render() {
|
||||
return <A>{foo()}</A>;
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.html()).toBe('<span>1<!----></span>');
|
||||
});
|
||||
|
||||
test('single expression, function expression', () => {
|
||||
const wrapper = mount({
|
||||
render() {
|
||||
return (
|
||||
<A>{() => 'foo'}</A>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.html()).toBe('<span>foo<!----></span>');
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { transform } from '@babel/core';
|
||||
import JSX, { Opts } from '../src';
|
||||
|
||||
interface Test {
|
||||
name: string;
|
||||
from: string;
|
||||
}
|
||||
|
||||
const transpile = (
|
||||
source: string, options: Opts = {},
|
||||
) => new Promise((resolve, reject) => transform(
|
||||
@ -18,7 +23,7 @@ const transpile = (
|
||||
},
|
||||
));
|
||||
|
||||
const tests = [
|
||||
const tests: Test[] = [
|
||||
{
|
||||
name: 'input[type="checkbox"]',
|
||||
from: '<input type="checkbox" v-model={test} />',
|
||||
@ -155,7 +160,7 @@ tests.forEach((
|
||||
);
|
||||
});
|
||||
|
||||
const overridePropsTests = [{
|
||||
const overridePropsTests: Test[] = [{
|
||||
name: 'single',
|
||||
from: '<div {...a} />',
|
||||
}, {
|
||||
@ -173,3 +178,34 @@ overridePropsTests.forEach((
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const slotsTests: Test[] = [
|
||||
{
|
||||
name: 'multiple expressions',
|
||||
from: '<A>{foo}{bar}</A>',
|
||||
},
|
||||
{
|
||||
name: 'single expression, function expression',
|
||||
from: `
|
||||
<A>{() => "foo"}</A>
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'single expression, non-literal value: runtime check',
|
||||
from: `
|
||||
const foo = () => 1;
|
||||
<A>{foo()}</A>;
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
slotsTests.forEach(({
|
||||
name, from,
|
||||
}) => {
|
||||
test(
|
||||
`passing object slots via JSX children ${name}`,
|
||||
async () => {
|
||||
expect(await transpile(from, { optimize: true })).toMatchSnapshot(name);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user