gcanti / tcomb-form

Forms library for react
https://gcanti.github.io/tcomb-form
MIT License
1.16k stars 136 forks source link

List with typeahead #138

Closed dajomu closed 9 years ago

dajomu commented 9 years ago

Hi, I'm trying to create a list with a typeahead section at the bottom. Do you have any suggestions as to how I would achieve this? Would I have to use a custom factory for the list?

I apologise if this is a bit of a vague request.

kompot commented 9 years ago

+1 on this, could not find examples of how to define a form with remote structs.

If it's not there yet maybe you could point out some entry points of where to start implementing this in tcomb-form.

gcanti commented 9 years ago

a typeahead section at the bottom

remote structs

Hi @dajomu @kompot Not sure what you want to achieve, could you please provide an example and/or more details?

kompot commented 9 years ago

Yeah, say we have two entities: Person and Roles such as

const Role = t.struct({
  id: t.Num,
  name: t.Str
});

const Person = t.struct({
  id: t.Num,
  username: t.Str,
  roles: t.list(Role)
});

List of roles might be very long. So I'd like to implement it with some typeahead control like react-select http://jedwatson.github.io/react-select/ which would fetch roles from remote source as you type.

blaflamme commented 9 years ago

I was asking the same here about how to integrate this widget with tcomb-form https://github.com/gcanti/tcomb-form/issues/82#issuecomment-105638968

gcanti commented 9 years ago

Hi everyone, this is the simplest example of integration I can think of.

Define a custom factory:

// react-select-factory.js

'use strict';

var Select = require('react-select');
var t = require('tcomb-form');

class ReactSelect extends t.form.Select {

  getTemplate() {
    return (locals) => { // <- locals contains the "recipe" to build the UI

      // handle error status
      var className = 'form-group';
      if (locals.hasError) {
        className += ' has-error';
      }

      // translate the option model from tcomb to react-select
      var options = locals.options.map(({value, text}) => ({value, label: text}));

      return (
        <div className={className}>
          <label className="control-label">{locals.label}</label>
          <Select
            name={locals.attrs.name}
            value={locals.value}
            options={options}
            onChange={locals.onChange}
          />
        </div>
      );
    };
  }

}

module.exports = ReactSelect;

Usage:

// app.js

var React = require('react');
var t = require('tcomb-form');
var ReactSelect = require('./react-select-factory');

var Form = t.form.Form;

var Gender = t.enums.of('Male Female');

var Person = t.struct({
  name: t.Str,
  surname: t.Str,
  gender: Gender
});

var options = {
  fields: {
    gender: {
      factory: ReactSelect // <- use my custom factory
    }
  }
};

var App = React.createClass({

  onSubmit(evt) {
    evt.preventDefault();
    var value = this.refs.form.getValue();
    if (value) {
      console.log(value);
    }
  },

  render() {
    return (
      <form onSubmit={this.onSubmit}>
        <Form ref="form"
          type={Person}
          options={options}
        />
        <button className="btn btn-primary">Save</button>
      </form>
    );
  }

});

React.render(<App />, document.getElementById('app'));

I'll add more documentation about custom factories when I'll find the time. If someone of you guys wants to help me out writing the docs I'd be really grateful.

dajomu commented 9 years ago

Thank you, I'll see if I can adapt the above solution to my needs. I'll let you know how I get on...

dajomu commented 9 years ago

Hi, I'm now working on this problem and I seem to have an issue with the getTemplate() method. It doesn't seem to be getting used. I still seem to be getting the template for a simple select box, so I guess that the ReactSelect is not getting used. I'm not getting any console errors, so I'm guessing that the getTemplate method is not overriding the normal template.

gcanti commented 9 years ago

Could you put up a gist so I can try to help you out?

dajomu commented 9 years ago

Here's a link to the gist - https://gist.github.com/dajomu/5f4dd55a3ee6f1cacbc5

