自下而上搜索以过滤嵌套菜单数组

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)