views:

77

answers:

3

I am required (please therefore no nit-picking the requirement, I've already nit-picked it, and this is the req) to convert certain form fields that have "object nesting" embedded in the field names, back to the object(s) themselves. Below are some typical form field names:

  • phones_0_patientPhoneTypeId
  • phones_0_phone
  • phones_1_patientPhoneTypeId
  • phones_1_phone

The form fields above were derived from an object such as the one toward the bottom (see "Data"), and that is the format of the object I need to reassemble. It can be assumed that any form field with a name that contains the underscore _ character needs to undergo this conversion. Also that the segment of the form field between underscores, if numeric, signifies a Javascript array, otherwise an object.

I found it easy to devise a (somewhat naive) implementation for the "flattening" of the original object for use by the form, but am struggling going in the other direction; below the object/data below I'm pasting my current attempt. One problem (perhaps the only one?) with it is that it does not currently properly account for array indexes, but this might be tricky because the object will subsequently be encoded as JSON, which will not account for sparse arrays. So if "phones_1" exists, but "phones_0" does not, I would nevertheless like to ensure that a slot exists for phones[0] even if that value is null.

Implementations that tweak what I have begun, or are entirely different, encouraged. If interested let me know if you'd like to see my code for the "flattening" part that is working. Thanks in advance

Data:

var obj = {
phones: [{
    "patientPhoneTypeId": 4,
    "phone": "8005551212"
},
{
    "patientPhoneTypeId": 2,
    "phone": "8885551212"
}]};

Code to date:

    var unflattened = {};

    for (var prop in values) {
        if (prop.indexOf('_') > -1) {
            var lastUnderbarPos = prop.lastIndexOf('_');

            var nestedProp = prop.substr(lastUnderbarPos + 1);
            var nesting = prop.substr(0, lastUnderbarPos).split("_");

            var nestedRef, isArray, isObject;

            for (var i=0, n=nesting.length; i<n; i++) {
                if (i===0) {
                    nestedRef = unflattened;
                }

                if (i < (n-1)) { // not last
                    if (/^\d+$/.test(nesting[i+1])) {
                        isArray = true;
                        isObject = false;
                    }
                    else {
                        isArray = true;
                        isObject = false;
                    }

                    var currProp = nesting[i];

                    if (!nestedRef[currProp]) {
                        if (isArray) {
                            nestedRef[currProp] = [];
                        }
                        else if (isObject) {
                            nestedRef[currProp] = {};
                        }
                    }

                    nestedRef = nestedRef[currProp];
                }
                else {
                    nestedRef[nestedProp] = values[prop];
                }
            }
        }
+1  A: 

I would change the form field names to be a bit more straightforward in implying what is an array, and what is an object using this structure:

property.<remaining>     => looking at "property" object
property[i].<remaining>  => looking at "property" array with index "i"

Using this information, it becomes easier to regenerate the array without too much string and index manipulation. Assuming we have an empty root object to begin with, you can add a method called set to it that just takes the property string as it is, and constructs the object no matter how deeply nested it is.

var object = {};

object.set = function(key, value) {
    var parts = key.split(".");
    var current = this; // begin at root
    var arrayRegex = /(\w+)\[(\d+)\]/, nameAndIndex, name, index;

    while(parts.length) {
        part = parts.shift();
        nameAndIndex = part.match(arrayRegex);
        // property is an array
        if(nameAndIndex != null) {
            name = nameAndIndex[1];
            index = nameAndIndex[2];
            current[name] = current[name] || [];
            current[name][index] = current[name][index] || {};
            current = current[name][index];
        }
        // property is an object
        else {
            current[part] = current[part] || {};
            current = current[part]; // edit: was missing this line before
        }
    }

    current[part] = value;
};

This can handle deeply nested structures but has a caveat in that, you must have an object at each level. For example, you can't do someProperty[1][2] but you can do someProperty[1].anotherProperty[2]. Also the last item must be an object property instead of an array element, meaning you can't do x.y.z.lastProperty[1], but you can do x.y.z.anArray[1].lastProperty. If the above cases do not work, then you can change the set method and add additional checks.

Created an example on http://jsfiddle.net/ysQpF/ with these test cases:

object.set('phones[0].phoneType', 1);
object.set('phones[0].phone', '312-223-4929');
object.set('phones[0].details[0].areaCode', '312');
object.set('phones[0].details[0].region[0].name', 'Alaska');
object.set('phones[0].details[0].region[1].name', 'California');
object.set('phones[1].phoneType', 2);
object.set('phones[1].phone', '408-332-3011');
Anurag
A: 

For each form field:

Eg. phones_0_patientPhoneTypeId

  1. Split the name on _ to get an array: ["phones","0","patientPhoneTypeId"]
  2. reverse() that array: ["patientPhoneTypeId","0","phones"]
  3. Put a template object in a variable, I'll use o
  4. pop() the array until it's empty and for each thing except the last one do o = o[resultOfPop]
  5. For the last one, just set the value such as o[resultOfPop] = valueFromField

If you don't have/want to use a template object you'll need to add an empty object or array in step 4.

svinto
A significant departure from my approach but I hope to try it and see how it might work as it sounds interesting and elegant
George Jempty
A: 

Thanks for the other answers. Here's how I tweaked the code I already had, I knew I was close.

    var unflattened = {};

    for (var prop in values) {
        if (prop.indexOf('_') > -1) {
            var nesting = prop.split("_");
            var nestedProp = nesting.pop();

            if (!nesting.length || !nestedProp) {
                console.log("form field name: '" + origProp + "' cannot be converted to 'nested' object");
                continue;
            }

            var nestedRef, currProp, isArray, isObject;

            for (var i=0, n=nesting.length; i<n; i++) {                 
                if (i === n-1) { //last
                    isArray = /^\d+$/.test(nestedProp);
                }
                else {
                    isArray = /^\d+$/.test(nesting[i+1]);
                }

                isObject = !isArray;
                currProp = nesting[i];

                if (i===0) {
                    nestedRef = unflattened;
                }

                if (!nestedRef[currProp]) {
                    if (isArray) {
                        nestedRef[currProp] = [];
                    }
                    else {
                        nestedRef[currProp] = {};
                    }
                }

                nestedRef = nestedRef[currProp];

                if (i === n-1) { //last
                    nestedRef[nestedProp] = values[prop];
                }
            }
        }
    }
George Jempty