mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-02 11:58:10 +08:00
Tree 支持懒加载 (#1846)
* Nav 支持配置展开和收起的字段名 * feat: Tree 支持懒加载 * 补充懒加载文档 * 文档调整 * 错别字
This commit is contained in:
parent
a9a57c6ae1
commit
9d06028e08
@ -569,6 +569,53 @@ order: 59
|
||||
}
|
||||
```
|
||||
|
||||
## 懒加载
|
||||
|
||||
> since 1.1.6
|
||||
|
||||
需要懒加载的选项请配置 `defer` 为 true,然后配置 `deferApi` 即可完成懒加载。如果不配置 `deferApi` 会使用 `source` 接口。
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"api": "https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/mock2/form/saveForm",
|
||||
"controls": [
|
||||
{
|
||||
"type": "tree",
|
||||
"name": "tree",
|
||||
"label": "Tree",
|
||||
"deferApi": "https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/mock2/form/deferOptions?label=${label}&waitSeconds=2",
|
||||
"options": [
|
||||
{
|
||||
"label": "Folder A",
|
||||
"value": 1,
|
||||
"collapsed": true,
|
||||
"children": [
|
||||
{
|
||||
"label": "file A",
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"label": "file B",
|
||||
"value": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "这下面是懒加载的",
|
||||
"value": 4,
|
||||
"defer": true
|
||||
},
|
||||
{
|
||||
"label": "file D",
|
||||
"value": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 属性表
|
||||
|
||||
当做选择器表单项使用时,除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
|
||||
|
@ -73,6 +73,7 @@ export default {
|
||||
},
|
||||
{
|
||||
name: 'tree',
|
||||
showOutline: true,
|
||||
type: 'tree',
|
||||
label: '动态树',
|
||||
source: '/api/mock2/options/tree?waitSeconds=1'
|
||||
@ -80,6 +81,48 @@ export default {
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
{
|
||||
name: 'tree',
|
||||
type: 'tree',
|
||||
label: '树懒加载',
|
||||
multiple: true,
|
||||
deferApi: '/api/mock2/form/deferOptions?label=${label}&waitSeconds=2',
|
||||
options: [
|
||||
{
|
||||
label: '法师',
|
||||
children: [
|
||||
{
|
||||
label: '诸葛亮',
|
||||
value: 'zhugeliang'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '战士',
|
||||
defer: true
|
||||
},
|
||||
{
|
||||
label: '打野',
|
||||
children: [
|
||||
{
|
||||
label: '李白',
|
||||
value: 'libai'
|
||||
},
|
||||
{
|
||||
label: '韩信',
|
||||
value: 'hanxin'
|
||||
},
|
||||
{
|
||||
label: '云中君',
|
||||
value: 'yunzhongjun'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
{
|
||||
name: 'matrix',
|
||||
type: 'matrix',
|
||||
|
@ -210,6 +210,10 @@
|
||||
width: calc(var(--Tree-itemArrowWidth) + var(--gap-xs));
|
||||
}
|
||||
|
||||
&-spinner {
|
||||
margin-right: var(--gap-xs);
|
||||
}
|
||||
|
||||
&-itemIcon {
|
||||
display: inline-flex;
|
||||
margin-right: var(--gap-xs);
|
||||
|
@ -198,7 +198,11 @@ export function normalizeOptions(
|
||||
return (options as Options).map(item => {
|
||||
const value = item && item.value;
|
||||
|
||||
const idx = value !== undefined ? share.values.indexOf(value) : -1;
|
||||
const idx =
|
||||
value !== undefined && !item.children
|
||||
? share.values.indexOf(value)
|
||||
: -1;
|
||||
|
||||
if (~idx) {
|
||||
return share.options[idx];
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import {highlight} from '../renderers/Form/Options';
|
||||
import {Icon} from './icons';
|
||||
import Checkbox from './Checkbox';
|
||||
import {LocaleProps, localeable} from '../locale';
|
||||
import Spinner from './Spinner';
|
||||
|
||||
interface TreeSelectorProps extends ThemeProps, LocaleProps {
|
||||
highlightTxt?: string;
|
||||
@ -86,11 +87,11 @@ interface TreeSelectorProps extends ThemeProps, LocaleProps {
|
||||
removable?: boolean;
|
||||
removeTip?: string;
|
||||
onDelete?: (value: Option) => void;
|
||||
onDeferLoad?: (option: Option) => void;
|
||||
}
|
||||
|
||||
interface TreeSelectorState {
|
||||
value: Array<any>;
|
||||
unfolded: {[propName: string]: string};
|
||||
|
||||
inputValue: string;
|
||||
addingParent: Option | null;
|
||||
@ -133,54 +134,60 @@ export class TreeSelector extends React.Component<
|
||||
removeTip: 'Tree.removeNode'
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
const props = this.props;
|
||||
unfolded: WeakMap<Object, boolean> = new WeakMap();
|
||||
|
||||
this.setState({
|
||||
constructor(props: TreeSelectorProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: value2array(props.value, {
|
||||
multiple: props.multiple,
|
||||
delimiter: props.delimiter,
|
||||
valueField: props.valueField,
|
||||
options: props.options
|
||||
}),
|
||||
unfolded: this.syncUnFolded(props),
|
||||
|
||||
inputValue: '',
|
||||
addingParent: null,
|
||||
isAdding: false,
|
||||
isEditing: false,
|
||||
editingItem: null
|
||||
});
|
||||
};
|
||||
|
||||
this.syncUnFolded(props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: TreeSelectorProps) {
|
||||
const toUpdate: any = {};
|
||||
componentDidUpdate(prevProps: TreeSelectorProps) {
|
||||
const props = this.props;
|
||||
|
||||
if (prevProps.options !== props.options) {
|
||||
this.syncUnFolded(props);
|
||||
}
|
||||
|
||||
if (
|
||||
this.props.value !== nextProps.value ||
|
||||
this.props.options !== nextProps.options
|
||||
prevProps.value !== props.value ||
|
||||
prevProps.options !== props.options
|
||||
) {
|
||||
toUpdate.value = value2array(nextProps.value, {
|
||||
multiple: nextProps.multiple,
|
||||
delimiter: nextProps.delimiter,
|
||||
valueField: nextProps.valueField,
|
||||
options: nextProps.options
|
||||
this.setState({
|
||||
value: value2array(props.value, {
|
||||
multiple: props.multiple,
|
||||
delimiter: props.delimiter,
|
||||
valueField: props.valueField,
|
||||
options: props.options
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.options !== nextProps.options) {
|
||||
toUpdate.unfolded = this.syncUnFolded(nextProps);
|
||||
}
|
||||
|
||||
this.setState(toUpdate);
|
||||
}
|
||||
|
||||
syncUnFolded(props: TreeSelectorProps) {
|
||||
// 初始化树节点的展开状态
|
||||
let unfolded: {[propName: string]: string} = {};
|
||||
let unfolded = this.unfolded;
|
||||
const {foldedField, unfoldedField} = this.props;
|
||||
|
||||
eachTree(props.options, (node: Option, index, level) => {
|
||||
if (unfolded.has(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.children && node.children.length) {
|
||||
let ret: any = true;
|
||||
|
||||
@ -194,7 +201,7 @@ export class TreeSelector extends React.Component<
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
unfolded[node[props.valueField as string]] = ret;
|
||||
unfolded.set(node, ret);
|
||||
}
|
||||
});
|
||||
|
||||
@ -203,14 +210,21 @@ export class TreeSelector extends React.Component<
|
||||
|
||||
@autobind
|
||||
toggleUnfolded(node: any) {
|
||||
this.setState({
|
||||
unfolded: {
|
||||
...this.state.unfolded,
|
||||
[node[this.props.valueField as string]]: !this.state.unfolded[
|
||||
node[this.props.valueField as string]
|
||||
]
|
||||
}
|
||||
});
|
||||
const unfolded = this.unfolded;
|
||||
const {onDeferLoad} = this.props;
|
||||
|
||||
if (node.defer && !node.loaded) {
|
||||
onDeferLoad?.(node);
|
||||
return;
|
||||
}
|
||||
|
||||
unfolded.set(node, !unfolded.get(node));
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
isUnfolded(node: any) {
|
||||
const unfolded = this.unfolded;
|
||||
return unfolded.get(node);
|
||||
}
|
||||
|
||||
@autobind
|
||||
@ -229,13 +243,21 @@ export class TreeSelector extends React.Component<
|
||||
|
||||
@autobind
|
||||
handleSelect(node: any, value?: any) {
|
||||
const {joinValues, valueField, onChange} = this.props;
|
||||
|
||||
if (node[valueField as string] === undefined) {
|
||||
if (node.defer && !node.loaded) {
|
||||
this.toggleUnfolded(node);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
value: [node]
|
||||
},
|
||||
() => {
|
||||
const {joinValues, valueField, onChange} = this.props;
|
||||
|
||||
onChange(joinValues ? node[valueField as string] : node);
|
||||
}
|
||||
);
|
||||
@ -513,7 +535,6 @@ export class TreeSelector extends React.Component<
|
||||
translate: __
|
||||
} = this.props;
|
||||
const {
|
||||
unfolded,
|
||||
value: stateValue,
|
||||
isAdding,
|
||||
addingParent,
|
||||
@ -606,11 +627,18 @@ export class TreeSelector extends React.Component<
|
||||
'is-disabled': nodeDisabled
|
||||
})}
|
||||
>
|
||||
{!isLeaf ? (
|
||||
{item.loading ? (
|
||||
<Spinner
|
||||
size="sm"
|
||||
show
|
||||
icon="reload"
|
||||
spinnerClassName={cx('Tree-spinner')}
|
||||
/>
|
||||
) : !isLeaf || item.defer ? (
|
||||
<div
|
||||
onClick={() => this.toggleUnfolded(item)}
|
||||
className={cx('Tree-itemArrow', {
|
||||
'is-folded': !unfolded[item[valueField]]
|
||||
'is-folded': !this.isUnfolded(item)
|
||||
})}
|
||||
>
|
||||
<Icon icon="right-arrow-bold" className="icon" />
|
||||
@ -693,7 +721,7 @@ export class TreeSelector extends React.Component<
|
||||
</div>
|
||||
)}
|
||||
{/* 有children而且为展开状态 或者 添加child时 */}
|
||||
{(childrenItems && unfolded[item[valueField]]) ||
|
||||
{(childrenItems && this.isUnfolded(item)) ||
|
||||
(isAdding && addingParent === item) ? (
|
||||
<ul className={cx('Tree-sublist')}>
|
||||
{isAdding && addingParent === item ? (
|
||||
@ -710,9 +738,7 @@ export class TreeSelector extends React.Component<
|
||||
) : null}
|
||||
{childrenItems}
|
||||
</ul>
|
||||
) : !childrenItems &&
|
||||
item.placeholder &&
|
||||
unfolded[item[valueField]] ? (
|
||||
) : !childrenItems && item.placeholder && this.isUnfolded(item) ? (
|
||||
<ul className={cx('Tree-sublist')}>
|
||||
<li className={cx('Tree-item')}>
|
||||
<div className={cx('Tree-placeholder')}>{item.placeholder}</div>
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
OptionsControlProps
|
||||
} from './Options';
|
||||
import {Spinner} from '../../components';
|
||||
import {SchemaApi} from '../../Schema';
|
||||
|
||||
/**
|
||||
* Tree 下拉选择框。
|
||||
@ -54,6 +55,8 @@ export interface TreeControlSchema extends FormOptionsControl {
|
||||
* 顶级节点是否可以创建子节点
|
||||
*/
|
||||
rootCreatable?: boolean;
|
||||
|
||||
deferApi?: SchemaApi;
|
||||
}
|
||||
|
||||
export interface TreeProps
|
||||
@ -121,6 +124,7 @@ export default class TreeControl extends React.Component<TreeProps> {
|
||||
rootCreatable,
|
||||
rootCreateTip,
|
||||
labelField,
|
||||
deferLoad,
|
||||
translate: __
|
||||
} = this.props;
|
||||
|
||||
@ -166,6 +170,7 @@ export default class TreeControl extends React.Component<TreeProps> {
|
||||
removeTip={removeTip}
|
||||
onDelete={onDelete}
|
||||
bultinCUD={!addControls && !editControls}
|
||||
onDeferLoad={deferLoad}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
autobind,
|
||||
createObject,
|
||||
findTree,
|
||||
isUnfolded,
|
||||
mapTree,
|
||||
someTree,
|
||||
spliceTree
|
||||
@ -35,6 +36,9 @@ export type NavItemSchema = {
|
||||
|
||||
to?: SchemaUrlPath;
|
||||
|
||||
unfolded?: boolean;
|
||||
active?: boolean;
|
||||
|
||||
defer?: boolean;
|
||||
deferApi?: SchemaApi;
|
||||
|
||||
@ -88,6 +92,7 @@ export interface Link {
|
||||
children?: Links;
|
||||
defer?: boolean;
|
||||
loading?: boolean;
|
||||
loaded?: boolean;
|
||||
[propName: string]: any;
|
||||
}
|
||||
export interface Links extends Array<Link> {}
|
||||
@ -131,7 +136,8 @@ export class Navigation extends React.Component<
|
||||
}
|
||||
const isActive: boolean = !!link.active;
|
||||
const {disabled, togglerClassName, classnames: cx, indentSize} = this.props;
|
||||
const hasSub = link.defer || (link.children && link.children.length);
|
||||
const hasSub =
|
||||
(link.defer && !link.loaded) || (link.children && link.children.length);
|
||||
|
||||
return (
|
||||
<li
|
||||
@ -223,10 +229,10 @@ const ConditionBuilderWithRemoteOptions = withRemoteConfig({
|
||||
motivation?: string
|
||||
) {
|
||||
if (Array.isArray(links) && motivation !== 'toggle') {
|
||||
const {data, env} = props;
|
||||
const {data, env, unfoldedField, foldedField} = props;
|
||||
|
||||
links = mapTree(links, (link: Link) => {
|
||||
return {
|
||||
const item: any = {
|
||||
...link,
|
||||
...getExprProperties(link, data as object),
|
||||
active:
|
||||
@ -237,11 +243,14 @@ const ConditionBuilderWithRemoteOptions = withRemoteConfig({
|
||||
link.hasOwnProperty('to') &&
|
||||
env &&
|
||||
env.isCurrentUrl(filter(link.to as string, data))
|
||||
)),
|
||||
unfolded:
|
||||
link.unfolded ||
|
||||
(link.children && link.children.some(link => !!link.active))
|
||||
))
|
||||
};
|
||||
|
||||
item.unfolded =
|
||||
isUnfolded(item, {unfoldedField, foldedField}) ||
|
||||
(link.children && link.children.some(link => !!link.active));
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
@ -251,7 +260,6 @@ const ConditionBuilderWithRemoteOptions = withRemoteConfig({
|
||||
beforeDeferLoad(item: Link, indexes: Array<number>, links: Array<Link>) {
|
||||
return spliceTree(links, indexes, 1, {
|
||||
...item,
|
||||
defer: undefined,
|
||||
loading: true
|
||||
});
|
||||
},
|
||||
@ -264,8 +272,8 @@ const ConditionBuilderWithRemoteOptions = withRemoteConfig({
|
||||
) {
|
||||
const newItem = {
|
||||
...item,
|
||||
defer: false,
|
||||
loading: false,
|
||||
loaded: true,
|
||||
error: ret.ok ? undefined : ret.msg
|
||||
};
|
||||
|
||||
@ -287,6 +295,8 @@ const ConditionBuilderWithRemoteOptions = withRemoteConfig({
|
||||
location?: any;
|
||||
env?: RendererEnv;
|
||||
data?: any;
|
||||
unfoldedField?: string;
|
||||
foldedField?: string;
|
||||
}
|
||||
> {
|
||||
constructor(props: any) {
|
||||
@ -312,7 +322,7 @@ const ConditionBuilderWithRemoteOptions = withRemoteConfig({
|
||||
toggleLink(target: Link) {
|
||||
const {config, updateConfig, deferLoad} = this.props;
|
||||
|
||||
if (target.defer) {
|
||||
if (target.defer && !target.loaded) {
|
||||
deferLoad(target);
|
||||
} else {
|
||||
updateConfig(
|
||||
@ -335,7 +345,11 @@ const ConditionBuilderWithRemoteOptions = withRemoteConfig({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!link.to && ((link.children && link.children.length) || link.defer)) {
|
||||
if (
|
||||
!link.to &&
|
||||
((link.children && link.children.length) ||
|
||||
(link.defer && !link.loaded))
|
||||
) {
|
||||
this.toggleLink(link);
|
||||
return;
|
||||
}
|
||||
|
@ -24,7 +24,8 @@ import {
|
||||
findTreeIndex,
|
||||
spliceTree,
|
||||
isEmpty,
|
||||
getTreeAncestors
|
||||
getTreeAncestors,
|
||||
filterTree
|
||||
} from '../utils/helper';
|
||||
import {flattenTree} from '../utils/helper';
|
||||
import {IRendererStore} from '.';
|
||||
@ -74,7 +75,7 @@ export const FormItemStore = StoreNode.named('FormItemStore')
|
||||
labelField: 'label',
|
||||
joinValues: true,
|
||||
extractValue: false,
|
||||
options: types.optional(types.array(types.frozen()), []),
|
||||
options: types.optional(types.frozen<Array<any>>(), []),
|
||||
expressionsInOptions: false,
|
||||
selectFirst: false,
|
||||
autoFill: types.frozen(),
|
||||
@ -385,9 +386,9 @@ export const FormItemStore = StoreNode.named('FormItemStore')
|
||||
if (!Array.isArray(options)) {
|
||||
return;
|
||||
}
|
||||
options = options.filter(item => item);
|
||||
options = filterTree(options, item => item);
|
||||
const originOptions = self.options.concat();
|
||||
options.length ? self.options.replace(options) : self.options.clear();
|
||||
self.options = options;
|
||||
syncOptions(originOptions);
|
||||
let selectedOptions;
|
||||
|
||||
@ -446,7 +447,9 @@ export const FormItemStore = StoreNode.named('FormItemStore')
|
||||
self.loading = false;
|
||||
}
|
||||
|
||||
self.loading = true;
|
||||
if (!config?.silent) {
|
||||
self.loading = true;
|
||||
}
|
||||
|
||||
const json: Payload = yield getEnv(self).fetcher(api, data, {
|
||||
autoAppend: false,
|
||||
@ -568,7 +571,15 @@ export const FormItemStore = StoreNode.named('FormItemStore')
|
||||
})
|
||||
);
|
||||
|
||||
let json = yield fetchOptions(api, data, config, false);
|
||||
let json = yield fetchOptions(
|
||||
api,
|
||||
data,
|
||||
{
|
||||
...config,
|
||||
silent: true
|
||||
},
|
||||
false
|
||||
);
|
||||
if (!json) {
|
||||
setOptions(
|
||||
spliceTree(self.options, indexes, 1, {
|
||||
|
@ -439,6 +439,28 @@ export function isVisible(
|
||||
);
|
||||
}
|
||||
|
||||
export function isUnfolded(
|
||||
node: any,
|
||||
config: {
|
||||
foldedField?: string;
|
||||
unfoldedField?: string;
|
||||
}
|
||||
): boolean {
|
||||
let {foldedField, unfoldedField} = config;
|
||||
|
||||
unfoldedField = unfoldedField || 'unfolded';
|
||||
foldedField = foldedField || 'folded';
|
||||
|
||||
let ret: boolean = false;
|
||||
if (unfoldedField && typeof node[unfoldedField] !== 'undefined') {
|
||||
ret = !!node[unfoldedField];
|
||||
} else if (foldedField && typeof node[foldedField] !== 'undefined') {
|
||||
ret = !node[foldedField];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤掉被隐藏的数组元素
|
||||
*/
|
||||
@ -925,7 +947,7 @@ export function getTree<T extends TreeItem>(
|
||||
*/
|
||||
export function filterTree<T extends TreeItem>(
|
||||
tree: Array<T>,
|
||||
iterator: (item: T, key: number, level: number) => boolean,
|
||||
iterator: (item: T, key: number, level: number) => any,
|
||||
level: number = 1,
|
||||
depthFirst: boolean = false
|
||||
) {
|
||||
@ -935,7 +957,15 @@ export function filterTree<T extends TreeItem>(
|
||||
let children: TreeArray | undefined = item.children
|
||||
? filterTree(item.children, iterator, level + 1, depthFirst)
|
||||
: undefined;
|
||||
children && (item = {...item, children: children});
|
||||
|
||||
if (
|
||||
Array.isArray(children) &&
|
||||
Array.isArray(item.children) &&
|
||||
children.length !== item.children.length
|
||||
) {
|
||||
item = {...item, children: children};
|
||||
}
|
||||
|
||||
return item;
|
||||
})
|
||||
.filter((item, index) => iterator(item, index, level));
|
||||
@ -945,10 +975,20 @@ export function filterTree<T extends TreeItem>(
|
||||
.filter((item, index) => iterator(item, index, level))
|
||||
.map(item => {
|
||||
if (item.children && item.children.splice) {
|
||||
item = {
|
||||
...item,
|
||||
children: filterTree(item.children, iterator, level + 1, depthFirst)
|
||||
};
|
||||
let children = filterTree(
|
||||
item.children,
|
||||
iterator,
|
||||
level + 1,
|
||||
depthFirst
|
||||
);
|
||||
|
||||
if (
|
||||
Array.isArray(children) &&
|
||||
Array.isArray(item.children) &&
|
||||
children.length !== item.children.length
|
||||
) {
|
||||
item = {...item, children: children};
|
||||
}
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user