mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
feat: 扩充setValue动作, 支持beforeSetData
This commit is contained in:
parent
71c7378e3d
commit
3961a49118
@ -12,7 +12,7 @@ import {
|
||||
SearchBox,
|
||||
InputBox
|
||||
} from 'amis';
|
||||
import {eachTree, mapTree} from 'amis-core';
|
||||
import {eachTree} from 'amis-core';
|
||||
import 'amis-ui/lib/locale/en-US';
|
||||
import {withRouter} from 'react-router';
|
||||
// @ts-ignore
|
||||
@ -553,7 +553,7 @@ export class App extends React.PureComponent<{
|
||||
|
||||
renderContent() {
|
||||
const locale = 'zh-CN'; // 暂时不支持切换,因为目前只有中文文档
|
||||
const theme = this.state.theme;
|
||||
const {theme} = this.state;
|
||||
|
||||
return (
|
||||
<React.Suspense
|
||||
|
169
examples/components/EventAction/update-data/SetVariable.jsx
Normal file
169
examples/components/EventAction/update-data/SetVariable.jsx
Normal file
@ -0,0 +1,169 @@
|
||||
/**
|
||||
* @file 变量更新示例
|
||||
*/
|
||||
import update from 'lodash/update';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import {cloneObject, setVariable} from 'amis-core';
|
||||
|
||||
const namespace = 'appVariables';
|
||||
const initData = JSON.parse(sessionStorage.getItem(namespace)) || {
|
||||
ProductName: 'BCC',
|
||||
Banlance: 1234.888,
|
||||
ProductNum: 10,
|
||||
isOnline: false,
|
||||
ProductList: ['BCC', 'BOS', 'VPC'],
|
||||
PROFILE: {
|
||||
FirstName: 'Amis',
|
||||
Age: 18,
|
||||
Address: {
|
||||
street: 'ShangDi',
|
||||
postcode: 100001
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
/** schema配置 */
|
||||
schema: {
|
||||
type: 'page',
|
||||
title: '更新变量数据',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '变量的命名空间通过环境变量设置为了<code>appVariables</code>, 可以通过\\${appVariables.xxx}来取值'
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
style: {
|
||||
padding: '8px',
|
||||
marginBottom: '8px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: '4px'
|
||||
},
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '<h2>数据域appVariables</h2>'
|
||||
},
|
||||
{
|
||||
type: 'json',
|
||||
id: 'u:44521540e64c',
|
||||
source: '${appVariables}',
|
||||
levelExpand: 10
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '<h3>接口中的<code>ProductName (\\${ProductName})</code>: <strong>${ProductName|default:-}</strong></h3>',
|
||||
inline: false,
|
||||
id: 'u:98ed5c5534ef'
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '<h3>变量中的<code>ProductName (\\${appVariables.ProductName})</code>: <strong>${appVariables.ProductName|default:-}</strong></h3>',
|
||||
inline: false,
|
||||
id: 'u:98ed5c5534ef'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'form',
|
||||
title: '表单',
|
||||
debug: true,
|
||||
body: [
|
||||
{
|
||||
label: '产品名称',
|
||||
type: 'input-text',
|
||||
name: 'product',
|
||||
placeholder: '请输入内容, 观察引用变量组件的变化',
|
||||
id: 'u:d9802fd83145',
|
||||
onEvent: {
|
||||
change: {
|
||||
weight: 0,
|
||||
actions: [
|
||||
{
|
||||
args: {
|
||||
path: 'appVariables.ProductName',
|
||||
value: '${event.data.value}'
|
||||
},
|
||||
actionType: 'setValue'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'static',
|
||||
label: '产品名称描述',
|
||||
id: 'u:7bd4e2a4f95e',
|
||||
value: '${appVariables.ProductName}',
|
||||
name: 'staticName'
|
||||
}
|
||||
],
|
||||
id: 'u:dc2580fa447a'
|
||||
}
|
||||
],
|
||||
initApi: '/api/mock2/page/initData2',
|
||||
onEvent: {
|
||||
inited: {
|
||||
weight: 0,
|
||||
actions: [
|
||||
{
|
||||
args: {
|
||||
path: 'appVariables.ProductName',
|
||||
value: '${event.data.ProductName}'
|
||||
},
|
||||
actionType: 'setValue'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
data: {[namespace]: JSON.parse(sessionStorage.getItem(namespace))}
|
||||
},
|
||||
/** 环境变量 */
|
||||
env: {
|
||||
beforeSetData: (renderer, action, event) => {
|
||||
const value = event?.data?.value ?? action?.args?.value;
|
||||
const path = action?.args?.path;
|
||||
const {session = 'global'} = renderer.props?.env ?? {};
|
||||
const comptList = event?.context?.scoped?.getComponentsByRefPath(
|
||||
session,
|
||||
path
|
||||
);
|
||||
|
||||
for (let component of comptList) {
|
||||
const {$path: targetPath, $schema: targetSchema} = component?.props;
|
||||
const {$path: triggerPath, $schema: triggerSchema} = renderer?.props;
|
||||
|
||||
if (
|
||||
!component.setData &&
|
||||
(targetPath === triggerPath || isEqual(targetSchema, triggerSchema))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (component?.props?.onChange) {
|
||||
const submitOnChange = !!component.props?.$schema?.submitOnChange;
|
||||
|
||||
component.props.onChange(value, submitOnChange, true);
|
||||
} else if (component?.setData) {
|
||||
const currentData = JSON.parse(
|
||||
sessionStorage.getItem(namespace) || JSON.stringify(initData)
|
||||
);
|
||||
const varPath = path.replace(/^appVariables\./, '');
|
||||
|
||||
update(currentData, varPath, origin => {
|
||||
return typeof value === typeof origin ? value : origin;
|
||||
});
|
||||
|
||||
sessionStorage.setItem(namespace, JSON.stringify(currentData));
|
||||
const newCtx = cloneObject(component?.props?.data ?? {});
|
||||
setVariable(newCtx, path, value, true);
|
||||
|
||||
component.setData(newCtx, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
@ -102,6 +102,7 @@ import UpdateButtonGroupSelectActionSchema from './EventAction/update-data/Updat
|
||||
import UpdateComboActionSchema from './EventAction/update-data/UpdateCombo';
|
||||
import SyncUpdateActionSchema from './EventAction/update-data/SyncUpdate';
|
||||
import DataAutoFillActionSchema from './EventAction/update-data/DataAutoFill';
|
||||
import SetVariable from './EventAction/update-data/SetVariable';
|
||||
import PreventFormActionSchema from './EventAction/prevent-defalut/PreventForm';
|
||||
import WizardSchema from './Wizard';
|
||||
import ChartSchema from './Chart';
|
||||
@ -634,6 +635,16 @@ export const examples = [
|
||||
label: '数据回填',
|
||||
path: '/examples/action/setdata/autofill',
|
||||
component: makeSchemaRenderer(DataAutoFillActionSchema)
|
||||
},
|
||||
{
|
||||
label: '更新全局变量数据',
|
||||
path: '/examples/action/setdata/variable',
|
||||
component: makeSchemaRenderer(
|
||||
SetVariable.schema,
|
||||
SetVariable.props ?? {},
|
||||
true,
|
||||
SetVariable.env
|
||||
)
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -6,6 +6,7 @@ import {normalizeLink} from 'amis-core';
|
||||
import {withRouter} from 'react-router';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import {qsparse, parseQuery} from 'amis-core';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
|
||||
function loadEditor() {
|
||||
return new Promise(resolve =>
|
||||
@ -15,7 +16,15 @@ function loadEditor() {
|
||||
|
||||
const viewMode = localStorage.getItem('amis-viewMode') || 'pc';
|
||||
|
||||
export default function (schema, showCode, envOverrides) {
|
||||
/**
|
||||
*
|
||||
* @param {*} schema schema配置
|
||||
* @param {*} schemaProps props配置
|
||||
* @param {*} showCode 是否展示代码
|
||||
* @param {Object} envOverrides 覆写环境变量
|
||||
* @returns
|
||||
*/
|
||||
export default function (schema, schemaProps, showCode, envOverrides) {
|
||||
if (!schema['$schema']) {
|
||||
schema = {
|
||||
...schema
|
||||
@ -202,6 +211,7 @@ export default function (schema, showCode, envOverrides) {
|
||||
{
|
||||
schema: schema,
|
||||
props: {
|
||||
...(isPlainObject(schemaProps) ? schemaProps : {}),
|
||||
location: this.props.location,
|
||||
theme: this.props.theme,
|
||||
locale: this.props.locale
|
||||
@ -244,6 +254,7 @@ export default function (schema, showCode, envOverrides) {
|
||||
return render(
|
||||
schema,
|
||||
{
|
||||
...(isPlainObject(schemaProps) ? schemaProps : {}),
|
||||
location,
|
||||
theme,
|
||||
locale
|
||||
|
19
mock/cfc/mock/page/initData2.json
Normal file
19
mock/cfc/mock/page/initData2.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"status": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"ProductName": "BOS",
|
||||
"Banlance": 1234.888,
|
||||
"ProductNum": 10,
|
||||
"isOnline": true,
|
||||
"ProductList": ["BCC", "CDN", "LSS"],
|
||||
"PROFILE": {
|
||||
"FirstName": "Amis",
|
||||
"Age": 18,
|
||||
"Address": {
|
||||
"city": "Beijing",
|
||||
"postcode": 100000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
import React from 'react';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
import {RendererEnv} from './env';
|
||||
import {RendererProps} from './factory';
|
||||
import {LocaleContext, TranslateFn} from './locale';
|
||||
import {RootRenderer, RootRendererProps} from './RootRenderer';
|
||||
import {RootRenderer} from './RootRenderer';
|
||||
import {SchemaRenderer} from './SchemaRenderer';
|
||||
import Scoped from './Scoped';
|
||||
import {IRendererStore} from './store';
|
||||
@ -68,9 +68,9 @@ export class Root extends React.Component<RootProps> {
|
||||
translate,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
const theme = env.theme;
|
||||
let themeName = this.props.theme || 'cxd';
|
||||
|
||||
if (themeName === 'default') {
|
||||
themeName = 'cxd';
|
||||
}
|
||||
@ -100,7 +100,7 @@ export class Root extends React.Component<RootProps> {
|
||||
rootStore: rootStore,
|
||||
resolveDefinitions: this.resolveDefinitions,
|
||||
location: location,
|
||||
data: data,
|
||||
data,
|
||||
env: env,
|
||||
classnames: theme.classnames,
|
||||
classPrefix: theme.classPrefix,
|
||||
|
@ -1,5 +1,4 @@
|
||||
import {observer} from 'mobx-react';
|
||||
import {getEnv} from 'mobx-state-tree';
|
||||
import React from 'react';
|
||||
import type {RootProps} from './Root';
|
||||
import {IScopedContext, ScopedContext} from './Scoped';
|
||||
|
@ -5,7 +5,6 @@ import LazyComponent from './components/LazyComponent';
|
||||
import {
|
||||
filterSchema,
|
||||
loadRenderer,
|
||||
RendererComponent,
|
||||
RendererConfig,
|
||||
RendererEnv,
|
||||
RendererProps,
|
||||
@ -18,7 +17,6 @@ import {DebugWrapper} from './utils/debug';
|
||||
import getExprProperties from './utils/filter-schema';
|
||||
import {anyChanged, chainEvents, autobind} from './utils/helper';
|
||||
import {SimpleMap} from './utils/SimpleMap';
|
||||
|
||||
import {bindEvent, dispatchEvent, RendererEvent} from './utils/renderer-event';
|
||||
import {isAlive} from 'mobx-state-tree';
|
||||
import {reaction} from 'mobx';
|
||||
@ -81,7 +79,6 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
|
||||
this.renderChild = this.renderChild.bind(this);
|
||||
this.reRender = this.reRender.bind(this);
|
||||
this.resolveRenderer(this.props);
|
||||
|
||||
this.dispatchEvent = this.dispatchEvent.bind(this);
|
||||
|
||||
// 监听topStore更新
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import find from 'lodash/find';
|
||||
import values from 'lodash/values';
|
||||
import hoistNonReactStatic from 'hoist-non-react-statics';
|
||||
import {dataMapping, registerFunction} from './utils/tpl-builtin';
|
||||
import {RendererEnv, RendererProps} from './factory';
|
||||
@ -12,12 +13,14 @@ import {
|
||||
autobind,
|
||||
qsstringify,
|
||||
qsparse,
|
||||
eachTree,
|
||||
findTree,
|
||||
TreeItem,
|
||||
parseQuery,
|
||||
getVariable
|
||||
} from './utils/helper';
|
||||
import {RendererData, ActionObject} from './types';
|
||||
import {isPureVariable} from './utils/isPureVariable';
|
||||
|
||||
export interface ScopedComponentType extends React.Component<RendererProps> {
|
||||
focus?: () => void;
|
||||
@ -33,6 +36,11 @@ export interface ScopedComponentType extends React.Component<RendererProps> {
|
||||
ctx?: RendererData
|
||||
) => void;
|
||||
context: any;
|
||||
setData?: (
|
||||
value?: string | {[key: string]: string},
|
||||
replace?: boolean,
|
||||
index?: number
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface IScopedContext {
|
||||
@ -127,6 +135,75 @@ function createScopedTools(
|
||||
return component;
|
||||
},
|
||||
|
||||
/**
|
||||
* 基于绑定的变量名称查找组件
|
||||
* 支持形如${xxx}的格式
|
||||
*
|
||||
* @param session store的session, 默认为全局的
|
||||
* @param path 变量路径, 包含命名空间
|
||||
*/
|
||||
getComponentsByRefPath(
|
||||
session: string,
|
||||
path: string
|
||||
): ScopedComponentType[] {
|
||||
if (!path || typeof path !== 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const cmptMaps: Record<string, ScopedComponentType> = {};
|
||||
let root: AliasIScopedContext = this;
|
||||
|
||||
while (root.parent) {
|
||||
root = root.parent;
|
||||
}
|
||||
|
||||
eachTree([root], (item: TreeItem) => {
|
||||
const scopedCmptList: ScopedComponentType[] =
|
||||
item.getComponents() || [];
|
||||
|
||||
if (Array.isArray(scopedCmptList)) {
|
||||
for (const cmpt of scopedCmptList) {
|
||||
const pathKey = cmpt?.props?.$path ?? 'unknown';
|
||||
const schema = cmpt?.props?.$schema ?? {};
|
||||
const cmptSession = cmpt?.props.env?.session ?? 'global';
|
||||
|
||||
/** 仅查找当前session的组件 */
|
||||
if (cmptMaps[pathKey] || session !== cmptSession) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** 非Scoped组件, 查找其所属的父容器 */
|
||||
if (cmpt?.setData && typeof cmpt.setData === 'function') {
|
||||
cmptMaps[pathKey] = cmpt;
|
||||
continue;
|
||||
}
|
||||
|
||||
/** 查找Scoped组件中的引用 */
|
||||
for (const key of Object.keys(schema)) {
|
||||
const expression = schema[key];
|
||||
|
||||
if (
|
||||
typeof expression === 'string' &&
|
||||
isPureVariable(expression)
|
||||
) {
|
||||
/** 考虑到数据映射函数的情况,将宿主变量提取出来 */
|
||||
const host = expression
|
||||
.substring(2, expression.length - 1)
|
||||
.split('|')[0];
|
||||
|
||||
if (host && host === path) {
|
||||
cmptMaps[pathKey] = cmpt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return values(cmptMaps);
|
||||
},
|
||||
|
||||
getComponents() {
|
||||
return components.concat();
|
||||
},
|
||||
|
@ -21,7 +21,7 @@ export enum LoopStatus {
|
||||
export interface ListenerAction {
|
||||
actionType: string; // 动作类型 逻辑动作|自定义(脚本支撑)|reload|url|ajax|dialog|drawer 其他扩充的组件动作
|
||||
description?: string; // 事件描述,actionType: broadcast
|
||||
componentId?: string; // 组件ID,用于直接执行指定组件的动作
|
||||
componentId?: string; // 组件ID,用于直接执行指定组件的动作,指定多个组件时使用英文逗号分隔
|
||||
args?: Record<string, any>; // 动作配置,可以配置数据映射
|
||||
data?: Record<string, any> | null; // 动作数据参数,可以配置数据映射
|
||||
dataMergeMode?: 'merge' | 'override'; // 参数模式,合并或者覆盖
|
||||
|
@ -19,6 +19,8 @@ export interface ICmptAction extends ListenerAction {
|
||||
| 'usability'
|
||||
| 'reload';
|
||||
args: {
|
||||
/** actionType为setValue时,目标变量的path */
|
||||
path?: string;
|
||||
value?: string | {[key: string]: string};
|
||||
index?: number; // setValue支持更新指定索引的数据,一般用于数组类型
|
||||
};
|
||||
@ -69,8 +71,24 @@ export class CmptAction implements RendererAction {
|
||||
return renderer.props.topStore.setDisable(action.componentId, usability);
|
||||
}
|
||||
|
||||
// 数据更新
|
||||
if (action.actionType === 'setValue') {
|
||||
const beforeSetData = renderer?.props?.env?.beforeSetData;
|
||||
const path = action.args?.path;
|
||||
|
||||
/** 如果args中携带path参数, 则认为是全局变量赋值, 否则认为是组件变量赋值 */
|
||||
if (
|
||||
path &&
|
||||
typeof path === 'string' &&
|
||||
beforeSetData &&
|
||||
typeof beforeSetData === 'function'
|
||||
) {
|
||||
const res = await beforeSetData(renderer, action, event);
|
||||
|
||||
if (res === false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (component?.setData) {
|
||||
return component?.setData(
|
||||
action.args?.value,
|
||||
|
@ -15,8 +15,11 @@ import {
|
||||
ToastLevel
|
||||
} from './types';
|
||||
import hoistNonReactStatic from 'hoist-non-react-statics';
|
||||
import {IScopedContext} from './Scoped';
|
||||
import {RendererEvent} from './utils/renderer-event';
|
||||
|
||||
import type {IScopedContext} from './Scoped';
|
||||
import type {RendererEvent} from './utils/renderer-event';
|
||||
import type {ListenerContext} from './actions/Action';
|
||||
import type {ICmptAction} from './actions/CmptAction';
|
||||
|
||||
export interface wsObject {
|
||||
url: string;
|
||||
@ -25,6 +28,7 @@ export interface wsObject {
|
||||
}
|
||||
|
||||
export interface RendererEnv {
|
||||
session?: string;
|
||||
fetcher: (api: Api, data?: any, options?: object) => Promise<Payload>;
|
||||
isCancel: (val: any) => boolean;
|
||||
wsFetcher: (
|
||||
@ -102,14 +106,23 @@ export interface RendererEnv {
|
||||
* 替换文本,用于实现 URL 替换、语言替换等
|
||||
*/
|
||||
replaceText?: {[propName: string]: any};
|
||||
|
||||
/**
|
||||
* 文本替换的黑名单,因为属性太多了所以改成黑名单的 fangs
|
||||
* 文本替换的黑名单,因为属性太多了所以改成黑名单的 flags
|
||||
*/
|
||||
replaceTextIgnoreKeys?: String[];
|
||||
|
||||
/**
|
||||
* 解析url参数
|
||||
*/
|
||||
parseLocation?: (location: any) => Object;
|
||||
|
||||
/** 数据更新前触发的Hook */
|
||||
beforeSetData?: (
|
||||
renderer: ListenerContext,
|
||||
action: ICmptAction,
|
||||
event: RendererEvent<any, any>
|
||||
) => Promise<void | boolean>;
|
||||
}
|
||||
|
||||
export const EnvContext = React.createContext<RendererEnv | void>(undefined);
|
||||
|
@ -5,6 +5,7 @@
|
||||
* This source code is licensed under the Apache license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
Renderer,
|
||||
getRendererByName,
|
||||
|
Loading…
Reference in New Issue
Block a user