Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nesting forms, complex data structures #120

Closed
ivome opened this issue May 7, 2015 · 33 comments
Closed

Nesting forms, complex data structures #120

ivome opened this issue May 7, 2015 · 33 comments

Comments

@ivome
Copy link

ivome commented May 7, 2015

I was wondering if there is a possibility of nesting forms to create complex data structures or if there is an easy way to implement that?

If the Form itself would implement the same interface as the Form element, we could nest them indefinitely and create complex hierarchical data structures with validation. Or maybe creating a special Input field is the way to go...

I tried around a little bit with creating my own Input element with sub elements, but that does not work because I don't have access to the validation rules in an external component.
For now I created a mapping function which can handle names like entity[property] and returns the value correctly, but that is not as nice, because all the nodes have to be inside the main form component. Also I cannot add validation rules and a sub form, like when I want to make a complete address required.
When trying to create reusable form partials I get the error:
Uncaught Error: Form Mixin requires component to be nested in a Form

I was thinking about something like that (simplified draft):

First create reusable form:

var AddressForm = React.createClass({
[...]
render: function(){
    return (
        <Formsy.Form {...this.props}>
             <input name="street"/>
             <input name="zip"/>
             <input name="city"/>
        </Formsy.Form>
    ); 
}
});

And then reuse that form as a field:

 <Formsy.Form>
   <input name="name"/>
   <input name="otherProperty"/>
   <AddressForm name="address"/>
</Formsy.Form>

Basically what I am trying to get from the form as a value is a complex data structure like that:

{
    name: 'Someclient',
    otherProperty: 123,
    address: {
        street: 'xyz',
        zip: 'xyz',
        city: 'xyz'
    }
}

Any thoughts on what's the best way to approach that? I think it would be a really great feature, I'd be happy to help implementing.

@kristian-puccio
Copy link

Nice! that would be very handy.

The other thing that might also work is keypaths so the input name describes the data structure.
<input name="address.street"/>

Both approaches would be very handy in different situations.

@peterpme
Copy link

peterpme commented May 8, 2015

I like this idea, but #121 and this are separate issues. I'd like to be able to pass through React components and consume React components several levels deep, but keep the data structure in tact.

@christianalfoni
Copy link
Owner

Hi guys and thanks for the discussion!

When React 0.14 is out, with the new context handling I think we could do something like this

var AddressForm = React.createClass({
[...]
render: function(){
    return (
        <Formsy.Form {...this.props}>
             <input name="street"/>
             <input name="zip"/>
             <input name="city"/>
        </Formsy.Form>
    ); 
}
});

 <Formsy.Form>
   <input name="name"/>
   <input name="otherProperty"/>
   <AddressForm name="address" extended/>
</Formsy.Form>

So the idea here is that the extended prop would be passed into the sub Formsy.Form, causing Formsy to return a plain DIV instead of a FORM. That way you could easily include forms in forms. But we depend on the context change to make the sub elements work I believe. I will do some tests tomorrow to verify if current context implementation can make things a bit easier for us.

But would this useful?

@ntucker
Copy link

ntucker commented May 22, 2015

@christianalfoni that sounds good, for now it would be nice to have the keypath as @kristian-puccio described, as that would make this usable until React 0.14. Something to go with that might also be a context manager like Handlebar.js's with, as well as path (parent) navigation with .. Of course that last thing is a nicety, but having SOME way of doing nesting is very necessary right now.

@ntucker
Copy link

ntucker commented May 22, 2015

Here's my quick hack to get nesting working that transforms . paths. It's very quick so probably needs some optimizations before inclusion. If you want I can polish it and create a PR.

    for (let k in model) {
      if (k.indexOf(".") >= 0) {
        let parts = k.split(".")
        let cur = model
        for (let part of parts.slice(0, -1)) {
          if (cur[part] === undefined) {
            cur[part] = {}
          }
          cur = cur[part]
        }
        cur[parts[parts.length-1]] = model[k]
        delete model[k]
      }
    }

@LeoIannacone
Copy link

Thank @ntucker !

