gcanti / tcomb-form

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

Is it possible to Use Custom Input Component? #291

Closed BigPrimeNumbers closed 8 years ago

BigPrimeNumbers commented 8 years ago

I'd like to be able to use this with a custom input component such as react-autosuggest, but am not sure how I would do that. Is this possible?

This would be using react 0.14.6, autocomplete 3.4.0, and the latest version of tcomb-form. Thanks!

gcanti commented 8 years ago

Hi,

Yes it's possible. See here for examples of integrating custom components:

https://github.com/gcanti/tcomb-form/issues/261 https://github.com/gcanti/tcomb-form/issues/273 https://github.com/gcanti/tcomb-form/issues/274

BigPrimeNumbers commented 8 years ago

Thanks for the references. Unfortunately, I'm a bit new at this and am still a bit confused. I've got the following in a file called AutocompleteFactory.js:

import React from 'react'
import t from 'tcomb-form'
import Autosuggest from 'react-autosuggest'

class AutosuggestComponent extends t.form.Component { // extend the base class

    getTemplate() {
        return (locals) => {
            return (
                <Autosuggest
                    suggestions={locals.suggestions}
                    onSuggestionsUpdateRequested={locals.onSuggestionsUpdateRequested}
                    getSuggestionValue={locals.getSuggestionValue}
                    renderSuggestion={locals.renderSuggestion}
                    inputProps={locals.inputProps}
                />
            )
        }
    }

}

export default AutosuggestComponent

and then in the main file:

import React from 'react'
import { Link } from 'react-router'
import Autosuggest from 'react-autosuggest'
import t from 'tcomb-form'
import AutosuggestComponent from './AutosuggestFactory'

const Form = t.form.Form;

const Type = t.struct({
  name: t.String,
  company: t.maybe(t.String)
});

const options = {
  fields: {
    name: {
      factory: AutosuggestComponent
    },
    company: {
      factory: AutosuggestComponent
    }
  }
};

...

export default class foo extends React.Component {
...
    onSubmit(evt) {
        evt.preventDefault()
        const value = this.refs.form.getValue()
        if (value) {
          console.log(value)
        }
    }
    onDrugSuggestionsUpdateRequested({value, reason}) {
        this.setState({
          suggestions: getSuggestions(value)
        });
    }
    render() {
        let inputProps = {
            suggestions: suggestions,
            onSuggestionsUpdateRequested: this.onSuggestionsUpdateRequested,
            getSuggestionValue: getSuggestionValue,
            renderSuggestion: renderSuggestion,
            inputProps: inputCompanyProps
        }
    return (
            <div>
                <Form
                    ref="form"
                    type={Type}
                    options={options}
                    value={inputProps}
                />
                <button onClick={this.onSubmit}>Save</button>
            </div>
   )
}

But the form just renders with ordinary text fields, not with Autocomplete components. Any help is geatly appreciated. Thanks for your time and hard work!

gcanti commented 8 years ago

Hi,

With your actual code, I can help you more.

I think that a custom template should work:

import Autosuggest from 'react-autosuggest'

//
// a suggestion config
//

const languages = [
  {
    name: 'C',
    year: 1972
  },
  {
    name: 'Elm',
    year: 2012
  },
  {
    name: 'Javascript',
    year: 1995
  },
  {
    name: 'Python',
    year: 1991
  }
]

function getSuggestions(value) {
  return languages.filter(language => language.name.toLowerCase().indexOf(value) === 0)
}

function getSuggestionValue(suggestion) {
  return suggestion.name
}

function renderSuggestion(suggestion) {
  return (
    <span>{suggestion.name}</span>
  )
}

//
// Template
// given a suggestion config returns the proper template
//

function getTemplate(options) {
  function renderInput(locals) {
    const value = locals.value || '' // react-autosuggest doesn't like null or undefined as value
    const inputProps = {
      ...locals.attrs,
      value: value,
      onChange: (evt, { newValue }) => {
        locals.onChange(newValue)
      }
    }
    const suggestions = options.getSuggestions(value)
    return (
      <Autosuggest
        suggestions={suggestions}
        getSuggestionValue={options.getSuggestionValue}
        renderSuggestion={options.renderSuggestion}
        inputProps={inputProps}
      />
    )
  }

  return t.form.Form.templates.textbox.clone({ renderInput })
}

//
// Usage
//

const Type = t.struct({
  language: t.String
})

const options = {
  fields: {
    language: {
      template: getTemplate({
        getSuggestions,
        getSuggestionValue,
        renderSuggestion
      })
    }
  }
}
BigPrimeNumbers commented 8 years ago

Thanks so much for your help with this. A few questions:

