简体   繁体   English

如何更新每个表单元素的状态以更改嵌套数据结构的正确属性,该数据结构是嵌套表单元素 DOM 的模型?

[英]How to update at each form element's state change the correct property of a nested data-structure which is a model of the nested form element DOM?

I want to modify a property in deeply nested object in Javascript and return the modified object.我想在 Javascript 中修改深度嵌套对象中的属性并返回修改后的对象。 For eg, I am rendering checkboxes in my application and the structure looks like below,例如,我在我的应用程序中呈现复选框,结构如下所示,

{
    level1: {
    name: 'Level 1',
    key: 'level1',
    checked: false,
    subLevels: {
      level2: {
        name: 'Level 2',
        key: 'level2',
        checked: false,
        subLevels: {
          level3: {
            name: 'Level 3',
            key: 'level3',
            checked: true,
          },
          level4: {
            name: 'Level 4',
            key: 'level4',
            checked: false,
          }
        }
      }
    }
  }
}

I am rendering the above structure like below,我正在渲染上面的结构,如下所示, 渲染的复选框

Now, if a user clicks on any of the checkboxes, I want to return the modified object with the updated state, so let's say if the user clicked on level4 checkbox, I want the below object to be returned.现在,如果用户单击任何复选框,我想返回具有更新状态的修改对象,因此假设用户单击level4复选框,我希望返回以下对象。 Also, I have the key corresponding to the checked checkbox, so for above scenario, i have ' level4 '.另外,我有对应于选中复选框的键,所以对于上述情况,我有' level4 '。

{
    level1: {
    name: 'Level 1',
    key: 'level1',
    checked: false,
    subLevels: {
      level2: {
        name: 'Level 2',
        key: 'level2',
        checked: false,
        subLevels: {
          level3: {
            name: 'Level 3',
            key: 'level3',
            checked: true,
          },
          level4: {
            name: 'Level 4',
            key: 'level4',
            checked: true,
          }
        }
      }
    }
  }
}

I wrote the below function to modify the value, but facing difficulty in returning a new object.我编写了以下函数来修改值,但在返回新对象时遇到了困难。 Also, the object could be deeply nested to any level,此外,对象可以深度嵌套到任何级别,

function changeVal(obj, checkedKey) {
    for(const key in obj) {
        if(key === 'subLevels' && typeof obj.subLevels === 'object') {
            changeVal(obj[key].subLevels);
        } 
        if(key === checkedKey) {
            obj[key].checked = !obj[key].checked;
        } 
    }
}

Could you please help out?你能帮忙吗?

Presented below is one possible way to achieve the desired objective.下面介绍的是实现预期目标的一种可能方式。

Code Snippet代码片段

 const myUpdate = (obj, k) => ( [k] in obj ? obj[k].checked = !obj[k].checked : Object.values(obj).forEach( v => myUpdate(v?.subLevels ?? {}, k) ), obj ); /* EXPLANATION of the code --- // method to update a "cloned" object // the caller passes a deep-cloned object // by using "structuredClone()" const myUpdate = (obj, k) => { // if "k" (say "level4") is in "obj" if ([k] in obj) { // just flip the "checked" prop (false to true, or vice-versa) obj[k].checked = !obj[k].checked } else { // else, recursive call using the "subLevels" prop // if there are no values in obj or no "subLevels" // simply pass empty object for recursion Object.values(obj).forEach( v => myUpdate(v?.subLevels ?? {}, k) ) }; // always return "obj" return obj; }; */ const dataObj = { level1: { name: 'Level 1', key: 'level1', checked: false, subLevels: { level2: { name: 'Level 2', key: 'level2', checked: false, subLevels: { level3: { name: 'Level 3', key: 'level3', checked: true, }, level4: { name: 'Level 4', key: 'level4', checked: false, } } } } } }; console.log( '\n\n setting level-4 to true :\n', myUpdate(structuredClone(dataObj), 'level4'), '\n\n setting level-3 to false :\n', myUpdate(structuredClone(dataObj), 'level3'), '\n\nand now the existing obj, un-altered:\n', dataObj, );
 .as-console-wrapper { max-height: 100% !important; top: 0 }

Explanation解释

Comments added to the snippet above.评论添加到上面的片段。