@christianalfoni can you please look at it? nesting forms are really useful in many many scenario...

@christianalfoni
Copy link
Owner

Cool, let me put this in on release today. Getting to work now :-)

@ntucker
Copy link

ntucker commented May 22, 2015

FYI: trying to figure out how to work with arrays now. My sample code doesn't work with them currently, though I'm not sure this library is built for it at all given you can't hold state externally.

I don't think the disallowment of managed values is a good idea - how do I add something to a list when I can't push state down via props?

@ivome
Copy link
Author

ivome commented May 22, 2015

Here is a mapping function I wrote to support object and array values. It has a dependency on lodash atm.

Not tested much yet, but could be used as an idea:

    /**
     * Maps the input values into a complex object
     * Notation for value names:
     *
     * myobject[property]
     * myobject[test][] -> array value
     * myobject[nestedobject][otherproperty]
     *
     * @param {Array} inputs
     * @return {Object}
     */
    mapInputsToValue: function(inputs){
        var values = {};
        _.each(inputs, function(value, key){
            var currentIndex = 0;
            var objectPath = [];
            // Iterate through the whole part of the obejct
            var foundPosition = key.indexOf(']', currentIndex);

            // We have simple value
            if (foundPosition === -1){
                values[key] = value;
            } else {
                // Add complex object
                var openingBracket = key.indexOf('[');
                if (openingBracket <= 0){
                    throw new Error('Invalid name for input field: ' + key);
                } else {
                    objectPath.push(key.substring(0, openingBracket));
                }

                // We have object property
                while (foundPosition !== -1){
                    // Get current attribute name
                    var attributeName = _.last(key.substring(0, foundPosition).split('['));
                    objectPath.push(attributeName);

                    foundPosition = key.indexOf(']', foundPosition + 1);
                }

                // Get object property
                var obj = values;
                for (var i = 0; i < objectPath.length - 1; i++) {
                    var n = objectPath[i];
                    if (n in obj) {
                        obj = obj[n];
                    } else {
                        obj[n] = {};
                        obj = obj[n];
                    }
                }
                obj[objectPath[objectPath.length - 1]] = value;
            }
        });
        return values;
    }

@kristian-puccio
Copy link

There are a few node modules that do this already. Maybe they might be
useful?

https://www.npmjs.com/package/key-path

On 22 May 2015 at 21:49, ivome notifications@github.com wrote:

Here is a mapping function I wrote to support object and array values. It
has a dependency on lodash atm.

Not tested much yet, but could be used as an idea:

/**     * Maps the input values into a complex object     * Notation for value names:     *     * myobject[property]     * myobject[test][] -> array value     * myobject[nestedobject][otherproperty]     *     * @param {Array} inputs     * @return {Object}     */
mapInputsToValue: function(inputs){
    var values = {};
    _.each(inputs, function(value, key){
        var currentIndex = 0;
        var objectPath = [];
        // Iterate through the whole part of the obejct
        var foundPosition = key.indexOf(']', currentIndex);

        // We have simple value
        if (foundPosition === -1){
            values[key] = value;
        } else {
            // Add complex object
            var openingBracket = key.indexOf('[');
            if (openingBracket <= 0){
                throw new Error('Invalid name for input field: ' + key);
            } else {
                objectPath.push(key.substring(0, openingBracket));
            }

            // We have object property
            while (foundPosition !== -1){
                // Get current attribute name
                var attributeName = _.last(key.substring(0, foundPosition).split('['));
                objectPath.push(attributeName);

                foundPosition = key.indexOf(']', foundPosition + 1);
            }

            // Get object property
            var obj = values;
            for (var i = 0; i < objectPath.length - 1; i++) {
                var n = objectPath[i];
                if (n in obj) {
                    obj = obj[n];
                } else {
                    obj[n] = {};
                    obj = obj[n];
                }
            }
            obj[objectPath[objectPath.length - 1]] = value;
        }
    });
    return values;
}


Reply to this email directly or view it on GitHub
#120 (comment)
.

@christianalfoni
Copy link
Owner