  1. How could I separate the factory into a separate file? I tried: AutosuggestFactory.js (the commented line is not actually in the code, but is a line that will has to do with my 3rd question)
//AutosuggestFactory.js
import React from 'react'
import t from 'tcomb-form'
import Autosuggest from 'react-autosuggest'

class AutosuggestComponent extends t.form.Component { // extend the base class

    getTemplate(options) {
        function renderInput(locals) {
            const value = locals.value || '' // react-autosuggest doesn't like null or undefined as value
            const inputProps = {
              ...locals.attrs,
              value: value,
              onChange: (evt, { newValue }) => {
                locals.onChange(newValue)
              }
            }
            const suggestions = options.getSuggestions(value)
            return (
              <Autosuggest
                suggestions={suggestions}
                getSuggestionValue={options.getSuggestionValue}
//onSuggestionsUpdateRequested={options. onSuggestionsUpdateRequested}
                renderSuggestion={options.renderSuggestion}
                inputProps={inputProps}
              />
            )
        }
        return t.form.Form.templates.textbox.clone({ renderInput })
    }
}

export default AutosuggestComponent

and in the main app:

//app.js
import React from 'react'
import { Link } from 'react-router'
import Autosuggest from 'react-autosuggest'
import t from 'tcomb-form'
import AutosuggestComponent from './AutosuggestFactory'

const Form = t.form.Form;

const Type = t.struct({
  name: t.String,
  company: t.maybe(t.String)
});

const options = {
  fields: {
    name: {
      factory: AutosuggestComponent.getTemplate({
        getSuggestions,
        getSuggestionValue,
        renderSuggestion,
      })
    }
  }
}
...
// THE REST IS THE SAME CODE FROM ABOVE

and get the error: "Uncaught TypeError: _AutosuggestFactory2.default.getTemplate is not a function"

  1. When I try the code as given above in your response, I get some sort of error about the tform textbox: "Uncaught TypeError: Can't add property config, object is not extensible" The console leads to this code:
    function create() {
      var overrides = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];