It may be missing some bits of code to make the actual react component function properly (I've copy/pasted bits from a settings screen component for simplicity), but this has all of the relevant code that I'm using to generate the form.

dajomu commented 9 years ago

My idea was to create the dropdown choices by making PostCodeSelect a function that generates the enum from a set of options. These options would be obtained from an API call that is triggered by the locals.onChange method. I haven't yet looked in to how to pass a method on via that yet...

gcanti commented 9 years ago

Here's a link to the gist...

This is working for me (code rearranged):

import React from 'react';
import t from 'tcomb-form';
import ReactSelect from './ReactSelect'; // <= using exactly you file ReactSelect.js

const Form = t.form.Form;

const LocationForm = (model) => ({
  fields: {
    postcode: {
      factory: ReactSelect
    }
  }
});

const PostCodeSelect = t.enums({
  please: 'E17 3AA',
  god: 'NW6 6RF',
  no: 'N6 5BB'
});

const LocationModel = () => t.struct({
  postcode: PostCodeSelect
});

const model = LocationModel();
const form = LocationForm(model);
const value = {postcode: "please"};

const App = React.createClass({

  onSubmit(evt) {
    evt.preventDefault();
    var value = this.refs.form.getValue();
    if (value) {
      console.log(value);
    }
  },

  render() {
    return (
      <form onSubmit={this.onSubmit}>
        <Form ref="form"
          type={model}
          options={form}
          value={value}
        />
        <button className="btn btn-primary">Save</button>
      </form>
    );
  }

});

React.render(<App />, document.getElementById('app'));
gcanti commented 9 years ago

These options would be obtained from an API call that is triggered by the locals.onChange method...

working on this...

dajomu commented 9 years ago

It's really odd, so there are no real changes from my code to yours, but all I seem to get is the familiar selection that comes with tcomb, rather than anything fancy. I've just tried chucking out all of my code and replacing it with the code you pasted above and I still get the wrong template. I'm using tcomb 0.4.11, would that be the cause of these problems?

gcanti commented 9 years ago

I'm using tcomb 0.4.11

Ah! I'm using the latest version (v0.5.4). Can you upgrade?

Otherwise with v0.4.x you could try this (not tested):

const LocationForm = (model) => ({
  fields: {
    postcode: {
      //factory: ReactSelect 
      template: function (locals) { // place here the code of the function returned by getTemplate

        // handle error status
        var className = 'form-group';
        if (locals.hasError) {
          className += ' has-error';
        }

        ...

      }
    }
  }
});
dajomu commented 9 years ago

I can't update it just at the moment (dependencies and time pressure). I'll attempt your solution above. Thanks very much for the help!

blaflamme commented 9 years ago

Thanks, just to let you know this recipe works great!

gcanti commented 9 years ago

Great, thanks for your feedback

jwaggener commented 9 years ago

Is there plain JavaScript solution and or a solution that relies on React's preference for composition over prototypical inheritance?

I'm referring to this: class ReactSelect extends t.form.Select

React's documentation provides no path for inheritance or extending in this way. And prefers composition.

Also I'm just not as adept at translating coffeescript to something I can use in plain JavaScript.

What I've done so far is some jankiness:

var React = require('react'),
  _ = require('lodash'),
  t = require('tcomb-form'),
  Select = t.form.Select;

Select.prototype.getTemplate = function() {
  return function(locals) {
    return (<div>hello</div>);
  };
};

module.exports = Select;

Not exactly the way to go! :-)

gcanti commented 9 years ago

Hi, This repo and the examples are written in ES6 (*) not coffeescript.

React's preference for composition

My preference as well. Sometimes classes are useful though.

(*) and a pinch of ES7

gcanti commented 9 years ago

@jwaggener if you are still interested this is maybe what you are looking for:

Minimal custom factory interface

A React component such that:

Props:

Methods:


Laws:

Notes:

jwaggener commented 9 years ago

@gcanti Thank you. I'll look at this. That is what I was asking about.

I also incorporated babelify in my build so I can utilize and follow ES6 features and syntax.

Fun! Thanks.

gcanti commented 9 years ago

:+1:

jwaggener commented 9 years ago

@gcanti

I found it very easy to implement the interface. I do not understand the purpose of the Law to call onChange.

I found that calling setState({value: myVal}) when changing the value and then returning a tcomb validation with the validate method was sufficient. When I invoke myForm.getValue() I was able to log the changed value.

Why do I need to call onChange? And is that method injected by tcomb-form

https://gist.github.com/jwaggener/e311a3fbe14b3e5a462d

Thanks for this great lib!

gcanti commented 9 years ago

Why do I need to call onChange? And is that method injected by tcomb-form

Yes it's injected by tcomb-form's top level API t.form.Form (I'll add this remark to the docs, thanks). A t.form.Form component can behave like a controlled component (say you want modify the value or the options on the fly). Relevant example (disable a field based on another field's value):

https://github.com/gcanti/tcomb-form/blob/master/GUIDE.md#rendering-options