Hi guys,

Are you referring to using other string values? Have now implemented:

<input name="address.street"/> => {address: { street: 'value' } }

Can not see any reason to support anything else?

@christianalfoni
Copy link
Owner

This is the code btw:

return Object.keys(this.model).reduce(function (mappedModel, key) {

  var keyArray = key.split('.');
  while (keyArray.length) {
    var currentKey = keyArray.shift();
    mappedModel[currentKey] = keyArray.length ? mappedModel[currentKey] || {} : this.model[key];
  }

  return mappedModel;

}.bind(this), {});

@christianalfoni
Copy link
Owner

Just closing it for now as any more complex implementation has to be next version. Want to get new version out today :-)

@ntucker
Copy link

ntucker commented May 22, 2015

this won't work on arrays I don't think; since model[key] will be a string

@christianalfoni
Copy link
Owner

But how could it be an array? <MyInput name=""/> is always a string?

@ntucker
Copy link

ntucker commented May 22, 2015

The structure could be an array, and y ou need to refer to that in name.
i.e.,

{nav_items: [{a:b, c:d}, {a:f, c:g}]}

PS) This doesn't seem to be called with onChange event? currentValues seesm unaltered

@christianalfoni
Copy link
Owner

Sorry, but I do not understand how the name of an input could lead to that structure? As I understand this you want:

<Formsy.Form>
  <MyInput name="foo"/>
  <MyInput name="address.street"/>
  <MyInput name="address.postCode"/>
</Formsy.Form>

Which leads to this structure:

{
  foo: 'value',
  address: {
    street: 'value',
    postCode: 'value'
  }
}

How would you structure the inputs to give a structure that would require arrays? Sorry for my misunderstanding... but its Friday after all ;-)

@ntucker
Copy link

ntucker commented May 22, 2015

That's exactly my point. This doesn't allow working with a very common use case. Even tcomb-form has built in list support ableit with a terrible UI and convoluted api.

@christianalfoni
Copy link
Owner

Please describe "list support". I do not understand what a "list" is in regards of naming an input? Would be great with an example of html syntax and how that translates to:

{nav_items: [{a:b, c:d}, {a:f, c:g}]}

What does this form look like?

@ntucker
Copy link

ntucker commented May 22, 2015

http://gcanti.github.io/resources/tcomb-form/playground/playground.html select 'lists' in the dropdown on the left

@ntucker
Copy link

ntucker commented May 22, 2015

I am currently using a + to refer to an item in an Array. aka) name="nav_items.+.a"

After further pursuit, I found my initial suggestion (below) leads to ambiguity as to where in the nested hierarchy the array lives, which led me to my current + version

Multiple inputs with the same name are supported - their extracted data will always be contained in an Array when they have some submittable data, with the exception of a group of radio buttons all having the same name, which will return the selected value only.

@chrbala
Copy link

chrbala commented May 25, 2015

I'd like to add my voice on the array issue.

The syntax could be something like this:

< MyInput name="addresses[0]" value="123 Some Street" />
< MyInput name="addresses[1]" value="456 Different Street" />

So right now that results in this kind of data:

{
    "addresses[0]": "123 Some Street",
    "addresses[1]": "456 Different Street"
}

It would be better if it added the data as an array like this:

{
    "addresses":
        ["123 Some Street", 
        "456 Different Street"]
}

And of course the primary use case for something like this would be to add a series of objects to an array with something like this:

< MyInput name={"addresses[" + index + "]"} />

It isn't the most convenient syntax to write in this case, so maybe there is a better way of writing it. But that's my idea for how this could be implemented.

Finally, dot notation could be used on the array like this:

< MyInput name="addresses[0].street" value="Some Street"/>

Which would result in an object like this:

{
    "addresses": [{street: "Some Street"}]
}

@chrbala
Copy link

chrbala commented May 26, 2015

Upon further research, it looks like the jQuery ajax function I am using to post data to the server expects associative data and parses it into a hierarchical JSON object.

The syntax is like this:

