Forum: Rails Spinoffs (closed, excessive spam) Prototype - Form To Json - A new method with hierach

Posted by TAOS (Guest)
on 2008-06-11 20:01
(Received via mailing list)
I try to use the prototype form method called serializeElements, to
get all elements in form and build a JSON var.
It work, but not in the best way.
It method get all fields in a form and build a JSON, but it dont put
JSON inside JSON.
Example:

If i have the follow form:

input name="person.name"
input name="person.age"
input name="person.address.street"

The serializeElements's method build a JSON like this

{ "person.name": "??", "person.age": "??", "person.address.street":
"??" }

With my modified method the JSON will be generated like this

{ name: "??", age: "??", address: { street: "??" } }
Or
{ car: { name: "??", age: "??", address: { street: "??" } } }

What are you think about this method?

Can i add this method in prototype framework? How?

/**
 * Serialize a form into a JSON var
 *
 * How to use:
 *
 * var json = formToJSON('form_test', { prefix : 'car.', exclude:
['description'] } );
 *
 * <form id="form_test">
 *   <input type="text" name="car.id" value="2" />
 *  <input type="text" name="car.name" value="Z5" />
 *  <input type="text" name="car.description" value="Sport car" />
 *  <select name="car.company.id">
 *    <option value="1" selected="selected">BMW</value>
 *    <option value="2">Mercedes</value>
 *   </select>
 *  <input type="checkbox" name="car.optionals" value="carsound"
checked="checked">Car Sound
 *  <input type="checkbox" name="car.optionals" value="gps">GPS
 *  <input type="checkbox" name="car.optionals" value="absbreak"
checked="checked">ABS Break
 * </form>
 *
 * The example above will generate the folow JSON.
 *
 * { id: '2', name: 'Z5', company: { id: 1 }, optionals: ['carsound',
'absbreak'] }
 */