The following example code provides a vanilla-implementation of a view-model based approach which enables bidirectional state-changes ... which is ... (1) view-changes update the view-model and (2) view-model triggered state-changes update the view.以下示例代码提供了基于视图模型的方法的普通实现,该方法启用了双向状态更改......这是......(1)视图更改更新视图模型和(2)视图模型触发状态-changes 更新视图。

The main function, named createControlViewModel , creates a nested view-model according to the provided nested form-control's DOM-structure.名为createControlViewModel的主函数根据提供的嵌套表单控件的 DOM 结构创建嵌套视图模型。

Since the implementation follows some generic rules, one can create view-models from different/varying HTML markup.由于实现遵循一些通用规则,因此可以从不同/变化的 HTML 标记创建视图模型。 Its most important feature is that the nested model is not build recursively.它最重要的特点是嵌套模型不是递归构建的。 But based on ...但基于...

  • an additionally provided control specific selector额外提供的控制特定选择器
  • and an additionally provided selector which targets each control's parent component/node,以及一个额外提供的选择器,它针对每个控件的父组件/节点,

... the nested hierarchy level of each control can be identified in a far more flexible/generic way in comparison to a fixed blueprint model. ...与固定的蓝图模型相比,每个控件的嵌套层次结构级别可以以更加灵活/通用的方式识别。 The latter would not allow any flexibility within and/or variety of the HTML markup.后者不允许 HTML 标记内部和/或变化的任何灵活性。

