Skip to content
This repository has been archived by the owner on Feb 16, 2021. It is now read-only.

List with Dynamic Items (Different structs based on selected value) #297

Closed
sockhead opened this issue Feb 17, 2016 · 14 comments
Closed

List with Dynamic Items (Different structs based on selected value) #297

sockhead opened this issue Feb 17, 2016 · 14 comments

Comments

@sockhead
Copy link

Is it possible to create a list of dynamic items? I'm not sure how to pass value to Account in order for it appropriately return the correct structure. Essentially I have a predefined group of account types but I allow the user to select Other if the account type they want is not available. If they select other they should then be given a textbox where they can input the name of the account type, but this should only happen if they select Other.

For example a list of accounts where the account has a structure of:

getType(value) {
  let props = {
    accounts: t.list(Account)
  }
  return t.struct(props);
}
const Account = t.struct({
  type: t.enums.of(accountTypes),
  Fields: t.maybe(t.list(Field))
});

However I want to be able to make the Account have a dynamic structure depending on the value of type. I want a new field to appear if and only if the item's type == 'Other' like below.

const Account = t.struct({
  type: t.enums.of(accountTypes),
  label: t.Str,
  Fields: t.maybe(t.list(Field))
});

I also tried creating a list where Account was a function, but I have no clue if it's possible to do what I'm hoping for and have no clue how to return the struct associated with each item.

How would I go about passing the correct item's value to each item in the list in order to dynamically generate the appropriate type for the required structure? Is it possible to do some sort of foreach on the list values and then generate the appropriate structures and concatenate them together in a list? Is this even a possibility with how List works?

gcanti added a commit that referenced this issue Feb 18, 2016
@gcanti
Copy link
Owner

gcanti commented Feb 18, 2016

Hi @sockhead,

Lists of different types are not supported at the moment and technically they won't be supported, ever. This is because a tcomb's list, by definition, contains only values of the same type. What we can do though is adding support for unions, this way the list would correctly contain only one type but that type would be a union:

const AccountType = t.enums.of([
  'type 1',
  'type 2',
  'other'
], 'AccountType')

const KnownAccount = t.struct({
  type: AccountType
}, 'KnownAccount')

// UnknownAccount extends KnownAccount so it owns also the type field
const UnknownAccount = KnownAccount.extend({
  label: t.String,
}, 'UnknownAccount')

// the union
const Account = t.union([KnownAccount, UnknownAccount], 'Account')

// the final form type
const Type = t.list(Account)

Generally tcomb's unions require a dispatch implementation in order to select the suitable type constructor for a given value and this would be the key in your use case:

// if account type is 'other' return the UnknownAccount type
Account.dispatch = value => value && value.type === 'other' ? UnknownAccount : KnownAccount

A complete example:

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

const AccountType = t.enums.of([
  'type 1',
  'type 2',
  'other'
], 'AccountType')

const KnownAccount = t.struct({
  type: AccountType
}, 'KnownAccount')

const UnknownAccount = KnownAccount.extend({
  label: t.String,
}, 'UnknownAccount')

const Account = t.union([KnownAccount, UnknownAccount], 'Account')

Account.dispatch = value => value && value.type === 'other' ? UnknownAccount : KnownAccount

const Type = t.list(Account)

const App = React.createClass({

  onSubmit(evt) {
    evt.preventDefault()
    const v = this.refs.form.getValue()
    if (v) {
      console.log(v)
    }
  },

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

})

There's a draft implementation on this branch https://github.com/gcanti/tcomb-form/tree/297
Seems to work very well but I must do a few additional tests

@sockhead
Copy link
Author

Thanks for the quick response. I'm currently using an older version of tcomb-form (0.5) but plan on updating to the latest version in the near future when I have time to go through and update everywhere that I use the forms.

Since I'm using an older version, the changes that you made to enable union support is quite substantial between 0.5 and 0.8.

I tried a quick crack at updating components.js but it broke my struct templates so I just reverted back to 0.5. I look forward to trying this out when I update to 0.8.

Thanks again!

@gcanti
Copy link
Owner

gcanti commented Feb 20, 2016

v0.5 is quite old, you are still using tcomb and tcomb-validation v1 I guess.

However, after scanning the changelog, upgrading to 0.8 shouldn't be too painful (tcomb-form API is fairly stable):

