这节我们来实现组件联动。
它是动作的一种类型:
比如 amis 里,点击按钮的时候修改视频组件为隐藏。
这种组件和组件之间的关联就叫组件联动:
那它是怎么实现的呢?
其实也很简单:
我们知道,forwardRef + useImperativeHandle 可以让组件暴露一些方法出来:
我们在递归渲染组件 renderComponents 的时候,把组件 ref 收集起来,放到一个 map 里。
key 为组件 id
{
1111: {
aaa() {
}
bbb() {
}
},
222: {
ccc() {
}
ddd() {
}
}
}
这样 id 为 111 的组件想调用 id 为 222 的组件的 ccc 方法,就只需要在动作里加一个配置:
actions: [
{
type: "componentMethod",
config: {
componentId: 222,
method: "ccc",
},
},
];
然后处理事件的时候,根据这个 componentId 和 method 从 refs 里拿到对应的方法执行就好了。
这样就实现了组件联动。
这个 actions 是配置在 components 的 store 里。
而组件有什么 method 是配置在 componentConfig 的 store 里。
思路理清了,我们来写下代码:
当然,现在的组件没啥好暴露的方法,我们加一个 Modal 组件:
materials/Modal/prod.tsx
import { Modal as AntdModal } from "antd";
import { forwardRef, useImperativeHandle, useState } from "react";
import { CommonComponentProps } from "../../interface";
export interface ModalRef {
open: () => void;
close: () => void;
}
const Modal: React.ForwardRefRenderFunction<ModalRef, CommonComponentProps> = (
{ children, title, onOk, onCancel, styles },
ref
) => {
const [open, setOpen] = useState(false);
useImperativeHandle(ref, () => {
return {
open: () => {
setOpen(true);
},
close: () => {
setOpen(false);
},
};
}, []);
return (
<AntdModal
title={title}
style={styles}
open={open}
onCancel={() => {
onCancel && onCancel();
setOpen(false);
}}
onOk={() => {
onOk && onOk();
}}
destroyOnClose>
{children}
</AntdModal>
);
};
export default forwardRef(Modal);
可以传入 title、onOk、onCancel、styles 的参数,并且暴露了 open、close 方法用于控制弹窗显示隐藏。
然后写下 dev 时的组件:
materials/Modal/dev.tsx
import { useMaterailDrop } from "../../hooks/useMaterailDrop";
import { CommonComponentProps } from "../../interface";
function Modal({ id, children, title, styles }: CommonComponentProps) {
const { canDrop, drop } = useMaterailDrop(["Button", "Container"], id);
return (
<div
ref={drop}
style={styles}
data-component-id={id}
className={`min-h-[100px] p-[20px] ${canDrop ? "border-[2px] border-[blue]" : "border-[1px] border-[#000]"}`}>
<h4>{title}</h4>
<div>{children}</div>
</div>
);
}
export default Modal;
dev 时的组件和 prod 时的组件不一样,我们要加上 drop 的处理,,设置 drop 时的高亮,添加 data-compnent-id,并且指定最小高度
在 componentConfig 里配一下:
Modal: {
name: 'Modal',
defaultProps: {
title: '弹窗'
},
setter: [
{
name: 'title',
label: '标题',
type: 'input'
}
],
stylesSetter: [],
events: [
{
name: 'onOk',
label: '确认事件',
},
{
name: 'onCancel',
label: '取消事件'
},
],
desc: '弹窗',
dev: ModalDev,
prod: ModalProd
},
试下效果:
编辑时可以拖拽组件进去,预览时为空,因为默认是隐藏的。
我们先改为默认显示试试:
然后我们设置下属性和样式:
绑定下事件:
和之前的功能能无缝结合。
低代码编辑器的核心完成后,支持不同场景只要增加不同组件就可以了。
然后我们回过头来继续做组件联动:
默认弹窗是隐藏的,我们要通过组件联动的方式,调用它的 open、close 方法来控制。
在 componentConfig 里配置下这两个 methods:
export interface ComponentMethod {
name: string;
label: string;
}
export interface ComponentConfig {
name: string;
defaultProps: Record<string, any>;
desc: string;
setter?: ComponentSetter[];
stylesSetter?: ComponentSetter[];
events?: ComponentEvent[];
methods?: ComponentMethod[];
dev: any;
prod: any;
}
methods: [
{
name: 'open',
label: '打开弹窗',
},
{
name: 'close',
label: '关闭弹窗'
}
],
然后在 ActionModal 里支持选择组件联动的方式:
import { Modal, Segmented } from "antd";
import { useEffect, useState } from "react";
import { GoToLink, GoToLinkConfig } from "./actions/GoToLink";
import { ShowMessage, ShowMessageConfig } from "./actions/ShowMessage";
import { CustomJS, CustomJSConfig } from "./actions/CustomJS";
import {
ComponentMethod,
ComponentMethodConfig,
} from "./actions/ComponentMethod";
export type ActionConfig =
| GoToLinkConfig
| ShowMessageConfig
| CustomJSConfig
| ComponentMethodConfig;
export interface ActionModalProps {
visible: boolean;
action?: ActionConfig;
handleOk: (config?: ActionConfig) => void;
handleCancel: () => void;
}
export function ActionModal(props: ActionModalProps) {
const { visible, action, handleOk, handleCancel } = props;
const map = {
goToLink: "访问链接",
showMessage: "消息提示",
customJS: "自定义 JS",
componentMethod: "组件方法",
};
const [key, setKey] = useState<string>("访问链接");
const [curConfig, setCurConfig] = useState<ActionConfig>();
useEffect(() => {
if (action?.type) {
setKey(map[action.type]);
}
}, [action]);
return (
<Modal
title="事件动作配置"
width={800}
open={visible}
okText="确认"
cancelText="取消"
onOk={() => handleOk(curConfig)}
onCancel={handleCancel}>
<div className="h-[500px]">
<Segmented
value={key}
onChange={setKey}
block
options={["访问链接", "消息提示", "组件方法", "自定义 JS"]}
/>
{key === "访问链接" && (
<GoToLink
key="goToLink"
value={action?.type === "goToLink" ? action.url : ""}
onChange={(config) => {
setCurConfig(config);
}}
/>
)}
{key === "消息提示" && (
<ShowMessage
key="showMessage"
value={
action?.type === "showMessage"
? action.config
: undefined
}
onChange={(config) => {
setCurConfig(config);
}}
/>
)}
{key === "组件方法" && (
<ComponentMethod
key="showMessage"
value={
action?.type === "componentMethod"
? action.config
: undefined
}
onChange={(config) => {
setCurConfig(config);
}}
/>
)}
{key === "自定义 JS" && (
<CustomJS
key="customJS"
value={action?.type === "customJS" ? action.code : ""}
onChange={(config) => {
setCurConfig(config);
}}
/>
)}
</div>
</Modal>
);
}
实现下这个 ComponentMethod 组件:
Setting/actions/ComponentMethod.tsx
import { useEffect, useState } from "react";
import {
Component,
getComponentById,
useComponetsStore,
} from "../../../stores/components";
import { Select, TreeSelect } from "antd";
import { useComponentConfigStore } from "../../../stores/component-config";
export interface ComponentMethodConfig {
type: "componentMethod";
config: {
componentId: number;
method: string;
};
}
export interface ComponentMethodProps {
value?: string;
onChange?: (config: ComponentMethodConfig) => void;
}
export function ComponentMethod(props: ComponentMethodProps) {
const { components, curComponentId } = useComponetsStore();
const { componentConfig } = useComponentConfigStore();
const [selectedComponent, setSelectedComponent] =
useState<Component | null>();
function componentChange(value: number) {
if (!curComponentId) return;
setSelectedComponent(getComponentById(value, components));
}
return (
<div className="mt-[40px]">
<div className="flex items-center gap-[10px]">
<div>组件:</div>
<div>
<TreeSelect
style={{ width: 500, height: 50 }}
treeData={components}
fieldNames={{
label: "name",
value: "id",
}}
onChange={(value) => {
componentChange(value);
}}
/>
</div>
</div>
{componentConfig[selectedComponent?.name || ""] && (
<div className="flex items-center gap-[10px] mt-[20px]">
<div>方法:</div>
<div>
<Select
style={{ width: 500, height: 50 }}
options={componentConfig[
selectedComponent?.name || ""
].methods?.map((method) => ({
label: method.label,
value: method.name,
}))}
onChange={(value) => {}}
/>
</div>
</div>
)}
</div>
);
}
就是两个 Select,一个选择组件、一个选择组件的方法。
需要加一个 selectedComponent 的 state 来记录选中的组件。
测试下:
这样,组件方法的选择就完成了。
我们再处理下 value 和 onChange,做下数据的保存和回显:
import { useEffect, useState } from "react";
import {
Component,
getComponentById,
useComponetsStore,
} from "../../../stores/components";
import { Select, TreeSelect } from "antd";
import { useComponentConfigStore } from "../../../stores/component-config";
export interface ComponentMethodConfig {
type: "componentMethod";
config: {
componentId: number;
method: string;
};
}
export interface ComponentMethodProps {
value?: ComponentMethodConfig["config"];
onChange?: (config: ComponentMethodConfig) => void;
}
export function ComponentMethod(props: ComponentMethodProps) {
const { value, onChange } = props;
const { components, curComponentId } = useComponetsStore();
const { componentConfig } = useComponentConfigStore();
const [selectedComponent, setSelectedComponent] =
useState<Component | null>();
const [curId, setCurId] = useState<number>();
const [curMethod, setCurMethod] = useState<string>();
useEffect(() => {
if (value) {
setCurId(value.componentId);
setCurMethod(value.method);
setSelectedComponent(
getComponentById(value.componentId, components)
);
}
}, [value]);
function componentChange(value: number) {
if (!curComponentId) return;
setCurId(value);
setSelectedComponent(getComponentById(value, components));
}
function componentMethodChange(value: string) {
if (!curComponentId || !selectedComponent) return;
setCurMethod(value);
onChange?.({
type: "componentMethod",
config: {
componentId: selectedComponent?.id,
method: value,
},
});
}
return (
<div className="mt-[40px]">
<div className="flex items-center gap-[10px]">
<div>组件:</div>
<div>
<TreeSelect
style={{ width: 500, height: 50 }}
treeData={components}
fieldNames={{
label: "name",
value: "id",
}}
value={curId}
onChange={(value) => {
componentChange(value);
}}
/>
</div>
</div>
{componentConfig[selectedComponent?.name || ""] && (
<div className="flex items-center gap-[10px] mt-[20px]">
<div>方法:</div>
<div>
<Select
style={{ width: 500, height: 50 }}
options={componentConfig[
selectedComponent?.name || ""
].methods?.map((method) => ({
label: method.label,
value: method.name,
}))}
value={curMethod}
onChange={(value) => {
componentMethodChange(value);
}}
/>
</div>
</div>
)}
</div>
);
}
然后还要在动作列表回显下:
{
item.type === "componentMethod" ? (
<div
key="componentMethod"
className="border border-[#aaa] m-[10px] p-[10px] relative">
<div className="text-[blue]">组件方法</div>
<div>
{getComponentById(item.config.componentId, components)?.desc}
</div>
<div>{item.config.componentId}</div>
<div>{item.config.method}</div>
<div
style={{
position: "absolute",
top: 10,
right: 30,
cursor: "pointer",
}}
onClick={() => editAction(item, index)}>
<EditOutlined />
</div>
<div
style={{
position: "absolute",
top: 10,
right: 10,
cursor: "pointer",
}}
onClick={() => deleteAction(event, index)}>
<DeleteOutlined />
</div>
</div>
) : null;
}
测试下:
添加、编辑都没问题。
然后我们在 Preview 里做下事件处理:
收集所有的 refs,按照 id 来索引,调用方法的时候根据 componentId 和 method 来调用。
const componentRefs = useRef<Record<string, any>>({});
ref: (ref: Record<string, any>) => { componentRefs.current[component.id] = ref; },
else if(action.type === 'componentMethod') {
const component = componentRefs.current[action.config.componentId];
if (component) {
component[action.config.method]?.();
}
}
测试下:
这样,组件联动就完成了。
案例代码上传了小册仓库,可以切换到这个 commit 查看:
git reset --hard 909a148d0145db4b7ce93ce2f16f676f87c37013
总结
这节我们实现了组件联动,也就是一个组件可以调用另一个组件的方法。
原理就是组件通过 forwardRef + useImperativeHandle 暴露一些方法,然后在 action 里配置 componentId、method。
这样预览的时候收集所有组件的 ref,事件触发的时候根据配置调用对应 componentId 的对应 method。
这样,我们支持了内置动作、自定义 JS、组件联动,事件绑定的功能就比较完整了。