One also can provide a list of property/attribute names which predefine the keys one wants to be part of the bidirectional state change handling.还可以提供属性/属性名称列表,这些名称预定义了希望成为双向状态更改处理的一部分的键。

 // +++ proof of concept / demo related code +++ // returns control specific pure model-data (according to the OP's model) // from the control specific view-model. function createCurrentChangeSnapshot({ node, children, ...modelData }) { return { ...modelData }; } // returns the pure overall model-data (according to the OP's model) // from the overall view-model. function createOverallModelSnapshot(model) { return Object .entries(model) .reduce((snapshot, [key, value]) => { const { node, children = null, ...modelData } = value; snapshot[key] = { ...modelData }; if (children !== null) { Object .assign(snapshot[key], { children: createOverallModelSnapshot(children) }); } return snapshot; }, {}); } // proof of concept related logging. function logModelSnapshots(viewModel, { model }) { // console.log({ model }); const overallModel = createOverallModelSnapshot(viewModel); const currentChange = createCurrentChangeSnapshot(model); console.log({ snapshots: { currentChange, overallModel } }); } // +++ model and view implementation related code +++ function handleViewStateChange(root, model, mutation) { const { target, attributeName, oldValue: recentValue = null } = mutation; root.dispatchEvent( new CustomEvent('view:state:change', { detail: { model, target, ...( (recentValue === null) // omit `recentValue` and alias `attributeName` as `propertyName` // in case mutation observer was not involved in the state change. ? { propertyName: attributeName } : { recentValue, attributeName } ), } }) ); } function applyViewToModelHandling(model, key, control, root) { // an 'attributes' type mutation does not cover an element's // property state change like `checked` for radio/checkbox // controls or even a form control's `value` change ... const observer = new MutationObserver( (mutationList/*, observer*/) => { mutationList.forEach(mutation => { debugger; if ( mutation.type === 'attributes' && mutation.attributeName === key ) { handleViewStateChange(root, model, mutation); } }); } ); observer.observe(control, { attributes: true }); // ... thus in order to compensate PROPERTY state changes // which are left unhandled by observing ATTRIBUTES mutations, // a form control additionally listens to an 'input' event and // forwards the change to a common view-state change-handler. control .addEventListener('input', ({ currentTarget }) => handleViewStateChange( root, model, { target: currentTarget, attributeName: key }, ) ); } function applyModelToViewHandling(model, key, control) { Object.defineProperty(model, key, { get() { return control[key]; }, set(value) { control[key] = value; }, enumerable: true, }); } function applyStateChangeHandlingToBoundContext(key) { const { root, model } = this; const { node: control } = model; applyModelToViewHandling(model, key, control); applyViewToModelHandling(model, key, control, root); } function enableStateChangeHandling(root, model, propertyNames) { propertyNames .forEach(applyStateChangeHandlingToBoundContext, { root, model }); } /** * - The main function creates a nested view-model according * to the provided nested form-control's DOM-structure. * - Since the implementation follows some generic rules, one can * create view-models from different/varying HTML markup. * - Its most important feature is that the nested model is not * build recursively. But based on ... * - an additionally provided control specific selector * - and an additionally provided selector which targets * each control's parent component/node, * ... the nested hierarchy level of each control can be * identified in a far more flexible/generic way in comparison * to a fixed blueprint model. The latter would not allow any * flexibility within and/or variety of the HTML markup. * - One also can provide a list of property/attribute names which * predefine the keys one wants to be part of the bidirectional * state change handling. */ function createControlViewModel( root, controlSelector, parentComponentSelector, propertyNames, ) { const modelStorage = new Map; const controlList = [ ...root .querySelectorAll(controlSelector) ]; const viewModel = controlList .reduce((modelRoot, control) => { const parentComponent = control .closest(parentComponentSelector) ?.parentElement ?.closest(parentComponentSelector); // retrieve model data from control. const { name: key, dataset: { name } } = control; // create control specific view-model. const controlModel = { node: control, key, name }; // store the control specific view-model // by the control element's reference. modelStorage.set(control, controlModel); // enable bidirectional state change // handling for any specified property. enableStateChangeHandling(root, controlModel, propertyNames); if (!parentComponent || !root.contains(parentComponent)) { // first level controls within root. modelRoot[key] = controlModel; } else { const parentControl = parentComponent .querySelector(controlSelector); // retrieve parent control model from view-model storage. const parentControlModel = modelStorage.get(parentControl); // child level controls of related parent. (parentControlModel.children ??= {})[key] = controlModel; // use `children` rather than the OP's `subLevels` property name. // (parentControlModel.subLevels ??= {})[key] = controlModel; } return modelRoot; }, {}); // proof of concept related logging. console.log({ controlList, viewModel }); root .addEventListener( 'view:state:change', ({ detail }) => logModelSnapshots(viewModel, detail), ); return viewModel; } // +++ proof of concept / demo +++ const viewModel = createControlViewModel( document.body, 'li > label > [type="checkbox"]', 'li', ['checked'], ); // - change view states, here the checkbox control's // `checked` properties via the overall view model. viewModel['level-1-a'] .children['level-2-a'] .children['level-3-b'].checked = true; viewModel['level-1-a'] .children['level-2-b'].checked = true; viewModel['level-1-b'] .checked = true;
 body { margin: 0; } ul { margin: 0; padding: 0 0 0 20px; } .as-console-wrapper { left: auto!important; width: 75%; min-height: 100%!important; }
 <ul> <li> <label> <input type="checkbox" name="level-1-a" data-name="Level 1 a" > <span class="label"> Level 1 a </span> </label> <ul> <li> <label> <input type="checkbox" name="level-2-a" data-name="Level 2 a" > <span class="label"> Level 2 a </span> </label> <ul> <li> <label> <input type="checkbox" name="level-3-a" data-name="Level 3 a" > <span class="label"> Level 3 a </span> </label> </li> <li> <label> <input type="checkbox" name="level-3-b" data-name="Level 3 b" > <span class="label"> Level 3 b </span> </label> </li> </ul> </li> <li> <label> <input type="checkbox" name="level-2-b" data-name="Level 2 b" > <span class="label"> Level 2 b </span> </label> </li> </ul> </li> <li> <label> <input type="checkbox" name="level-1-b" data-name="Level 1 b" > <span class="label"> Level 1 b </span> </label> </li> </ul>

 var data = { level1: { name: 'Level 1', key: 'level1', checked: false, subLevels: { level2: { name: 'Level 2', key: 'level2', checked: false, subLevels: { level3: { name: 'Level 3', key: 'level3', checked: true, }, level4: { name: 'Level 4', key: 'level4', checked: false, } } } } } } var newJsonObject = traverseNesteddata(data, "level4"); console.log(newJsonObject); var keepTheLevel4; function traverseNesteddata(data, checkedKey){ for(var singleValue in data){ if(typeof data[singleValue] == 'object'){ traverseNesteddata(data[singleValue], checkedKey); }else{ if(data[singleValue] === checkedKey) { if(data.checked === false) data.checked = true; else data.checked = false; }} } return data; }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM