christianalfoni / formsy-react

A form input builder and validator for React JS
MIT License
2.6k stars 438 forks source link

Nesting forms, complex data structures #120

Closed ivome closed 8 years ago

ivome commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

@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 commented 9 years ago

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 commented 9 years ago

Thank @ntucker !

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

christianalfoni commented 9 years ago

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

ntucker commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 https://github.com/christianalfoni/formsy-react/issues/120#issuecomment-104641730 .

christianalfoni commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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

ntucker commented 9 years ago

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

christianalfoni commented 9 years ago

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

ntucker commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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

ntucker commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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.

christianalfoni commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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

chrbala commented 9 years ago

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

christianalfoni commented 9 years ago

me too,heh

ericdfields commented 9 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

@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 commented 7 years ago

@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.