如何更新每个表单元素的状态更改嵌套数据结构的正确 属性,这是嵌套表单元素 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?
我想在 Javascript 和 return 中修改深层嵌套对象中的 属性 修改对象。例如,我在我的应用程序中呈现 checkboxes 并且结构如下所示,
{
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,
}
}
}
}
}
}
我正在渲染上面的结构,如下所示,
现在,如果用户单击任何复选框,我想 return 具有更新状态的修改对象,所以假设用户单击了 level4 复选框,我希望下面的对象被 returned。另外,我有对应于选中复选框的键,所以对于上述情况,我有'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,
}
}
}
}
}
}
我编写了以下函数来修改值,但在 return 创建新对象时遇到困难。此外,该对象可以深入嵌套到任何级别,
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;
}
}
}
你能帮忙吗?
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;
}
下面介绍的是实现所需 objective.
的一种可能方法
代码段
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 }
说明
添加到上面代码段的评论。
以下示例代码提供了基于 view-model 的 vanilla-implementation 方法,该方法支持双向 state-changes ... 即 ... (1) view-changes 更新view-model 和 (2) view-model 触发 state-changes 更新视图。
主函数,命名为createControlViewModel
,根据提供的嵌套form-control的DOM-structure创建一个嵌套view-model。
由于实施遵循一些通用规则,因此可以从 different/varying HTML 标记创建 view-models。它最重要的特点是嵌套模型不是递归构建的。但是基于...
- 额外提供的控件特定选择器
- 以及一个额外提供的选择器,它针对每个控件的父级 component/node、
...与固定的蓝图模型相比,每个控件的嵌套层次结构级别可以用 flexible/generic 多得多的方式来识别。后者不允许在 HTML 标记的 and/or 范围内有任何灵活性。
还可以提供 property/attribute 个名称的列表,这些名称预定义了想要成为双向状态更改处理的一部分的键。
// +++ 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>
我想在 Javascript 和 return 中修改深层嵌套对象中的 属性 修改对象。例如,我在我的应用程序中呈现 checkboxes 并且结构如下所示,
{
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,
}
}
}
}
}
}
我正在渲染上面的结构,如下所示,
现在,如果用户单击任何复选框,我想 return 具有更新状态的修改对象,所以假设用户单击了 level4 复选框,我希望下面的对象被 returned。另外,我有对应于选中复选框的键,所以对于上述情况,我有'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,
}
}
}
}
}
}
我编写了以下函数来修改值,但在 return 创建新对象时遇到困难。此外,该对象可以深入嵌套到任何级别,
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;
}
}
}
你能帮忙吗?
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;
}
下面介绍的是实现所需 objective.
的一种可能方法代码段
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 }
说明
添加到上面代码段的评论。
以下示例代码提供了基于 view-model 的 vanilla-implementation 方法,该方法支持双向 state-changes ... 即 ... (1) view-changes 更新view-model 和 (2) view-model 触发 state-changes 更新视图。
主函数,命名为createControlViewModel
,根据提供的嵌套form-control的DOM-structure创建一个嵌套view-model。
由于实施遵循一些通用规则,因此可以从 different/varying HTML 标记创建 view-models。它最重要的特点是嵌套模型不是递归构建的。但是基于...
- 额外提供的控件特定选择器
- 以及一个额外提供的选择器,它针对每个控件的父级 component/node、
...与固定的蓝图模型相比,每个控件的嵌套层次结构级别可以用 flexible/generic 多得多的方式来识别。后者不允许在 HTML 标记的 and/or 范围内有任何灵活性。
还可以提供 property/attribute 个名称的列表,这些名称预定义了想要成为双向状态更改处理的一部分的键。
// +++ 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>