Closed sockhead closed 8 years ago
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
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!
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:
tcomb-validation
v1 for tcomb-validation
v2 (APIs are compatible, your code shouldn't break here)react
v0.13 for react
v0.14 (well, this is big)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
I needed exactly same functionality. Wanted to know if unions are supported in v0.8.1. Great Work!
@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
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
@amrut-bawane after cloning the repo
npm install
npm run build
should build the lib
folder
@amrut-bawane Nevermind, just released version https://github.com/gcanti/tcomb-form/releases/tag/v0.8.2
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
@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>
)
}
})
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.
@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>
)
}
})
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;
}
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.
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:
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.
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?