      function textbox(locals) {
        locals.config = textbox.getConfig(locals); // THE ERROR IS HERE
        locals.attrs = textbox.getAttrs(locals);

        if (locals.type === 'hidden') {
          return textbox.renderHiddenTextbox(locals);
        }

        var children = locals.config.horizontal ? textbox.renderHorizontal(locals) : textbox.renderVertical(locals);

        return textbox.renderFormGroup(children, locals);
      }
...
  1. Also, I need to be able to include functions that are not global to the app, so currently the global options struct wouldn't be able to resolve some of the more local functions that are not in the global scope. How do I include those as parameters to the getTemplate function? Specifically, the onSuggestionsUpdateRequested method above in my original post.

Here is a code reference to a simple implementation of a single Autocomplete if it helps Codepen Autocomplete example

gcanti commented 8 years ago

How could I separate the factory into a separate file?

You don't need a factory nor a onSuggestionsUpdateRequested implementation, just a template:

Complete working example

import Autosuggest from 'react-autosuggest'

const languages = [
  {
    name: 'C',
    year: 1972
  },
  {
    name: 'Elm',
    year: 2012
  },
  {
    name: 'Javascript',
    year: 1995
  },
  {
    name: 'Python',
    year: 1991
  }
]

function getSuggestions(value) {
  return languages.filter(language => language.name.indexOf(value) === 0)
}

function getSuggestionValue(suggestion) {
  return suggestion.name
}

function renderSuggestion(suggestion) {
  return (
    <span>{suggestion.name}</span>
  )
}

// define the template only once
function getTemplate(options) {
  function renderInput(locals) {
    const value = locals.value || '' // react-autosuggest doesn't like null or undefined as value
    const inputProps = {
      ...locals.attrs,
      value: value,
      onChange: (evt, { newValue }) => {
        locals.onChange(newValue)
      }
    }
    const suggestions = options.getSuggestions(value)
    return (
      <Autosuggest
        suggestions={suggestions}
        getSuggestionValue={options.getSuggestionValue}
        renderSuggestion={options.renderSuggestion}
        inputProps={inputProps}
      />
    )
  }

  return t.form.Form.templates.textbox.clone({ renderInput })
}

// define the type
const Type = t.struct({
  language: t.String
})

const options = {
  fields: {
    language: {
      attrs: {
        placeholder: 'Type C'
      },
      template: getTemplate({
        getSuggestions,
        getSuggestionValue,
        renderSuggestion
      })
    }
  }
}

const App = React.createClass({

  getInitialState() {
    return {
      value: {},
    }
  },

  onSubmit(evt) {
    evt.preventDefault()
    const v = this.refs.form.getValue()
    if (v) {
      console.log(v) // eslint-disable-line
    }
  },

  onChange(value, path) {
    this.setState({ value })
  },

  render() {
    return (
      <form onSubmit={this.onSubmit}>
        <t.form.Form
          ref="form"
          type={Type}
          options={options}
          value={this.state.value}
          onChange={this.onChange}
        />
        <div className="form-group">
          <button type="submit" className="btn btn-primary">Save</button>
        </div>
      </form>
    )
  }

})
braindrained commented 8 years ago

Hi @gcanti I tried this but when I press the arrow keys to select a suggestion the vale of the input change and the suggestions too so I can only select the first or the last suggestion.

gcanti commented 8 years ago

I guess you could disentangle the current textbox value and the current selection, something like:

import Autosuggest from 'react-autosuggest'

const languages = [
  {
    name: 'C',
    year: 1972
  },
  {
    name: 'Elm',
    year: 2012
  },
  {
    name: 'Elm2',
    year: 2012
  },
  {
    name: 'Javascript',
    year: 1995
  },
  {
    name: 'Python',
    year: 1991
  }
]

function getSuggestions(value) {
  return languages.filter(language => language.name.indexOf(value) === 0)
}

function getSuggestionValue(suggestion) {
  return suggestion.name
}

function renderSuggestion(suggestion) {
  return (
    <span>{suggestion.name}</span>
  )
}

class Auto extends React.Component {
  consructor(props) {
    super(props)
    this.state = {}
  }
  render() {
    const value = this.props.value || '' // react-autosuggest doesn't like null or undefined as value
    const onChange = (evt) => {
      if (evt.reason === 'enter' || evt.reason === 'click') {
        this.props.onChange(this.state.value)
      }
      if (evt.reason === 'type') {
        this.props.onChange(evt.value)
      }
    }
    const inputProps = {
      ...this.props.attrs,
      value: value,
      onChange: (evt, { newValue }) => {
        this.setState({ value: newValue })
      }
    }
    const suggestions = this.props.options.getSuggestions(value)
    return (
      <Autosuggest
        suggestions={suggestions}
        onSuggestionsUpdateRequested={onChange}
        getSuggestionValue={this.props.options.getSuggestionValue}
        renderSuggestion={this.props.options.renderSuggestion}
        inputProps={inputProps}
      />
    )
  }
}

// define the template only once
function getTemplate(options) {
  function renderInput(locals) {
    return <Auto {...locals} options={options} />
  }

  return t.form.Form.templates.textbox.clone({ renderInput })
}

// define the type
const Type = t.struct({
  language: t.String
})

const options = {
  fields: {
    language: {
      attrs: {
        placeholder: 'Type C'
      },
      template: getTemplate({
        getSuggestions,
        getSuggestionValue,
        renderSuggestion
      })
    }
  }
}
braindrained commented 8 years ago

It's clear now! Thank you!