选择同级节点(递归呈现的复选框)时,如何递归取消选择表兄弟节点

时间:2018-10-02 14:53:19

标签: javascript recursion ember.js

我正在建立一个多选检查组项目列表。目标是一次仅拥有一个活动组。因此,用户可以选择父项-但是如果他们选择了子项,则该子组将成为焦点,而选中的父项将变为未选中状态。

<div class="checkboxhandler">
  <input 
    type="checkbox" 
    checked={{@item.checked}}
    onclick={{action @onClick @item.id}}
  >
  <label>{{@item.label}} -- checked: {{@item.checked}}</label>

  {{#if @item.children}}
    {{#each @item.children as |child|}}

       <CheckboxGroup @item={{child}} @onClick={{@onClick}} />

    {{/each}}
  {{/if}}
</div>

我已经了解了带有递归帮助程序检查的复选框。

这是辅助树-取消选择的逻辑发生在这里。此应用程序还需要保留selectedItems的数组-但需要清除这些数组以及复选框。

const toggle = value => !value;
const disable = () => false;

// the roots / siblings are contained by arrays
export function check(tree, id, transform = toggle) {
  if (tree === undefined) return undefined;

  if (Array.isArray(tree)) {
    return tree.map(t => check(t, id, transform));
  } 

  if (tree.id === id || id === 'all') {
    return checkNode(tree, id, transform);
  }

  if (tree.children) {
    return checkChildren(tree, id, transform);
  }

  return tree;
}

function selectOnlySubtree(tree, id, transform) {
  return tree.map(subTree => {
    const newTree = check(subTree, id, transform);

    if (!newTree.children || (transform !== disable && didChange(newTree, subTree))) {
      return newTree;
    } 

    return disableTree(subTree);
  });
}

function isTargetAtThisLevel(tree, id) {
  return tree.map(t => t.id).includes(id);
}

function checkNode(tree, id, transform) {
  return { 
    ...tree, 
    checked: transform(tree.checked),
    children: disableTree(tree.children)
  };
}

function disableTree(tree) {
  return check(tree, 'all', disable);
}

function checkChildren(tree, id, transform) {
  const newChildren = check(tree.children, id, transform);
  const changed = didChange(tree.children, newChildren);
  const checked = changed ? false : (
    id === 'all' ? transform(tree.checked) : tree.checked
  );

    return { 
        ...tree, 
        checked: checked,
    children: check(tree.children, id, transform) 
  };
}

export function didChange(treeA, treeB) {
  const rootsChanged = treeA.checked !== treeB.checked;

  if (rootsChanged) return true;

  if (Array.isArray(treeA) && Array.isArray(treeB)) {
    return didChangeList(treeA, treeB);
  }

  if (treeA.children && treeB.children) {
        return didChangeList(treeA.children, treeB.children);
  }

  return false;
}

function didChangeList(a, b) {
  const compares = a.map((childA, index) => {
    return didChange(childA, b[index]);
  });

  const nothingChanged = compares.every(v => v === false);

  return !nothingChanged;
}

//最新的余烬小提琴 https://canary.ember-twiddle.com/f28edbf72193427c2a527e51d57e759f?openFiles=components.wrapping-component.js%2Ctemplates.components.checkbox-group.hbs

所以这些是有效条件 -仅由父母选择 enter image description here

-仅选择了孩子 enter image description here

但是当前的错误正在发生

  1. 当前-我可以选择辣椒-然后选择汉堡-并且不取消选中 出现辣椒-这是一个错误
  2. 当前-我可以选择咖啡机-然后选择泡菜-并且不会取消对咖啡机的检查-所以这是一个错误
  3. 当前-我可以选择过滤器-然后选择辣椒-并且不取消过滤器检查-这是一个错误

//错误1的说明-因此在这种情况下,应仅选择汉堡 enter image description here

//错误2的说明-因此在这种情况下,应该只选择泡菜 enter image description here

//问题3的说明-因此在这种情况下,应仅选择辣椒 enter image description here

2 个答案:

答案 0 :(得分:1)

您可以在每次进行false -> true转换后,取消选中整棵树,除了包含当前正在更新的项目的图层。

例如:

  • 环绕一层
  • 使用.find检查被设置为true的项目是否在图层中
    • 如果是:
      • 将其checked属性设置为true
      • 跳过其他项目
    • 如果不是:
      • 将所有项目设置为checked: false
  • 对每个具有children数组的项目进行递归

const turnOn = (options, id) => {
  const target = options.find(o => o.id === id);

  if (target) {
    target.checked = true;        
  } else {
    options.forEach(o => { o.checked = false; })
  }

  // Recurse
  options.forEach(({ children = [] }) => turnOn(children, id));
}

var opts = options();

turnOn(opts, 3);
turnOn(opts, 4);
console.log("Checked after 3 & 4:", getChecked(opts));

turnOn(opts, 2);
console.log("Checked after 2:", getChecked(opts));

turnOn(opts, 6);
console.log("Checked after 6:", getChecked(opts));





function options() {
  return [{
    id: 1,
    label: 'burger',
    checked: false,
    children: [{
      id: 3,
      label: 'tomato',
      checked: false
    }, {
      id: 4,
      label: 'lettus',
      checked: false
    }, {
      id: 5,
      label: 'pickle',
      checked: false
    }]
  }, {
    id: 2,
    label: 'kebab',
    checked: false,
    children: [{
      id: 6,
      label: 'ketchup',
      checked: false
    }, {
      id: 7,
      label: 'chilli',
      checked: false
    }]
  }];
};

function getChecked(opts) {
  return opts.reduce((acc, o) => acc.concat(
    o.checked ? o.label : [],
    getChecked(o.children || [])
  ), []);
}

注意:我使用了一个递归函数,该函数只对树进行变异而不是返回新树,因为它更易于阅读

答案 1 :(得分:0)

我认为,如果在该项目本身上选择了某个项目,则停止存储信息,您实际上可以从很多中受益。这还将使您免于修改项目的麻烦。只需将所选ID的数组分别存储到数据即可。接下来,您只需要一个助手contains/includes

因此您可以执行以下操作:

<input type="checkbox" checked={{contains item.id selectedIds}} />

接下来,您的规则集非常简单。有效选择仅包含相同 group 的项目。一组可能是

  1. 所有父母,或
  2. 同一父母的所有孩子。

现在,不取消选择某些项目要容易得多,而是假设必须选择一项 (您单击的一项)以验证哪些已选择的项目仍然是有效的选择。那就很简单:所有这些都在同一组中。

因此,首先我们找到用户单击的项目的id的组。如果父母名单包含id,则我们使用父母名单,否则我们找到children包含ID的父母:

const group = data.find(x => x.id === id)
  ? data
  : data.find(p => p.children.find(c => c.id === id)).children;

现在我们有了这个小组。要获取组中的所有id,我们需要const validIds = group.map(x => x.id)

接下来,新的selectedIds应该是:

  • 用户点击的项目的id
  • 以及selectedIds中的所有旧validIds

我们可以这样做:

const newSelectedIds = [
  id,
  ...oldSelectedIds.filter(s => validIds.includes(s)),
];

here is a twiddle implementing this approach


如果将ember-data用于数据,则实际上可以进一步简化此操作。如果您有父母与子女之间的关系,则可以通过两种方式关注这种关系。因此,您可以通过item.parent.children之类的方式查找所有兄弟姐妹。然后,您可以将模型而不是仅将id传递给函数,并使用以下内容找到有效的组:

const group = item instanceof ParentModel
  ? this.data
  : item.parent.children;

更具可读性。