< MyInput name="addresses[0][street]" value="123 Some Street"/>
< MyInput name="addresses[1][street]" value="456 Different Street"/>

which resolves to:

{
    addresses:
        [{"street": "123 Some Street"}, 
        {"street": "456 Different Street"}]
}

Hopefully this is helpful to those trying to get hierarchical forms working with arrays - and possibly the development of this project.

nathanwelch added a commit to smashgg/formsy-react that referenced this issue Jun 2, 2015
@christianalfoni
Copy link
Owner

Hi guys,

sorry for late reply. I think it makes sense to conform the name syntax to application/x-www-form-urlencoded type of data, which is like you describe @chrbala.

Tried finding a good lib for this, but there does not seem to exist any? But we can sure build it. So does everybody feel comfortable with:

< MyInput name="addresses[0][street]" value="Some Street"/>
< MyInput name="addresses[1][street]" value="Different Street"/>

{
    addresses:
        [{"street": "123 Some Street"}, 
        {"street": "456 Different Street"}]
}

@ntucker
Copy link

ntucker commented Jun 10, 2015

Do you mean?

< MyInput name="addresses[0][street]" value="Some Street"/>
< MyInput name="addresses[1][street]" value="Different Street"/>

{
    addresses:
        [{"street": "123 Some Street"}, 
        {"street": "456 Different Street"}]
}

@christianalfoni
Copy link
Owner

Yes, sorry, of course. Copy/paste error :-)

@chrbala
Copy link

chrbala commented Jun 10, 2015

Ah, yeah, I did mean that. I edited my above post to reflect that.

@christianalfoni
Copy link
Owner

me too,heh

@ericdfields
Copy link

I'm dealing w/ the same data structure for nested objects (rails api). I built a Formsy form that handles formatting of the data quite well. The following is not pretty, nor well-tested, but it handles nested objects (at least one-level deep) just fine, and minimizes the amount of boilerplate you have to write.

My lib:

var React = require('react')
var Formsy = require('formsy-react');

export class FormFor extends React.Component {

  constructor(props) {
    super(props)
    this.formatChild = this.formatChild.bind(this)
  }

  formatChild(child) {
    if (typeof child == 'string') {
      return child
    }

    let entityName = this.props.entity

    if (child.props.fields_for) {
      child.props.entity = this.props.entity
      return child
    }
    if (!_.isEmpty(child.props.name)) {
      child.props.field_name = child.props.name
      if (child.props.name.match(/[\d*]/g)) {
        child.props.name = [entityName, child.props.name].join('')
      } else {
        child.props.name = [entityName, '[', child.props.name, ']'].join('')
      }
    }
    if (child.props.children) {
      child.props.children = React.Children.map(child.props.children, this.formatChild)
    }
    return child
  }

  render() {
    let children = React.Children.map( this.props.children, this.formatChild )
    return (
      <Formsy.Form {...this.props}>
        { children }
      </Formsy.Form>
    )
  }

}

And then in your render method:

        <FormFor 
          entity="patient" 
          onValidSubmit={this.submit.bind(this)} 
          onValid={this.enableButton.bind(this)} 
          onInvalid={this.disableButton.bind(this)}>
                  <TextInput
                    wrapperClass="col-md-3"
                    name="first_name"  
                    label="First name:" 
                    value={ this.props.patient.first_name } 
                    required />
     </FormFor>

A nested object component might look like so:

import { attributesPrefixer } from 'helpers/forms_helper'
var HiddenInput = require('components/ui/forms/hidden_input_component')
var TextInput = require('components/ui/forms/text_input_component')
var CheckboxInput = require('components/ui/forms/checkbox_input_component')
var classNames = require('classnames')

