jonjamz / blaze-forms

Dead easy reactive forms with validation (Meteor).
https://atmospherejs.com/templates/forms
MIT License
113 stars 11 forks source link

Add more extensive examples to the docs, like how to deal with arrays and objects #48

Open jcheroske opened 9 years ago

jcheroske commented 9 years ago

Can you talk about what functionality exists in the library to support properties that are arrays or objects? I'm just working on a simple contact form as a learning exercise, but it features the ability to add multiple emails or phone numbers. Here is the schema:

new SimpleSchema({

   name: {
      type: String,
      label: 'Name',
      max: 40
   },
   emails: {
      type: [String],
      regEx: SimpleSchema.RegEx.Email,
      max: 40
   },
   phones: {
      type: [String],
      max: 20
   },
   description: {
      type: String,
      label: 'Description',
      max: 1000
   }

})

Any suggestions as to how I would use Forms to support this?

jonjamz commented 9 years ago

Of course!

You could create a custom element called multiString and use the validationValue function when you call createElement to do something like this--although I haven't tested it:

<template name="elements__multiString">
  <div class="reactive-element">
    {{#each value}}
      <input type="text" value={{.}}>
    {{/each}}
  </div>
</template>

The above is a bit naive in assuming that you'll be passing initial data with an array of strings for the field you want to use (like email). So to get 2 inputs, you'd pass in ['',''] if there wasn't an existing value.

We're delegating the keyup event to a containing div element so that we can capture events from any of the inputs inside.

ReactiveForms.createElement({
  template: 'elements__multiString',
  validationEvent: 'keyup',
  validationValue: function (el, clean, template) {
    var values = $(el).find('input').map(function () {
      return $(this).val();
    });
    return values; // An array with all your input values
  }
})

From there, SimpleSchema validation should work on the array of strings automatically.

Hope this helps, definitely share any cool things you come up with!

jcheroske commented 9 years ago

Thanks for the quick reply! I will meditate on your solution and see if I can get it to work.

Your library, imho, has good smell. Much better than autoForm, which seems to have gone down a certain rabbit hole. I've got some Angular experience, and I've been kind of shocked that there isn't a full featured form library for Meteor. Yours is the best I've seen so far, and I would love to see you continue to add to it. Have you looked at the Angular form docs? You're not that far away from what they have, and your approach is similar. If you duplicated much of the functionality they have there, but in the much nicer and more sane Meteor way of doing things, you would be a hero.

Is there an object that the form is bound to, such that, if I modify that object then the content of the form's fields changes? I know, for updates, you can use a data object to populate the form. Does that object get bound to the form then? What about an insert form with no data object? Is there an object somewhere that represents the form data?

jonjamz commented 9 years ago

Thank you for the kind words. I definitely intend to do my best with this package--I'm using it myself in all kinds of ways, and constantly thinking about how to improve it as well.

The form-level data object is privately scoped, and the only way to change it manually is:

The action function is bound to the form's data context here:

https://github.com/meteortemplates/forms/blob/master/lib/module.coffee#L207

For version 2.0.0 of this package, I'll introduce named contexts as a possibility, so you'll be able to access form data contexts from the outside--allowing you to persist form state across template renderings and create widgets in other parts of the page (for example, to use the forms package to create a shopping cart).

jcheroske commented 9 years ago

Having access to the form's backing object, and having that object be reactively tied to the form is incredibly powerful. For example, in an Angular template, you can loop over an array property and build up some form fields. In your code, if you simply add another element to the array, boom, you get another set of fields. Makes dealing with nested properties really easy. Perhaps a block template that allows looping over a property could be useful?

I know I mention Angular a lot, but I'm not really a fan of the library. I just think that, when it came to forms, they got the concepts right and that it should be used as a reference. Trying to do something in Angular still makes my head want to explode.

jonjamz commented 9 years ago

Haha that's why I never really got into it. Seemed like there were too many ways to do the same thing and a lot of boilerplate and jargon for everything.

In templates:forms it's possible to control an element's state in several ways. You can easily add custom events and helpers to an element (since it's just a Blaze template, and Blaze allows stacking multiple calls to helpers and events). And you can add your own reactive state to any element using the created callback from within createElement:


// Add helpers for the custom state
Template['inputElement'].helpers({
  numberOfFields: function () {
    var currentFieldCount = Template.instance().numberOfFields.get();
    var times = [];
    _.times(currentFieldCount, function (n) {
      times.push(n);
    });
    return times;
  },
  getValueFor: function (n, values) {
    if (n && values && values[n]) {
      return values[n];
    }
  }
});

// Add event to change custom state
Tempate['inputElement'].events({
  'click .add-field': function (event, template) {
    var currentFieldCount = Template.instance().numberOfFields.get();
    Template.instance().numberOfFields.set(currentFieldCount++);
  }
});

// Add ReactiveVar here using the `created` callback
ReactiveForms.createElement({
  template: 'inputElement',
  validationEvent: 'keyup',
  validationValue: function (el, clean, template) {
    var values = $(el).find('input').map(function () {
      return $(this).val();
    });
    return values; // An array with all your input values
  },
  created: function () {
    this.numberOfFields = new ReactiveVar(1); // Default to one field
  }
});

Then in the element's template:

<template name="inputElement">
  <div class="reactive-element">
    {{#each numberOfFields}}
      <input class="reactive-element" value={{getValueFor . ../value}}>
    {{/each}}
  </div>
  <button class="add-field">Add another field</button>
</template>

The created callback is run after the element is set up by templates:forms, so you have access to any reactive data that was placed in the element and can perform any custom initialization you like.

Try to avoid thinking of templates:forms in the context of two-way data binding. Using a form block, the trip back to the original data object never gets completed unless you manually handle it that way in your action function--although that would be an odd way to use the package. The form block's internal data context is meant to be stored until the action function is run, and the only reason you should try to manually access that context is to read from it, not write to it. The writing should happen via elements.

One of the cool things about this package is that you can create pretty much any crazy element you want and the API to do it just uses regular Blaze. I think the key to maintaining that feel is careful decoupling like what exists currently.

If the form block's data context was named, and that named context was reactive and writable from anywhere, it might simplify things in one case but complicate them in another. I don't really know at this point, but it's worth visiting for 2.0.0.

jcheroske commented 9 years ago

Wow! Thanks for the long reply. I'm new to Meteor and Blaze, and am still learning the right way of doing things. Hopefully in time it'll become more natural to me. I'm gonna spend some time really trying to get what you wrote.

I like what you said about not focusing on 2-way binding. And I don't really think the 2-way thing is strictly necessary if there's another, elegant way to get the job done. What I'm really looking for in a form library is what I guess I might call FOM, or Form-to-Object Mapping. Sort of like ORM, but instead of the impedance mismatch existing between objects and the RDB, it's between the form and the structured data. Features that make it easy to map between form fields and arrays and sub-objects would be nice. That's really all I'm looking for, but it may just be staring me in the face.

jonjamz commented 9 years ago

Right--this package does that using the validationValue function. It's in the hands of whoever designs the element. The good thing is you only need to write the transform one time and you can re-use the element all you want.

The data in this in your action function is an object--the compiled results of everything returned from validationValue in your elements, or $(el).val() if you didn't specify a validationValue. It will be formed in a way that matches what you specified in your schema.

With arrays, objects, and integers, it's in your hands to use validationValue to form the data from the DOM properly before it gets passed through validation, and subsequently added to the form's internal data context.

jcheroske commented 9 years ago

Ok, I will chew on that. Thanks, Jon.

johngonzalez commented 9 years ago

I have used the complete code example and wrapped the input element inside form block, but when I make click in "Add another field" button the form make submit. seleccion_003

jonjamz commented 9 years ago

You may need to put event.preventDefault() at the top of the event handler for the .add-field button.

johngonzalez commented 9 years ago

I want have validation String in each input from array (see my before figure). So I made this:

<template name="inputElement">
  <div class="reactive-element">
    {{#each numberOfFields}}
      {{> basicInput field="answers.ans"}}
    {{/each}}
  </div>
  <button class="add-field">Add another field</button>
</template>

The field answers.$.ans is required but the validation doesn't works. Other question: where and how the helper getValueFor should be in this design. Thanks

jonjamz commented 9 years ago

If you can make an example repository for this I can help solve it. There have been a few other requests around validating a variable list of fields, so I may be able to write more elegant support for this into the package.

jonjamz commented 9 years ago

When I wrote the example above, the idea was to put the field on the outer element, retrieve the values from the inputs inside and compile them into an array, and return those for validation. But the way you're doing it here involves a different concept.

johngonzalez commented 9 years ago

Hello jonjamz here the repository: https://github.com/johngonzalez/examples-forms Thanks for your collaboration

0o-de-lally commented 9 years ago

+1 Also looking for docs on Arrays.

@jonjamz Really enjoying this package. Any change we can see the example in plain JS instead of coffeescript? Thanks!

sanoyphilippe commented 7 years ago

Hi I'm also looking for more documentation or support with regards to arrays of objects @jonjamz have you solved out how to solve the way @johngonzalez implements it? I was also trying to do it in the same way. Though it appears that this package wasn't designed to be used for that?

Also I found a bug wherein the schema isn't retrieved if the field you specify for the element component is a field within an object element of an array field. Like this field 'position.$.supplierId' -> wherein position is an array of objects field

jonjamz commented 7 years ago

The issue with @johngonzalez example from what I can tell after a quick look is that he is generating many components with the same field. This package does not examine template structure and infer data structure from it--it is up to you to create components that handle data in the format you want, create the template with the structure you want, and then grab and format data the way you want before passing it to validation.

As for the rest I'll have to answer later...