自下而上搜索以过滤嵌套菜单数组
Bottom Up Search to Filter a Nested Menu Array
我在这里搜索了自下而上的搜索示例,我了解它们是如何完成的,但我有一个我无法解决的特定需求。
我们有一个菜单系统,我需要为其创建过滤器。
例如,如果菜单是
- Testing
- Test
- Something
- Something Else
- Test
如果我过滤“测试”,我希望返回 4 个节点,并删除其他所有内容。棘手的部分是在这种情况下它无法删除“Something”节点,因为它具有需要访问的匹配子节点。
我的代码有效,但仅适用于顶级项目,因为在第一个递归步骤中 filter
通过删除任何可能具有匹配子项但父项不匹配的内容。
private recursiveFilter(menuItems: MenuItem[], label: string): MenuItem[] {
if (label === '') {
this.menuItems = this.originalMenuItems;
return this.menuItems;
} else if (!menuItems) {
return [];
}
return menuItems
.filter((el) => el.label?.toLowerCase().includes(label.toLowerCase()))
.map((el) => {
if (!(el.items || !Array.isArray(el.items))) {
return el;
} else {
const menuChildren = el.items as MenuItem[];
if (menuChildren) {
el.items = this.recursiveFilter(menuChildren, label);
}
return el;
}
});
}
具有嵌套子项的 MenuItem 示例:
{
"label": "Messages",
"expanded": false,
"items": [
{
"label": "Dashboard",
"expanded": false,
"routerLink": "/messages",
"visible": true
},
{
"label": "Voicemail",
"expanded": false,
"items": [
{
"label": "Inbox",
"icon": "pi pi-fw pi-folder",
"routerLink": "/mailbox/inbox"
},
{
"label": "Archived",
"icon": "pi pi-fw pi-folder",
"routerLink": "/mailbox/archived"
},
{
"label": "Trash",
"icon": "pi pi-fw pi-folder",
"routerLink": "/mailbox/trash"
}
]
},
{
"label": "Text",
"expanded": false,
"items": [
{
"label": "Messages",
"routerLink": "/sms",
"routerLinkActiveOptions": {
"exact": true
}
},
{
"label": "Send",
"routerLink": "/sms/send",
"routerLinkActiveOptions": {
"exact": true
}
},
{
"label": "Contacts",
"routerLink": "/sms/contacts"
}
]
}
]
}
在递归调用确定要保留哪些子项后进行过滤。如果过滤后仍然存在任何子元素 或 标签匹配,则保留该父项。
大致如下:
private recursiveFilter(menuItems: MenuItem[], label: string, labelLower = label.toLowerCase()) {
return menuItems.filter((item) => {
if (item.items) item.items = recursiveFilter(item.items, label, labelLower);
return item.label?.toLowerCase().includes(labelLower) || item.items?.length;
});
}
.filter
中的副作用有点臭,但我认为这是解决这个问题的最清晰的方法。
现场演示:
const recursiveFilter = (menuItems, label) => (
menuItems.filter((item) => {
if (item.items) item.items = recursiveFilter(item.items, label);
return item.label?.toLowerCase().includes(label) || item.items?.length;
})
);
const topItem={label:"Messages",expanded:!1,items:[{label:"Dashboard",expanded:!1,routerLink:"/messages",visible:!0},{label:"Voicemail",expanded:!1,items:[{label:"Inbox",icon:"pi pi-fw pi-folder",routerLink:"/mailbox/inbox"},{label:"Archived",icon:"pi pi-fw pi-folder",routerLink:"/mailbox/archived"},{label:"Trash",icon:"pi pi-fw pi-folder",routerLink:"/mailbox/trash"}]},{label:"Text",expanded:!1,items:[{label:"Messages",routerLink:"/sms",routerLinkActiveOptions:{exact:!0}},{label:"Send",routerLink:"/sms/send",routerLinkActiveOptions:{exact:!0}},{label:"Contacts",routerLink:"/sms/contacts"}]}]};
topItem.items = recursiveFilter(topItem.items, 'send');
console.log(topItem);
我更喜欢将递归过滤与所需的特定检查分开。然后我们可以根据需要传入一个谓词函数。我觉得这更简单。所以我可能会这样写:
const deepFilter = (pred) => ({items = [], ...rest}) => {
const children = items .flatMap (deepFilter (pred))
return (children .length || pred (rest))
? [{...rest, ...(items.length ? {items: children} : {})}]
: []
}
const matchLabel = (text) => ({label = ''}) =>
label .toLowerCase () .includes (text .toLowerCase())
const filterByLabel = (t) =>
deepFilter (matchLabel (t))
const menuItem = {label: "Messages", expanded: false, items: [{label: "Dashboard", expanded: false, routerLink: "/messages", visible: true}, {label: "Voicemail", expanded: false, items: [{label: "Inbox", icon: "pi pi-fw pi-folder", routerLink: "/mailbox/inbox"}, {label: "Archived", icon: "pi pi-fw pi-folder", routerLink: "/mailbox/archived"}, {label: "Trash", icon: "pi pi-fw pi-folder", routerLink: "/mailbox/trash"}]}, {label: "Text", expanded: false, items: [{label: "Messages", routerLink: "/sms", routerLinkActiveOptions: {exact: true}}, {label: "Send", routerLink: "/sms/send", routerLinkActiveOptions: {exact: true}}, {label: "Contacts", routerLink: "/sms/contacts"}]}]}
console .log ('labels containing "send":', filterByLabel ('send') (menuItem))
console .log ('labels containing "a":', filterByLabel ('a') (menuItem))
console .log ('labels containing "foobar":', filterByLabel ('foobar') (menuItem))
.as-console-wrapper {max-height: 100% !important; top: 0}
这里,deepFilter
接受一个谓词函数,returns一个函数接受一个树children嵌套为items
和returns只有那些项目匹配谓词或有 children 匹配谓词。我们编写 matchLabel
谓词,它接受一个搜索字符串和 returns 一个函数,该函数测试传递给它的 object 是否有一个 label
属性 那个 (case-insensitively) 包含搜索词。最后,filterByLabel
简单地组合它们,接受一个搜索词并返回一个函数,该函数接受一个项目和(递归地)returns 如果它匹配该词或者它的任何 children 做.
变化
有些事情我们可能想要改变。
首先,matchLabel
每次都是在文本上调用.toLowerCase
。我们可以通过以下替代方法避免这种情况:
const matchLabel = (t, text = t.toLowerCase()) => ({label = ''}) =>
label .toLowerCase () .includes (text)
其次,deepFilter
很好而且通用...除了它 hard-codes 函数中 items
数组的树结构。我们可能希望通过将该节点名称作为参数来使其更通用。我们可以通过
const deepFilter = (childName) => (pred) => ({[childName]: items = [], ...rest}) => {
const children = items .flatMap (deepFilter (childName) (pred))
return (children .length || pred (rest))
? [{...rest, ...(items.length ? {[childName]: children} : {})}]
: []
}
然后像这样使用它:
const filterByLabel = (t) =>
deepFilter ('items') (matchLabel (t))
或者通过命名中间函数:
const deepFilterItemTree = deepFilter ('items')
const filterByLabel = (t) =>
deepFilterItemTree (matchLabel (t))
第三,如果我们碰巧有一个compose
函数,我们可以将主函数重写为
const filterByLabel = compose (deepFilter, matchLabel)
或者如果我们采纳第二个建议,作为以下之一:
const filterByLabel = compose (deepFilter ('items'), matchLabel)
// or
const filterByLabel = compose (deepFilterItemTree, matchLabel)
我在这里搜索了自下而上的搜索示例,我了解它们是如何完成的,但我有一个我无法解决的特定需求。
我们有一个菜单系统,我需要为其创建过滤器。
例如,如果菜单是
- Testing
- Test
- Something
- Something Else
- Test
如果我过滤“测试”,我希望返回 4 个节点,并删除其他所有内容。棘手的部分是在这种情况下它无法删除“Something”节点,因为它具有需要访问的匹配子节点。
我的代码有效,但仅适用于顶级项目,因为在第一个递归步骤中 filter
通过删除任何可能具有匹配子项但父项不匹配的内容。
private recursiveFilter(menuItems: MenuItem[], label: string): MenuItem[] {
if (label === '') {
this.menuItems = this.originalMenuItems;
return this.menuItems;
} else if (!menuItems) {
return [];
}
return menuItems
.filter((el) => el.label?.toLowerCase().includes(label.toLowerCase()))
.map((el) => {
if (!(el.items || !Array.isArray(el.items))) {
return el;
} else {
const menuChildren = el.items as MenuItem[];
if (menuChildren) {
el.items = this.recursiveFilter(menuChildren, label);
}
return el;
}
});
}
具有嵌套子项的 MenuItem 示例:
{
"label": "Messages",
"expanded": false,
"items": [
{
"label": "Dashboard",
"expanded": false,
"routerLink": "/messages",
"visible": true
},
{
"label": "Voicemail",
"expanded": false,
"items": [
{
"label": "Inbox",
"icon": "pi pi-fw pi-folder",
"routerLink": "/mailbox/inbox"
},
{
"label": "Archived",
"icon": "pi pi-fw pi-folder",
"routerLink": "/mailbox/archived"
},
{
"label": "Trash",
"icon": "pi pi-fw pi-folder",
"routerLink": "/mailbox/trash"
}
]
},
{
"label": "Text",
"expanded": false,
"items": [
{
"label": "Messages",
"routerLink": "/sms",
"routerLinkActiveOptions": {
"exact": true
}
},
{
"label": "Send",
"routerLink": "/sms/send",
"routerLinkActiveOptions": {
"exact": true
}
},
{
"label": "Contacts",
"routerLink": "/sms/contacts"
}
]
}
]
}
在递归调用确定要保留哪些子项后进行过滤。如果过滤后仍然存在任何子元素 或 标签匹配,则保留该父项。
大致如下:
private recursiveFilter(menuItems: MenuItem[], label: string, labelLower = label.toLowerCase()) {
return menuItems.filter((item) => {
if (item.items) item.items = recursiveFilter(item.items, label, labelLower);
return item.label?.toLowerCase().includes(labelLower) || item.items?.length;
});
}
.filter
中的副作用有点臭,但我认为这是解决这个问题的最清晰的方法。
现场演示:
const recursiveFilter = (menuItems, label) => (
menuItems.filter((item) => {
if (item.items) item.items = recursiveFilter(item.items, label);
return item.label?.toLowerCase().includes(label) || item.items?.length;
})
);
const topItem={label:"Messages",expanded:!1,items:[{label:"Dashboard",expanded:!1,routerLink:"/messages",visible:!0},{label:"Voicemail",expanded:!1,items:[{label:"Inbox",icon:"pi pi-fw pi-folder",routerLink:"/mailbox/inbox"},{label:"Archived",icon:"pi pi-fw pi-folder",routerLink:"/mailbox/archived"},{label:"Trash",icon:"pi pi-fw pi-folder",routerLink:"/mailbox/trash"}]},{label:"Text",expanded:!1,items:[{label:"Messages",routerLink:"/sms",routerLinkActiveOptions:{exact:!0}},{label:"Send",routerLink:"/sms/send",routerLinkActiveOptions:{exact:!0}},{label:"Contacts",routerLink:"/sms/contacts"}]}]};
topItem.items = recursiveFilter(topItem.items, 'send');
console.log(topItem);
我更喜欢将递归过滤与所需的特定检查分开。然后我们可以根据需要传入一个谓词函数。我觉得这更简单。所以我可能会这样写:
const deepFilter = (pred) => ({items = [], ...rest}) => {
const children = items .flatMap (deepFilter (pred))
return (children .length || pred (rest))
? [{...rest, ...(items.length ? {items: children} : {})}]
: []
}
const matchLabel = (text) => ({label = ''}) =>
label .toLowerCase () .includes (text .toLowerCase())
const filterByLabel = (t) =>
deepFilter (matchLabel (t))
const menuItem = {label: "Messages", expanded: false, items: [{label: "Dashboard", expanded: false, routerLink: "/messages", visible: true}, {label: "Voicemail", expanded: false, items: [{label: "Inbox", icon: "pi pi-fw pi-folder", routerLink: "/mailbox/inbox"}, {label: "Archived", icon: "pi pi-fw pi-folder", routerLink: "/mailbox/archived"}, {label: "Trash", icon: "pi pi-fw pi-folder", routerLink: "/mailbox/trash"}]}, {label: "Text", expanded: false, items: [{label: "Messages", routerLink: "/sms", routerLinkActiveOptions: {exact: true}}, {label: "Send", routerLink: "/sms/send", routerLinkActiveOptions: {exact: true}}, {label: "Contacts", routerLink: "/sms/contacts"}]}]}
console .log ('labels containing "send":', filterByLabel ('send') (menuItem))
console .log ('labels containing "a":', filterByLabel ('a') (menuItem))
console .log ('labels containing "foobar":', filterByLabel ('foobar') (menuItem))
.as-console-wrapper {max-height: 100% !important; top: 0}
这里,deepFilter
接受一个谓词函数,returns一个函数接受一个树children嵌套为items
和returns只有那些项目匹配谓词或有 children 匹配谓词。我们编写 matchLabel
谓词,它接受一个搜索字符串和 returns 一个函数,该函数测试传递给它的 object 是否有一个 label
属性 那个 (case-insensitively) 包含搜索词。最后,filterByLabel
简单地组合它们,接受一个搜索词并返回一个函数,该函数接受一个项目和(递归地)returns 如果它匹配该词或者它的任何 children 做.
变化
有些事情我们可能想要改变。
首先,
matchLabel
每次都是在文本上调用.toLowerCase
。我们可以通过以下替代方法避免这种情况:const matchLabel = (t, text = t.toLowerCase()) => ({label = ''}) => label .toLowerCase () .includes (text)
其次,
deepFilter
很好而且通用...除了它 hard-codes 函数中items
数组的树结构。我们可能希望通过将该节点名称作为参数来使其更通用。我们可以通过const deepFilter = (childName) => (pred) => ({[childName]: items = [], ...rest}) => { const children = items .flatMap (deepFilter (childName) (pred)) return (children .length || pred (rest)) ? [{...rest, ...(items.length ? {[childName]: children} : {})}] : [] }
然后像这样使用它:
const filterByLabel = (t) => deepFilter ('items') (matchLabel (t))
或者通过命名中间函数:
const deepFilterItemTree = deepFilter ('items') const filterByLabel = (t) => deepFilterItemTree (matchLabel (t))
第三,如果我们碰巧有一个
compose
函数,我们可以将主函数重写为const filterByLabel = compose (deepFilter, matchLabel)
或者如果我们采纳第二个建议,作为以下之一:
const filterByLabel = compose (deepFilter ('items'), matchLabel) // or const filterByLabel = compose (deepFilterItemTree, matchLabel)