function formToJSON(form, options) {
  elements = $(form).getElements();
  if (typeof options != 'object') options = { hash: !!options };
  else if (Object.isUndefined(options.hash)) options.hash = true;
  var key, value, submitted = false, submit = options.submit;

  var data = elements.inject({ }, function(result, element) {
    if (!element.disabled && element.name) {
        key = element.name;
        value = $(element).getValue();
        if (value != null && (element.type != 'submit' || (!submitted &&
            submit !== false && (!submit || key == submit) && (submitted
= true)))) {

          key = ( !Object.isUndefined(options.prefix) ) ?
key.replace(options.prefix,"") : key;

          if( Object.isArray(options.exclude) )
            if( options.exclude.indexOf(key) != -1 )
              return result;

              if( key.indexOf(".") != -1 ) {
                var processkey = key;
                var tree = "";
                while( processkey.indexOf(".") != -1 ) {
                  var newkey = processkey.substring( 0,
processkey.indexOf(".") );
                  processkey = processkey.replace( newkey + ".", "" );

                  tree += (tree == "") ? newkey : "." + newkey;

                  if( eval("result." + tree) == undefined ||
eval("result." + tree) == null )
                    eval("result." + tree + " = new Object()") ;

            if( processkey.indexOf(".") == -1 )
              if( processkey in eval( "result." + tree ) ) {
                if ( !Object.isArray( eval( "result." + tree + "." +
processkey ) ) )
                  eval("result." + tree + "." + processkey + " = [ 
result." +
tree + "." + processkey + "]");

                eval("result." + tree + "." + processkey +
".push(value)");
              } else
                      eval("result." + tree + "." + processkey + " =
value");
                }
              } else
                   if (key in result) {
                    if ( !Object.isArray( result[key] ) )
                      result[key] = [ result[key] ];

                result[key].push(value);
              } else
            result[key] = value;
          }
    }

    return result;
  });
  return options.hash ? data : Object.toQueryString(data);
};


thx,
Thiago Antonius
Posted by TAOS (Guest)
on 2008-06-11 20:04
(Received via mailing list)
The code in pastebin for better view.

http://www.pastebin.us/?show=f247e7a5a
Posted by Frederick Polgardy (Guest)
on 2008-06-11 20:22
(Received via mailing list)
Wow, first of all, lose the eval()'s.

You don't need to dynamically construct a call to eval() to resolve a
property.  Instead of:

eval("someObject." + someProperty)

You should say:

someObject[someProperty]

And this is, of course, chainable:

a[property1][property2]

-Fred

On Wed, Jun 11, 2008 at 1:01 PM, TAOS <thiagoaos@gmail.com> wrote:

> input name="person.name"
> { name: "??", age: "??", address: { street: "??" } }
>  * How to use:
>  *       <option value="2">Mercedes</value>
>  * { id: '2', name: 'Z5', company: { id: 1 }, optionals: ['carsound',
>                key = element.name;
>                        if( Object.isArray(options.exclude) )
>                                        processkey = processkey.replace(
>
>
>                        if ( !Object.isArray( result[key] ) )
>        return options.hash ? data : Object.toQueryString(data);
> };
>
>
> thx,
> Thiago Antonius
> >
>


--
Science answers questions; philosophy questions answers.
Posted by kangax (Guest)
on 2008-06-11 21:12
(Received via mailing list)
This looks like an edge case to me ;)
Prototype.js can accomplish similar conversion quite easily:

Object.toJSON($('someForm').serialize());

Besides that, I don't see a reason to duplicate half of Form#serialize
in here. Why not simply iterate over a resulting object?

- kangax
Posted by TAOS (Guest)
on 2008-06-11 21:49
(Received via mailing list)
How can i lose the eval in this case.

The tree's var is a hierach of the iterable key. like
person.address.phone.something ...
I can't do this someObject['person.address.phone.something'], then i
need to use eval
Posted by TAOS (Guest)
on 2008-06-11 21:56
(Received via mailing list)
kangax,

why i will iterate something twice? if in a iterate i can do two
things?

Is more easy i use a prefix in my adpted method to works like this $
('form_test').serialize()

of course the method need get better the code, but its is more useful
then the original.


sorry to my bad english.
Posted by TAOS (Guest)
on 2008-06-11 22:05
(Received via mailing list)
Sorry, i am refering a attribute options var instead prefix.

Is more easy i use a prefix in my adpted method to works like this $
('form_test').serialize()
Posted by kangax (Guest)
on 2008-06-11 22:23
(Received via mailing list)
In this case, iterating twice simply removes duplication. The method
itself is barely performance "critical" (few item collections with not
too "heavy" calculations). Not repeating yourself is, imo, critical.
As far as "eval" goes, you seem to be confused about property access:

eval("result." + tree + "." + processkey + ".push(value)");
// is "same" as
result[tree][processkey].push(value);

- kangax
Posted by TAOS (Guest)
on 2008-06-11 23:05
(Received via mailing list)
Is not same.

i need to break in each point.

someObject['person.address.phone.something']['property'] = 5 will
generate

{ 'person.address.phone.something': { 'property': 5 } }

using eval will generate

{ person: { address: { phone: { something: { property: 5 } } } } }

this is the diference.
Posted by TAOS (Guest)
on 2008-06-11 23:22
(Received via mailing list)
I too dislike repeat code. Then i am purpose improve this method code
and replace the old method.
Is easy do whit this adpted method do too what the old do.
Posted by Frederick Polgardy (Guest)
on 2008-06-11 23:25
(Received via mailing list)
I think I understand what you're trying to do - you want to take form
elements in the form of "a.b.c.d" and generate the correct nested JSON
object.  Since I have a soft spot for recursive algorithms, I'll 
probably
try to implement this on the way home on the train. ;-)  But while the
underlying structure will be different, there still isn't a good reason 
to
use eval().  Not just because it's slower, but because it's a lot harder 
to
comprehend.  All the expressions you're trying to build up and eval() 
can be
expressed with bracket notation and, possibly, string concatenation to 
build
up property strings that refer to form elements.

I'll play with this a little tonight.

-Fred

On Wed, Jun 11, 2008 at 4:04 PM, TAOS <thiagoaos@gmail.com> wrote:

> using eval will generate
> >
> > > why i will iterate something twice? if in a iterate i can do two
> > > On 11 jun, 16:12, kangax <kan...@gmail.com> wrote:
> > > > - kangax
> >
> > > > > "??" }
> >
> > > > >  *  <input type="text" name="car.name" value="Z5" />
> > > > >  * </form>
> };
> > > > >                         submit !== false && (!submit || key ==
> > > > >                                         return result;
> processkey.replace( newkey + ".", "" );
> > > > >                                                 if(
> > > > >
> > > > >                                 result[key] = [ result[key] ];
> > > > >         return options.hash ? data : Object.toQueryString(data);
> >
> > > > > };
> >
> > > > > thx,
> > > > > Thiago Antonius
> >
>


--
Science answers questions; philosophy questions answers.
Posted by kangax (Guest)
on 2008-06-11 23:43
(Received via mailing list)
Fair enough, but still makes little sense to use eval:

function toObject(str) {
  var result = { };
  str.split('.').inject(result, function(parent, child) {
    return parent[child] = parent[child] || { };
  });
  return result;
};

toObject('foo.bar.baz'); // { foo: { bar: baz: { }}}

- kangax
Posted by Frederick Polgardy (Guest)
on 2008-06-12 03:48
(Received via mailing list)
Right, all the uses of eval were unnecessary.

I came up with a quick utility along these lines, that allows you to 
pass in
an object like:

{"a.b.c": 1, "a.b.d":2, "a.b.e[0].f": 3, "a.b.e[1].g": 4}

Which you might get from a Prototype form utility function, and get back 
an
exploded object like:

{"a": {"b": {"c":1, "d":2, "e": [{"f": 3}, {"g": 4}]}}}

Which is suitable for passing to Object.toJSON().  It basically just 
handles
intermediate objects that can be ordinary values or arrays, but that 
handles
all the cases I can think of.  Let me know what you think.

  function to_exploded_object(object) {
    var root = {};

    $H(object).each(function(property) {
      var current = root,
          path = property.key.split("."),
          last = path.pop();

      function set_and_advance_key(key, value) {
        var match = key.match(/^(\w+)(?:\[(\d+)\])?/),
            name = match[1],
            index = match[2];

        if (index) {
          index = parseInt(index);
          current[name] = current[name] || [];
          current[name][index] = current[name][index] || value;
          current = current[name][index];
        } else {
          current[name] = current[name] || value;
          current = current[name];
        }
      }

      path.each(function(key) { set_and_advance_key(key, {}); });
      set_and_advance_key(last, property.value);
    });

    return root;
  }

-Fred

On Wed, Jun 11, 2008 at 4:43 PM, kangax <kangax@gmail.com> wrote:

>
> toObject('foo.bar.baz'); // { foo: { bar: baz: { }}}


--
Science answers questions; philosophy questions answers.
Posted by TAOS (Guest)
on 2008-06-12 14:44
(Received via mailing list)
How to use this?

json = to_exploded_object( $('form_test').serialize() );

or

json = to_exploded_object( Object.toJSON( $
('form_test').serialize() ) );

Look the result in both examples, this don't work like i expected, but
i get the ideia to don't use eval. thx both.

{  0:""",   1:"c",   2:"o",   3:"n",   4:"c",   5:"e",   6:"s",
7:"s",   8:"i",   9:"o",   10:"n",   11:"a",   12:"r",   13:"i",
14:"a",   15:".",   16:"c",   17:"a",   18:"r",   19:"r",
  20:"o",   21:".",   22:"m",   23:"a",   24:"r",   25:"c",
26:"a",   27:".",   28:"c",   29:"o",   30:"d",   31:"i",   32:"g",
33:"o",   34:"=",   35:"2",   36:"0",   37:"&",
  38:"c",   39:"o",   40:"n",   41:"c",   42:"e",   43:"s",
44:"s",   45:"i",  ....
Posted by TAOS (Guest)
on 2008-06-12 14:49
(Received via mailing list)
This don't work too

var json = toObject( $('form_test').serialize() );

this result undefined in the second object in hirarch.

I will adapted my method, leave eval, and make with that do what
prototype method do in default, and with option do more.
Posted by Frederick Polgardy (Guest)
on 2008-06-12 15:13
(Received via mailing list)
You passed the serialized string into the function, which won't work.
(It'll iterate through the characters, as you can see.)

There isn't a Prototype function that collects a form's values into an
object, but it's pretty simple to write one:

function to_object(form) {
  return $(form).getElements().inject({}, function(object, element) {
    o[e.name] = $F(e);
    return o;
  });
}

This doesn't handle multiple valued elements like checkboxes and
multiselects, but it gives the basic idea.  You'd then pass the result 
of
that to to_exploded_object() to handle the rest:

var object = to_exploded_object(to_object('form_test'));

I might refactor this a bit into a single function.  I think it has a 
pretty
important use-case: where you want to submit a JSON request directly 
from a
form in HTML/JS without going through a server-side framework that 
handles
this unpacking for you.

-Fred

On Thu, Jun 12, 2008 at 7:43 AM, TAOS <thiagoaos@gmail.com> wrote:

> Look the result in both examples, this don't work like i expected, but
>
> >
> > all the cases I can think of.  Let me know what you think.
> >         var match = key.match(/^(\w+)(?:\[(\d+)\])?/),
> >           current = current[name];
> > -Fred
> > >  return result;
> > > };
> >
> > > toObject('foo.bar.baz'); // { foo: { bar: baz: { }}}
> >
> > --
> > Science answers questions; philosophy questions answers.
> >
>


--
Science answers questions; philosophy questions answers.
Posted by Frederick Polgardy (Guest)
on 2008-06-12 15:36
(Received via mailing list)
Here's an updated version of my to_exploded_object function, that takes 
a
form instead of an object, so you can pass the form in in one shot.  It
still doesn't handle multivalued inputs, but it does handle arrays.

  function toJSONObject(form) {
    var object = {};

    Form.getElements(form).each(function(element) {
      var current = object,
          path = element.name.split("."),
          last = path.pop();

      function advanceToNextNode(key, value) {
        var match = key.match(/^(\w+)(?:\[(\d+)\])?/),
            name = match[1],
            index = match[2];

        if (index) {
          index = parseInt(index);
          current[name] = current[name] || [];
          current[name][index] = current[name][index] || value;
          current = current[name][index];
        } else {
          current[name] = current[name] || value;
          current = current[name];
        }
      }

      path.each(function(key) { advanceToNextNode(key, {}); });
      advanceToNextNode(last, Form.Element.getValue(element));
    });

    return object;
  }

  Object.toJSON(toJSONObject('form_test'));

(The function still returns an object, not a JSON string, because there 
are
a lot of use-cases for keeping it in that form as well.)

-Fred

--
Science answers questions; philosophy questions answers.
Posted by TAOS (Guest)
on 2008-06-12 15:52
(Received via mailing list)
Now i fix my method.

It work like a prototype serialize method without options and with ths
option hierarchy = true work like my way, broke in each point.

examples with this form:

Look here http://pastebin.com/f41ba8348 to view the code with better
look.

<form id="form_person">
<input name="person.name" value="Thiago"/>
<input name="person.age" value="23"/>
<input name="person.address.street" value="Marketplace Street" />
<select name="person.country.id">
  <option value="BR">BR</option>
  <option value="EUA">EUA</option>
</select>
<input type="checkbox" name="person.intersting" value="Prototype"
checked="checked"/>
<input type="checkbox" name="person.intersting" value="Ruby"
checked="checked"/>
<input type="checkbox" name="person.intersting" value="Java" />
</form>

formToJSON('form_person');

"person.name=Thiago&person.age=23&person.address.street=Marketplace
%20Street&person.country.id=BR&person.intersting=Prototype&person.intersting=Ruby"

formToJSON('form_person', {});

{
  person.name:"Thiago",
  person.age:"23",
  person.address.street:"Marketplace Street",
  person.country.id:"BR",
  person.intersting:["Prototype", "Ruby"]
}

formToJSON('form_person', { hierarchy: true, prefix : 'person.',
exclude: ['intersting'] } );

{
  name:"Thiago",
  age:"23",
  address:{street:"Marketplac..."},
  country:{id:"BR"}
}


And finally the method. No repeat code, and no eval


function formToJSON(form, options) {
  elements = $(form).getElements();
  if (typeof options != 'object') options = { hash: !!options };
  else if (Object.isUndefined(options.hash)) options.hash = true;
  var key, value, submitted = false, submit = options.submit;

  var data = elements.inject({ }, function(result, element) {
    if (!element.disabled && element.name) {
        key = element.name;
        value = $(element).getValue();
        if (value != null && (element.type != 'submit' || (!submitted &&
            submit !== false && (!submit || key == submit) && (submitted
= true)))) {

          function set_key(key, obj, value) {
          if (key in obj) {
                    if ( !Object.isArray( obj[key] ) ) obj[key] =
[ obj[key] ];
                    obj[key].push(value);
              } else   obj[key] = value;
          return obj;
          }

          function is_exclude_key(key) {
            if( Object.isArray(options.exclude) &&
options.exclude.indexOf(key) != -1 ) return true;
            return false;
          }

          if( !Object.isUndefined(options.hierarchy) &&
options.hierarchy ) {
            key = ( !Object.isUndefined(options.prefix) ) ?
key.replace(options.prefix,"") : key;
            if( is_exclude_key(key) ) return result;
                if( key.indexOf(".") != -1 ) {
                  var newkey = key.substring( 0,
key.indexOf(".") );
                  key = key.replace( newkey + ".", "" );

                  function set_and_advance_key(processkey, obj, value) {
                    if( processkey.indexOf(".") != -1 ) {
                      var newkey = processkey.substring( 0,
processkey.indexOf(".") );
                      processkey = processkey.replace( newkey + ".", "" 
);
                      obj[newkey] = set_and_advance_key( processkey, {},
value )
              }  else obj = set_key(processkey, obj, value);
                    return obj;
                  }
                  result[newkey] = set_and_advance_key(key, {}, value);
                } else result = set_key(key, result, value);
              } else if( !is_exclude_key(key) ) result = set_key(key,
result, value);
      }
    }
    return result;
  });
  return options.hash ? data : Object.toQueryString(data);
};


and now, with yout tips i think that the method is better.
How can i get more better yet?
Posted by Tobie Langel (tobie)
on 2008-06-12 21:06
(Received via mailing list)
Find don't you just define a toJSON method for forms and form
elements.

Prototype's implementation was designed with that in mind.

See http://prototypejs.org/learn/json for details. (About midways)

Best,

Tobie
Posted by TAOS (Guest)
on 2008-06-12 22:25
(Received via mailing list)
I don't understand what you want to tell me.

the toJSON method is diferent that the method that i do.

Did you refer about the method's name? formToObject is better?
Posted by kangax (Guest)
on 2008-06-12 22:51
(Received via mailing list)
Tobie suggests that extending form elements with such method would
make more sense in your case:

Element.addMethods('form', {
  toJSON: function(element) {
    // your logic here
  }
});
...
Object.toJSON($('someForm'));
// or:
$('someForm').toJSON();

- kangax
Posted by TAOS (Guest)
on 2008-06-12 23:03
(Received via mailing list)
Ok, i will extend form element, but first i need to be if the method
now is better and make sense.
Posted by TAOS (Guest)
on 2008-06-12 23:11
(Received via mailing list)
Ok, i will extend form element, but first i need to be if the method
now is better and make sense.
This topic is locked and can not be replied to.