Breaking changes per version:

  • 0.5 -> 0.6 drop tcomb-validation v1 for tcomb-validation v2 (APIs are compatible, your code shouldn't break here)
  • 0.6 -> 0.7 drop react v0.13 for react v0.14 (well, this is big)
  • 0.7 -> 0.8 drop uvdom, uvdom-bootstrap dependencies (your code shouldn't break here if you are using the default bootstrap templates, the default language (english) and you are not relying on the uvdom and uvdom-bootstrap modules)

If you decide to upgrade let me know how it goes and if you need some help.

Cheers,
Giulio

@amrut-bawane
Copy link

I needed exactly same functionality. Wanted to know if unions are supported in v0.8.1.
Great Work!

@gcanti
Copy link
Owner

gcanti commented Feb 22, 2016

@amrut-bawane Currently there's a candidate implementation in the https://github.com/gcanti/tcomb-form/tree/297 branch if you want to give it a whirl. If everything's ok (needs tests) I'll write some documentation and then release v0.8.2

@amrut-bawane
Copy link

Yeah i tried that version. Lib folder is missing hence module is not being imported. Not perfectly sure about this issue, maybe you can look into it.
Thanks

@gcanti
Copy link
Owner

gcanti commented Feb 22, 2016

@amrut-bawane after cloning the repo

npm install
npm run build

should build the lib folder

@gcanti
Copy link
Owner

gcanti commented Feb 22, 2016

@amrut-bawane Nevermind, just released version https://github.com/gcanti/tcomb-form/releases/tag/v0.8.2

@amrut-bawane
Copy link

Ohh that's cool!
A small issue that I am facing is, while trying to add item to a list field. How to get handler to the addItem, removeItem functions associated with the list from, let's say clicking a button outside of the form?
Appreciate your quick fixes

@gcanti
Copy link
Owner

gcanti commented Feb 23, 2016

@amrut-bawane

How to get handler to the addItem, removeItem functions associated with the list from, let's say clicking a button outside of the form?

tcomb-form's forms are controlled components. You already have complete control on the form by tweaking its value:

const Type = t.list(Account)

const App = React.createClass({

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

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

  addItem() {
    // adds a new item
    this.setState({ value: this.state.value.concat(undefined) })
  },

  render() {
    return (
      <div>
        <t.form.Form
          ref="form"
          type={Type}
          value={this.state.value}
          onChange={this.onChange}
        />
        <div className="form-group">
          <button type="button" className="btn btn-primary" onClick={this.addItem}>Add item</button>
        </div>
      </div>
    )
  }

})

@amrut-bawane
Copy link

I wish to create a list that can accept fields of two types - FilterField and MetaField.

generateSchema() {
    var FilterField = t.enums.of([
        'Pattern',
        'Color',
        'Brand'
    ], 'FilterField');

    var MetaField = t.struct({
        title: t.String,
        identifier: t.String,
        canCreateVariant: t.Boolean,
        required: t.Boolean,
        isSearchFilter: t.Boolean,
        type: t.String,
        unit: t.String,
        minVal: t.String,
        maxVal: t.String,
        toolTip: t.String,
        placeHolder: t.String
      }, 'MetaField');

    var Field = t.union([FilterField, MetaField], 'Field');
    const that = this;
    Field.dispatch = value => {
      if(that.state.filterField) return FilterField;
      else return MetaField;
    }

    var Fields = t.list(Field);

    var Schema = t.struct({
      title: t.String,
      description: t.String,
      longName: t.String,
      canAddProducts: t.Boolean,
      formfields: Fields
    });
     return Schema;
  }

Upon receiving a click event, I am calling the addItem method of the list -

addFilter(e) {
    this.setState({filterField:true}, () => {
      this.refs.form.getComponent('formfields').addItem(e);
    });
  }
addMeta(e) {
    this.setState({filterField:false}, () => {
      this.refs.form.getComponent('formfields').addItem(e);  
    });
  }

The issue is each time the list gets updated, only the latest entry i.e. either FilterField or MetaField gets added to the list.

@gcanti
Copy link
Owner

gcanti commented Feb 23, 2016

@amrut-bawane relying on getComponent('xxx').addItem is not safe: it's an internal API and is not documented. The idiomatic way is to leverage the tcomb-form's controlled component behaviour:

const FilterField = t.enums.of([
  'Pattern',
  'Color',
  'Brand'
], 'FilterField')

const MetaField = t.struct({
  title: t.String,
  identifier: t.String,
  canCreateconstiant: t.Boolean,
  required: t.Boolean,
  isSearchFilter: t.Boolean,
  type: t.String,
  unit: t.String,
  minVal: t.String,
  maxVal: t.String,
  toolTip: t.String,
  placeHolder: t.String
}, 'MetaField')

const Field = t.union([FilterField, MetaField], 'Field')

Field.dispatch = value => {
  if (t.Object.is(value)) {
    return MetaField
  }
  return FilterField
}

const Fields = t.list(Field)

const Schema = t.struct({
  title: t.String,
  description: t.String,
  longName: t.String,
  canAddProducts: t.Boolean,
  formfields: Fields
})

const App = React.createClass({

  getInitialState() {
    return {
      value: {
        formfields: []
      }
    }
  },

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

  addFilter() {
    this.setState({
      // here I'm using the tcomb immutability helpers, use what you want but be sure to change the `value` reference
      // otherwise tcomb-form won't detect any change
      value: t.update(this.state.value, {
        formfields: {
          $push: [undefined]
        }
      })
    })
  },

  addMeta() {
    this.setState({
      value: t.update(this.state.value, {
        formfields: {
          $push: [{}]
        }
      })
    })
  },

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

  render() {
    return (
      <form onSubmit={this.onSubmit}>
        <t.form.Form
          ref="form"
          type={Schema}
          value={this.state.value}
          onChange={this.onChange}
        />
        <div className="form-group">
          <button type="button" className="btn btn-primary" onClick={this.addFilter}>Add filter</button>
          <button type="button" className="btn btn-primary" onClick={this.addMeta}>Add meta</button>
          <button type="submit" className="btn btn-primary">Save</button>
        </div>
      </form>
    )
  }

})

@amrut-bawane
Copy link

Cool, that lets me add fields of both types. But as the dispatch method of the union field is called, all the list items update to the same type- all turn to FilterFields or MetaFields. I guess that's how a union works, but any workaround to get me the required functionality?

 Field.dispatch = value => {
      if(that.state.filterField) return FilterField;
      else return MetaField;
    }

@gcanti
Copy link
Owner

gcanti commented Feb 23, 2016

But as the dispatch method of the union field is called, all the list items update to the same type- all turn to FilterFields or MetaFields

Weird, that is not the result I see when I run the example above.
Please open a new issue with the complete code you are running in order to reproduce the problem.

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

No branches or pull requests

3 participants