I've had to do this a few times too. You're pretty close already with splitting by the period. From here, you'll just need to use bracket notation to iterate into each level of depth determined by that string, and then use map()
to return the new array of matching property values.
The only complication I see between the data you are trying to retrieve and the formfieldkeyPath
you want to use (department.subjectCost
) is that department
is the area you want to iterate through and subjectCost
is the data we'll want to collect via map()
. This will leave some ambiguity as to which property path "part" is the array to iterate through, and which is the data you want to collect when you loop through that array.
To resolve this, we will need to use a slightly different symbol to separate those two parts, the path to the array you want to loop through, and the path to the property you want to collect within that array. Essentially, we'll be constructing two nested arrays populated with the path names and then loop through those property path names to get the desired data.
For instance, we could transform obj.subobj.department->subjectCost
into [['obj','subobj','department'],['subjectCost']]
if your object was nested in two other objects obj
and subobj
. So to make this work for your simpler example, we'll just use department->subjectCost
since we'll need to save the .
character for objects with higher complexity.
** One other important note here is that because we'll be using the symbols .
and ->
to split our string, this will conflict with any objects keys that actually contain those exact strings in their property names, so be wary of that. If you do have properties with either of those symbols in their names, simply change the symbols used in my provided function below. Perhaps instead of .
and ->
, you can use ..
and ...
where two dots could represent a path (one level down) and the three dots would represent the split between the array's path and the path to the desired data you are collecting. In our case, it would be department...subjectCost
.
Example 1: using your provided sample data
Here it is in action for department->subjectCost
using the sample data you provided (.
and ->
):
Usage
iterPropPath(SubjectViewModel[0], 'department->subjectCost');
// -> [null, 50]
Full Example
const SubjectViewModel = [{
id: 4,
SubjectInfo: 'xx',
studentId: 16,
department: [{
id: 1,
departmentName: 'xxx',
subjectCost: null,
departmentBenefits: [{/**/}, {/**/}]
}, {
id: 2,
departmentName: 'yyy',
subjectCost: 50,
departmentBenefits: [{/**/}, {/**/}]
}],
studentBenefits: [{/**/}, {/**/}]
}];
const iterPropPath = (obj, path) => {
path = path.replace(/\[(\w+)\]/g, '.$1').replace(/^\./, '');
const pathArray = path.split('->').map(e => e.split('.').filter(f => f)).filter(e => e && e.length);
if (pathArray.length !== 2) {
console.error('iterPropPath requires a valid object path to a nested array and a valid path within that array to the desired property value. Property path levels should be delimited by `.` and the array path/property path should be delimited by `->`.');
return false;
}
const [arrPath, propPath] = pathArray;
let arr = obj;
let arrPathValid = true;
arrPath.slice().forEach(step => arr.hasOwnProperty(step) ? (arr = arr[arrPath.shift(1)], !arrPath.length && (!Array.isArray(arr) || !arr.length) && (arrPathValid = false)) : (arrPathValid = false));
if (!arrPathValid) {
console.error(`Array path ${arrPath.join('.')} is invalid.`);
return false;
}
return arr.map((e,iPropPath) => (iPropPath = propPath.slice(), propPath.forEach(step => e && e.hasOwnProperty(step) && (e = e[iPropPath.shift(1)])), e));
};
const formfieldkeyPath = 'department->subjectCost';
const subjectCosts = iterPropPath(SubjectViewModel[0], formfieldkeyPath);
console.log(subjectCosts);
This follows the path of the provided object SubjectViewModel[0]
down the specified path to the array and then iterates through the array, returning a newly mapped array of the desired values, in this case [null, 50]
(the subjectCost
values).
Example 2: more complex example
Below is a deeper example of this function in action, which makes use bracket notation. In order to support bracket notation, I added a couple of replace
statements inspired by @Alnitak's answer to a related question back in 2011:
Usage
iterPropPath(deepArray, 'entries.data->a[1].names[2]');
// -> ["Ron", "Eli", "Bre"]
Full Example
const deepArray = {
id: 0,
entries: {
randomData: true,
data: [
{ a: ['test1-1', { names: [ 'Tom', 'Kim', 'Ron' ] }], b: [['test1-2'], ['test1-3']]},
{ a: ['test2-1', { names: [ 'Jim', 'Lia', 'Eli' ] }], b: [['test2-2'], ['test2-3']]},
{ a: ['test3-1', { names: [ 'Tia', 'Jon', 'Bre' ] }], b: [['test3-2'], ['test3-3']]}
]
}
};
const iterPropPath = (obj, path) => {
path = path.replace(/\[(\w+)\]/g, '.$1').replace(/^\./, '');
const pathArray = path.split('->').map(e => e.split('.').filter(f => f)).filter(e => e && e.length);
if (pathArray.length !== 2) {
console.error('iterPropPath requires a valid object path to a nested array and a valid path within that array to the desired property value. Property path levels should be delimited by `.` and the array path/property path should be delimited by `->`.');
return false;
}
const [arrPath, propPath] = pathArray;
let arr = obj;
let arrPathValid = true;
arrPath.slice().forEach(step => arr.hasOwnProperty(step) ? (arr = arr[arrPath.shift(1)], !arrPath.length && (!Array.isArray(arr) || !arr.length) && (arrPathValid = false)) : (arrPathValid = false));
if (!arrPathValid) {
console.error(`Array path ${arrPath.join('.')} is invalid.`);
return false;
}
return arr.map((e,iPropPath) => (iPropPath = propPath.slice(), propPath.forEach(step => e && e.hasOwnProperty(step) && (e = e[iPropPath.shift(1)])), e));
};
const path = 'entries.data->a[1].names[2]';
const namesFromDeepArray = iterPropPath(deepArray, path);
console.log(namesFromDeepArray);