function patientPhoneNumberFieldsTemplate(field_group,i) {

  function handleRemove(i,event) {
    event.preventDefault()
    Vanda.flux.getActions('CaseManagerPatient').removePhoneNumber(i)
  }

  function prefixer(index,name) {
    return attributesPrefixer('phone_numbers', index, name)
  }

  let classes = classNames({
    'row': true,
    'inset-row': true,
    'hide': field_group.mark_for_destroy
  })

  return (
    <div className={classes} key={i}>
      <TextInput
        wrapperClass="col-md-3"
        name={ prefixer(i,'title') }
        label="Title:"
        placeholder="Mobile, Work, etc."
        value={ field_group.title } />

      <TextInput
        wrapperClass="col-md-3"
        name={ prefixer(i,'value') } 
        label="Number:" 
        value={ field_group.value } />

      <CheckboxInput
        wrapperClass="col-md-3"
        name={ prefixer(i, 'is_primary') }
        label="Primary number:"
        value={ field_group.is_primary } />

      <HiddenInput
        name={ prefixer(i, 'id') }
        value={ field_group.id } />

      <button className="btn btn-danger" onClick={ handleRemove.bind(null,i) }>Remove</button>
    </div>
  )
}


export function patientPhoneNumberFields(fields) {
  let fields = fields.map( patientPhoneNumberFieldsTemplate )

  return (
    { fields }
  )
}

And that super-handy attributesPrefixer up there looks like this:

  attributesPrefixer(attributes_name,index,name) {
    return '[' + attributes_name + '_attributes][' + index + '][' + name + ']'
  },

THE PAYOFF

You get the naming convention you need to submit the form:

<input name="patient[first_name]" type="text" class="form-control" value="Testing" data-reactid=".0.$=11:0.0.0.3.1.$=1$=010=20:0.$=1$=011=20:0.$=1$=010=20:0.1:0">

Some nested inputs, out of context:

<input name="patient[addresses_attributes][0][id]" type="hidden" value="81822" data-reactid=".0.$=11:0.0.0.3.1.$=1$=010=20:0.$=1$=015=20:0.$=1$=011=02$fields=020=02$0=2$0:$0.$=1$=011=20:0">

<input name="patient[addresses_attributes][0][title]" type="text" class="form-control" placeholder="Home, Work, etc." value="23" data-reactid=".0.$=11:0.0.0.3.1.$=1$=010=20:0.$=1$=015=20:0.$=1$=011=02$fields=020=02$0=2$0:$0.$=1$=012=20:0.$=1$=010=20:0.1:0">

<input name="patient[addresses_attributes][0][city]" type="text" class="form-control" value="mala" data-reactid=".0.$=11:0.0.0.3.1.$=1$=010=20:0.$=1$=015=20:0.$=1$=011=02$fields=020=02$0=2$0:$0.$=1$=014=20:0.$=1$=010=20:0.1:0">

Etc.

Again, suits my needs, is not a solution, but maybe this helps. Happy to answer questions.

@christianalfoni
Copy link
Owner

Okay, this is now implemented, using the separate project: https://github.com/christianalfoni/form-data-to-object. Using it as a hard dependency as it is unlikely you need it for something else... will be part of next release

@dsteinbach
Copy link

Sorry, I am not sure if this was addressed in this issue but can I now do something like this to take advantage of "sub-validation"?

var AddressForm = React.createClass({
   [...]
   addValidAddress:function(){
      this.setValue(this.model());
   }
   render: function(){
      return (
          <Formsy.Form {...this.props} onValidSubmit={::this.addValidAddress}>
               <input name="street"/>
               <input name="zip"/>
               <input name="city"/>
          </Formsy.Form>
      ); 
   }
});
 <Formsy.Form>
   <input name="name"/>
   <input name="otherProperty"/>
   <AddressForm name="address"/>
</Formsy.Form>

@christianalfoni mentioned this would be available once react@0.14 came out

@Semigradsky
Copy link
Collaborator

@dsteinbach you can not nesting forms:
http://www.w3.org/TR/2011/WD-html5-20110525/forms.html#the-form-element

Content model:
Flow content, but with no form element descendants.

@klis87
Copy link

klis87 commented Jul 3, 2017

@Semigradsky Nested form could be displayed as div, then we could check whether part of a form is valid or not - this functionality is present in many form libraries, including Angular and Redux Form and it is implemented without any violation of HTML5 spec.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests