[英]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 ...但基于...
... 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.