How do I recursively de-select cousin nodes when selecting sibling nodes (recursively rendered checkboxes)

不想你离开。 提交于 2019-12-22 18:52:09

问题


I am building a multi-select checkdown group item list. The goal is to have only ONE active group at a time. So the user can select parents - but if they select a child item, that child group becomes the focus and the selected parents become unchecked.

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

I've got as far with the checkboxes with recursive helper checks.

This is the helper tree - where the logic to deselect takes place. This application also needs to hold the array for selectedItems - but needs to clear those array's as well as the checkboxes.

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

//latest ember fiddle https://canary.ember-twiddle.com/f28edbf72193427c2a527e51d57e759f?openFiles=components.wrapping-component.js%2Ctemplates.components.checkbox-group.hbs

so these are valid conditions -- just parents selected

-- just children selected

but the current bugs are occuring

  1. currently - I can select chilli - then burger - and no decheck of chilli occurs - so that's a bug
  2. currently - I can select coffee maker - then pickle - and no decheck of coffee maker occurs - so that's a bug
  3. currently - I can select filter - then chilli - and no decheck of filter occurs - so that's a bug

//illustration of error 1 - so only burger should remain selected in this instance

//illustration of error 2 - so only pickle should remain selected in this instance

//illustration of problem 3 - so only chilli should remain selected in this instance


回答1:


You could, after every false -> true transition, uncheck the whole tree except the layer that includes the item that's currently being updated.

For example:

  • Loop over a layer
  • Check if the item that's being set to true is in the layer using .find
    • If it is:
      • Set it's checked property to true
      • Skip the other items
    • If it is not:
      • Set all items to checked: false
  • Recurse for every item with a children array

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 || [])
  ), []);
}

Note: I went with a recursive function that simply mutates the tree rather than returning a new one because it's easier to read




回答2:


I think you could actually benefit a lot if you would stop storing the information if a item is selected on that item itself. This will also save you from the problem to modify the items. Just store an array of selected ids separately to the data. Next you only need one helper, contains/includes.

So you can do something like this:

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

Next your ruleset was pretty simple. A valid selection contains only items of the same group. A group may be

  1. all parents, or
  2. all children of the same parent.

Now it's far easier to not deselect some items, but assuming that one item must be selected (the one you clicked on) to verify which already selected items are still valid selections. And thats pretty easy: all that are in the same group.

So first we find the group for the id of the item the user clicked on. If the list of parents contains the id then we use the list of parents, otherwise we find the parent which children contains the id:

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

Now we have the group. To get all ids of the group we do const validIds = group.map(x => x.id).

Next the new selectedIds should be:

  • the id of the item the user clicked on,
  • and all old selectedIds that are also in validIds.

We can do this like this:

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

here is a twiddle implementing this approach.


If you use ember-data for your data you can actually simplify this even more. If you have a relationship between parents and children, you can follow the relationship in both ways. So you can find all siblings by something like item.parent.children. Then you can pass the model instead of just the id to the function and find the valid group with something like this:

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

which is a bit more readable.



来源:https://stackoverflow.com/questions/52610960/how-do-i-recursively-de-select-cousin-nodes-when-selecting-sibling-nodes-recurs

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!