Tree 支持懒加载 (#1846)

* Nav 支持配置展开和收起的字段名

* feat: Tree 支持懒加载

* 补充懒加载文档

* 文档调整

* 错别字
This commit is contained in:
liaoxuezhi 2021-04-22 16:42:37 +08:00 committed by GitHub
parent a9a57c6ae1
commit 9d06028e08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 258 additions and 64 deletions

View File

@ -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) 中的配置以外,还支持下面一些配置

View File

@ -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',

View File

@ -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);

View File

@ -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];
}

View File

@ -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>

View File

@ -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>

View File

@ -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;
}

View File

@ -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, {

View File

@ -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;
});