2016-04-04 10:21:18 +09:00
import React , { PropTypes , Component } from 'react' ;
2016-06-10 10:25:38 +09:00
import { FormattedMessage , intlShape } from 'react-intl' ;
2016-04-04 10:21:18 +09:00
import Formsy from 'formsy-react' ;
import { Button } from 'react-bootstrap' ;
2016-06-14 10:45:03 +09:00
import Flash from "./Flash.jsx" ;
2016-06-03 11:03:36 +09:00
import FormGroup from "./FormGroup.jsx" ;
2016-06-13 16:05:46 +09:00
import { flatten , deepValue , getEditableFields , getInsertableFields } from './utils.js' ;
2016-04-04 10:21:18 +09:00
2016-04-07 15:29:02 +09:00
/ *
1. Constructor
2. Helpers
3. Errors
4. Context
4. Method & Callback
5. Render
* /
2016-04-04 10:21:18 +09:00
class NovaForm extends Component {
2016-06-22 10:16:07 +02:00
2016-04-07 15:29:02 +09:00
// --------------------------------------------------------------------- //
// ----------------------------- Constructor --------------------------- //
// --------------------------------------------------------------------- //
2016-04-04 16:50:07 +09:00
constructor ( props ) {
super ( props ) ;
2016-04-04 10:21:18 +09:00
this . submitForm = this . submitForm . bind ( this ) ;
2016-04-07 22:21:01 +09:00
this . updateState = this . updateState . bind ( this ) ;
2016-04-04 10:21:18 +09:00
this . methodCallback = this . methodCallback . bind ( this ) ;
2016-05-12 11:46:30 +09:00
this . addToAutofilledValues = this . addToAutofilledValues . bind ( this ) ;
2016-04-04 16:50:07 +09:00
this . throwError = this . throwError . bind ( this ) ;
this . clearErrors = this . clearErrors . bind ( this ) ;
2016-04-17 16:47:04 +09:00
this . updateCurrentValue = this . updateCurrentValue . bind ( this ) ;
2016-10-13 21:57:19 +08:00
this . formKeyDown = this . formKeyDown . bind ( this ) ;
2016-04-17 16:47:04 +09:00
2016-04-15 15:42:14 +09:00
// a debounced version of seState that only updates state every 500 ms (not used)
this . debouncedSetState = _ . debounce ( this . setState , 500 ) ;
2016-06-22 10:16:07 +02:00
2016-04-04 10:21:18 +09:00
this . state = {
disabled : false ,
2016-04-07 22:21:01 +09:00
errors : [ ] ,
2016-05-12 11:46:30 +09:00
autofilledValues : { } ,
2016-04-07 22:21:01 +09:00
currentValues : { }
2016-04-04 10:21:18 +09:00
} ;
}
2016-04-07 15:29:02 +09:00
// --------------------------------------------------------------------- //
// ------------------------------- Helpers ----------------------------- //
// --------------------------------------------------------------------- //
2016-06-13 16:05:46 +09:00
// return the current schema based on either the schema or collection prop
getSchema ( ) {
return this . props . schema ? this . props . schema : this . props . collection . simpleSchema ( ) . _schema ;
}
2016-06-03 11:03:36 +09:00
getFieldGroups ( ) {
2016-06-13 16:05:46 +09:00
const schema = this . getSchema ( ) ;
2016-06-03 11:03:36 +09:00
// build fields array by iterating over the list of field names
let fields = this . getFieldNames ( ) . map ( fieldName => {
2016-06-22 10:16:07 +02:00
2016-06-03 11:03:36 +09:00
// get schema for the current field
const fieldSchema = schema [ fieldName ] ;
fieldSchema . name = fieldName ;
2016-06-09 17:42:20 +09:00
// intialize properties
2016-06-03 11:03:36 +09:00
let field = {
name : fieldName ,
datatype : fieldSchema . type ,
control : fieldSchema . control ,
2016-06-22 10:16:07 +02:00
layout : this . props . layout ,
order : fieldSchema . order
2016-06-03 11:03:36 +09:00
}
2016-06-09 17:42:20 +09:00
// add label
const intlFieldName = this . context . intl . formatMessage ( { id : this . props . collection . _name + "." + fieldName } ) ;
field . label = ( typeof this . props . labelFunction === "function" ) ? this . props . labelFunction ( intlFieldName ) : intlFieldName ,
2016-06-03 11:03:36 +09:00
// add value
2016-06-22 10:16:07 +02:00
field . value = this . getDocument ( ) && deepValue ( this . getDocument ( ) , fieldName ) ? deepValue ( this . getDocument ( ) , fieldName ) : "" ;
2016-06-03 11:03:36 +09:00
2016-10-14 11:55:47 +02:00
// backward compatibility from 'autoform' to 'form'
if ( fieldSchema . autoform ) {
fieldSchema . form = fieldSchema . autoform ;
console . warn ( ` 🔭 Telescope Nova Warning: The 'autoform' field is deprecated. You should rename it to 'form' instead. It was defined on your ' ${ fieldName } ' field on the ' ${ this . props . collection . _name } ' collection ` ) ;
}
2016-06-03 11:03:36 +09:00
// replace value by prefilled value if value is empty
2016-10-05 08:43:13 +02:00
if ( fieldSchema . form && fieldSchema . form . prefill ) {
const prefilledValue = typeof fieldSchema . form . prefill === "function" ? fieldSchema . form . prefill . call ( fieldSchema ) : fieldSchema . form . prefill ;
2016-06-03 11:03:36 +09:00
if ( ! ! prefilledValue && ! field . value ) {
field . prefilledValue = prefilledValue ;
field . value = prefilledValue ;
}
}
2016-09-21 14:42:05 +02:00
// replace empty value, which has not been prefilled, by the default value from the schema
2016-09-22 08:11:38 +02:00
if ( fieldSchema . defaultValue && field . value === "" ) {
2016-09-21 14:42:05 +02:00
field . value = fieldSchema . defaultValue ;
}
2016-06-03 11:03:36 +09:00
// add options if they exist
2016-10-05 08:43:13 +02:00
if ( fieldSchema . form && fieldSchema . form . options ) {
field . options = typeof fieldSchema . form . options === "function" ? fieldSchema . form . options . call ( fieldSchema ) : fieldSchema . form . options ;
2016-06-03 11:03:36 +09:00
}
2016-10-05 08:43:13 +02:00
if ( fieldSchema . form && fieldSchema . form . disabled ) {
field . disabled = typeof fieldSchema . form . disabled === "function" ? fieldSchema . form . disabled . call ( fieldSchema ) : fieldSchema . form . disabled ;
2016-06-03 11:03:36 +09:00
}
2016-10-05 08:43:13 +02:00
if ( fieldSchema . form && fieldSchema . form . help ) {
field . help = typeof fieldSchema . form . help === "function" ? fieldSchema . form . help . call ( fieldSchema ) : fieldSchema . form . help ;
2016-06-03 11:03:36 +09:00
}
2016-07-04 10:32:00 +09:00
// add placeholder
2016-10-05 08:43:13 +02:00
if ( fieldSchema . form && fieldSchema . form . placeholder ) {
field . placeholder = fieldSchema . form . placeholder ;
2016-07-04 10:32:00 +09:00
}
2016-06-28 17:33:30 +09:00
if ( fieldSchema . beforeComponent ) field . beforeComponent = fieldSchema . beforeComponent ;
if ( fieldSchema . afterComponent ) field . afterComponent = fieldSchema . afterComponent ;
2016-06-03 11:03:36 +09:00
// add group
if ( fieldSchema . group ) {
field . group = fieldSchema . group ;
}
2016-10-26 15:20:32 +09:00
// add document
field . document = this . getDocument ( ) ;
2016-07-21 00:37:06 +02:00
2016-06-03 11:03:36 +09:00
return field ;
} ) ;
// remove fields where control = "none"
fields = _ . reject ( fields , field => field . control === "none" ) ;
2016-06-22 10:16:07 +02:00
fields = _ . sortBy ( fields , "order" ) ;
2016-06-03 11:03:36 +09:00
// console.log(fields)
// get list of all groups used in current fields
let groups = _ . compact ( _ . unique ( _ . pluck ( fields , "group" ) ) ) ;
2016-06-22 10:16:07 +02:00
2016-06-03 11:03:36 +09:00
// for each group, add relevant fields
groups = groups . map ( group => {
2016-06-09 20:26:33 +09:00
group . label = group . label || this . context . intl . formatMessage ( { id : group . name } ) ;
2016-06-03 11:03:36 +09:00
group . fields = _ . filter ( fields , field => { return field . group && field . group . name === group . name } ) ;
return group ;
} ) ;
// add default group
groups = [ {
2016-06-22 10:16:07 +02:00
name : "default" ,
label : "default" ,
2016-06-03 11:03:36 +09:00
order : 0 ,
fields : _ . filter ( fields , field => { return ! field . group ; } )
} ] . concat ( groups ) ;
// sort by order
groups = _ . sortBy ( groups , "order" ) ;
// console.log(groups);
return groups ;
}
2016-04-04 16:50:07 +09:00
// if a document is being passed, this is an edit form
2016-06-22 10:16:07 +02:00
getFormType ( ) {
2016-04-08 10:09:19 +09:00
return this . props . document ? "edit" : "new" ;
2016-04-04 10:21:18 +09:00
}
2016-04-04 16:50:07 +09:00
// get relevant fields
2016-04-15 11:35:04 +02:00
getFieldNames ( ) {
2016-06-13 16:05:46 +09:00
const fields = this . props . fields ;
2016-04-17 10:48:02 +09:00
// get all editable/insertable fields (depending on current form type)
2016-10-14 08:47:18 +02:00
let relevantFields = this . getFormType ( ) === "edit" ? getEditableFields ( this . getSchema ( ) , this . context . currentUser , this . getDocument ( ) ) : getInsertableFields ( this . getSchema ( ) , this . context . currentUser ) ;
2016-04-15 11:35:04 +02:00
2016-04-17 10:48:02 +09:00
// if "fields" prop is specified, restrict list of fields to it
2016-04-16 16:20:18 +02:00
if ( typeof fields !== "undefined" && fields . length > 0 ) {
2016-04-17 10:48:02 +09:00
relevantFields = _ . intersection ( relevantFields , fields ) ;
2016-04-15 11:35:04 +02:00
}
2016-04-16 16:20:18 +02:00
return relevantFields ;
2016-04-04 10:21:18 +09:00
}
2016-05-12 11:46:30 +09:00
// for each field, we apply the following logic:
// - if its value is currently being inputted, use that
// - else if its value was provided by the db, use that (i.e. props.document)
// - else if its value is provded by the autofilledValues object, use that
2016-04-07 22:21:01 +09:00
getDocument ( ) {
2016-05-12 11:46:30 +09:00
const currentDocument = _ . clone ( this . props . document ) || { } ;
const document = Object . assign ( _ . clone ( this . state . autofilledValues ) , currentDocument , _ . clone ( this . state . currentValues ) ) ;
2016-04-07 22:21:01 +09:00
return document ;
}
2016-04-17 16:49:31 +09:00
// NOTE: this is not called anymore since we're updating on blur, not on change
2016-04-07 22:21:01 +09:00
// whenever the form changes, update its state
2016-04-17 16:49:31 +09:00
updateState ( e ) {
// e can sometimes be event, sometims be currentValue
// see https://github.com/christianalfoni/formsy-react/issues/203
if ( e . stopPropagation ) {
e . stopPropagation ( ) ;
} else {
// get rid of empty fields
_ . forEach ( e , ( value , key ) => {
if ( _ . isEmpty ( value ) ) {
delete e [ key ] ;
}
} ) ;
this . setState ( {
currentValues : e
} ) ;
}
}
2016-04-17 16:47:04 +09:00
// manually update current value (i.e. on blur). See above for on change instead
updateCurrentValue ( fieldName , fieldValue ) {
const currentValues = this . state . currentValues ;
currentValues [ fieldName ] = fieldValue ;
this . setState ( { currentValues : currentValues } ) ;
2016-04-07 22:21:01 +09:00
}
2016-04-07 15:29:02 +09:00
// --------------------------------------------------------------------- //
// ------------------------------- Errors ------------------------------ //
// --------------------------------------------------------------------- //
2016-04-04 16:50:07 +09:00
2016-10-05 11:19:20 +02:00
// clear all errors and re-enable the form
2016-04-04 16:50:07 +09:00
clearErrors ( ) {
this . setState ( {
2016-10-05 11:19:20 +02:00
errors : [ ] ,
disabled : false ,
2016-04-04 16:50:07 +09:00
} ) ;
}
// render errors
renderErrors ( ) {
return < div className = "form-errors" > { this . state . errors . map ( message => < Flash key = { message } message = { message } / > ) } < / div >
}
2016-04-07 15:29:02 +09:00
// --------------------------------------------------------------------- //
// ------------------------------- Context ----------------------------- //
// --------------------------------------------------------------------- //
2016-06-22 10:16:07 +02:00
2016-04-07 15:29:02 +09:00
// add error to state
throwError ( error ) {
this . setState ( {
errors : [ error ]
} ) ;
}
2016-04-07 15:24:38 +09:00
// add something to prefilled values
2016-05-12 11:46:30 +09:00
addToAutofilledValues ( property ) {
2016-04-07 12:43:41 +09:00
this . setState ( {
2016-05-12 11:46:30 +09:00
autofilledValues : { ... this . state . autofilledValues , ... property }
2016-04-07 12:43:41 +09:00
} ) ;
}
2016-05-12 11:46:30 +09:00
// clear value
clearValue ( property ) {
}
2016-04-07 15:24:38 +09:00
// pass on context to all child components
2016-04-04 16:50:07 +09:00
getChildContext ( ) {
return {
throwError : this . throwError ,
2016-05-12 11:46:30 +09:00
autofilledValues : this . state . autofilledValues ,
addToAutofilledValues : this . addToAutofilledValues ,
2016-07-04 10:32:00 +09:00
updateCurrentValue : this . updateCurrentValue ,
getDocument : this . getDocument ,
2016-04-04 16:50:07 +09:00
} ;
}
2016-04-07 15:29:02 +09:00
// --------------------------------------------------------------------- //
// ------------------------------- Method ------------------------------ //
// --------------------------------------------------------------------- //
2016-04-04 16:50:07 +09:00
// common callback for both new and edit forms
2016-04-04 10:21:18 +09:00
methodCallback ( error , document ) {
if ( error ) { // error
2016-10-05 11:19:20 +02:00
this . setState ( { disabled : false } ) ;
console . log ( error ) ;
2016-04-04 10:21:18 +09:00
2016-06-10 10:43:23 +09:00
const errorContent = this . context . intl . formatMessage ( { id : error . reason } , { details : error . details } )
2016-04-04 10:21:18 +09:00
// add error to state
2016-04-04 16:50:07 +09:00
this . throwError ( {
2016-06-10 10:43:23 +09:00
content : errorContent ,
2016-04-04 16:50:07 +09:00
type : "error"
2016-04-04 10:21:18 +09:00
} ) ;
// run error callback if it exists
if ( this . props . errorCallback ) this . props . errorCallback ( document , error ) ;
} else { // success
// reset form if this is a new document form
if ( this . getFormType ( ) === "new" ) this . refs . form . reset ( ) ;
// run success callback if it exists
if ( this . props . successCallback ) this . props . successCallback ( document ) ;
// run close callback if it exists in context (i.e. we're inside a modal)
if ( this . context . closeCallback ) this . context . closeCallback ( ) ;
2016-10-05 11:19:20 +02:00
// else there is no close callback (i.e. we're not inside a modal), call the clear errors method
// note: we don't want to update the state of an unmounted component
else this . clearErrors ( ) ;
2016-06-22 10:16:07 +02:00
2016-04-04 10:21:18 +09:00
}
}
2016-04-04 16:50:07 +09:00
// submit form handler
2016-04-04 10:21:18 +09:00
submitForm ( data ) {
this . setState ( { disabled : true } ) ;
2016-08-09 07:57:12 +02:00
// complete the data with values from custom components which are not being catched by Formsy mixin
// note: it follows the same logic as NovaForm's getDocument method
data = {
... this . state . autofilledValues , // ex: can be values from EmbedlyURL or NewsletterSubscribe component
... data , // original data generated thanks to Formsy
... this . state . currentValues , // ex: can be values from DateTime component
} ;
2016-07-06 15:53:14 +02:00
2016-04-07 15:24:38 +09:00
const fields = this . getFieldNames ( ) ;
2016-04-04 10:21:18 +09:00
// if there's a submit callback, run it
2016-06-17 13:05:17 +09:00
if ( this . props . submitCallback ) {
data = this . props . submitCallback ( data ) ;
}
2016-06-22 10:16:07 +02:00
2016-04-04 10:21:18 +09:00
if ( this . getFormType ( ) === "new" ) { // new document form
// remove any empty properties
2016-06-13 16:05:46 +09:00
let document = _ . compactObject ( flatten ( data ) ) ;
2016-04-04 10:21:18 +09:00
// add prefilled properties
if ( this . props . prefilledProps ) {
document = Object . assign ( document , this . props . prefilledProps ) ;
}
// call method with new document
Meteor . call ( this . props . methodName , document , this . methodCallback ) ;
} else { // edit document form
2016-04-07 22:21:01 +09:00
const document = this . getDocument ( ) ;
2016-04-04 10:21:18 +09:00
// put all keys with data on $set
2016-06-13 16:05:46 +09:00
const set = _ . compactObject ( flatten ( data ) ) ;
2016-06-22 10:16:07 +02:00
2016-04-04 10:21:18 +09:00
// put all keys without data on $unset
const unsetKeys = _ . difference ( fields , _ . keys ( set ) ) ;
const unset = _ . object ( unsetKeys , unsetKeys . map ( ( ) => true ) ) ;
2016-06-22 10:16:07 +02:00
2016-04-04 10:21:18 +09:00
// build modifier
const modifier = { $set : set } ;
if ( ! _ . isEmpty ( unset ) ) modifier . $unset = unset ;
// call method with _id of document being edited and modifier
Meteor . call ( this . props . methodName , document . _id , modifier , this . methodCallback ) ;
}
}
2016-10-05 11:19:20 +02:00
componentWillUnmount ( ) {
2016-10-14 11:23:12 +02:00
// note: patch to cancel closeCallback given by parent
// we clean the event by hand
2016-10-05 11:19:20 +02:00
// example : the closeCallback is a function that closes a modal by calling setState, this modal being the parent of this NovaForm component
// if this componentWillUnmount hook is triggered, that means that the modal doesn't exist anymore!
// let's not call setState on an unmounted component (avoid no-op / memory leak)
this . context . closeCallback = f => f ;
}
2016-10-13 21:57:19 +08:00
// key down handler
formKeyDown ( event ) {
if ( ( event . ctrlKey || event . metaKey ) && event . keyCode === 13 ) {
this . submitForm ( this . refs . form . getModel ( ) ) ;
}
}
2016-04-07 15:29:02 +09:00
// --------------------------------------------------------------------- //
// ------------------------------- Render ------------------------------ //
// --------------------------------------------------------------------- //
2016-04-04 10:21:18 +09:00
render ( ) {
2016-06-22 10:16:07 +02:00
2016-06-03 11:03:36 +09:00
const fieldGroups = this . getFieldGroups ( ) ;
2016-04-04 10:21:18 +09:00
return (
2016-04-07 15:24:38 +09:00
< div className = { "document-" + this . getFormType ( ) } >
2016-06-22 10:16:07 +02:00
< Formsy.Form
onSubmit = { this . submitForm }
2016-10-13 21:57:19 +08:00
onKeyDown = { this . formKeyDown }
2016-06-22 10:16:07 +02:00
disabled = { this . state . disabled }
ref = "form"
2016-04-08 10:29:32 +09:00
>
2016-04-04 10:21:18 +09:00
{ this . renderErrors ( ) }
2016-06-03 11:03:36 +09:00
{ fieldGroups . map ( group => < FormGroup key = { group . name } { ...group } updateCurrentValue = { this . updateCurrentValue } / > ) }
2016-06-09 17:42:20 +09:00
< Button type = "submit" bsStyle = "primary" > < FormattedMessage id = "forms.submit" / > < / Button >
{ this . props . cancelCallback ? < a className = "form-cancel" onClick = { this . props . cancelCallback } > < FormattedMessage id = "forms.cancel" / > < / a > : null }
2016-04-04 10:21:18 +09:00
< / Formsy.Form >
< / div >
)
}
}
NovaForm . propTypes = {
2016-06-14 16:36:05 +09:00
collection : React . PropTypes . object ,
2016-06-13 16:05:46 +09:00
schema : React . PropTypes . object ,
2016-04-04 10:21:18 +09:00
document : React . PropTypes . object , // if a document is passed, this will be an edit form
2016-04-04 11:30:14 +09:00
submitCallback : React . PropTypes . func ,
2016-04-04 10:21:18 +09:00
successCallback : React . PropTypes . func ,
errorCallback : React . PropTypes . func ,
methodName : React . PropTypes . string ,
labelFunction : React . PropTypes . func ,
2016-04-08 10:29:32 +09:00
prefilledProps : React . PropTypes . object ,
layout : React . PropTypes . string ,
2016-04-15 11:35:04 +02:00
cancelCallback : React . PropTypes . func ,
2016-04-16 16:20:18 +02:00
fields : React . PropTypes . arrayOf ( React . PropTypes . string )
2016-04-08 10:29:32 +09:00
}
2016-08-05 18:26:55 +02:00
NovaForm . defaultProps = {
2016-04-08 10:29:32 +09:00
layout : "horizontal"
2016-04-04 10:21:18 +09:00
}
NovaForm . contextTypes = {
2016-06-09 17:42:20 +09:00
closeCallback : React . PropTypes . func ,
2016-10-14 08:47:18 +02:00
currentUser : React . PropTypes . object ,
2016-06-09 17:42:20 +09:00
intl : intlShape
2016-04-04 10:21:18 +09:00
}
2016-04-04 16:50:07 +09:00
NovaForm . childContextTypes = {
2016-05-12 11:46:30 +09:00
autofilledValues : React . PropTypes . object ,
addToAutofilledValues : React . PropTypes . func ,
updateCurrentValue : React . PropTypes . func ,
2016-07-04 10:32:00 +09:00
throwError : React . PropTypes . func ,
getDocument : React . PropTypes . func
2016-04-04 16:50:07 +09:00
}
2016-04-04 10:21:18 +09:00
module . exports = NovaForm ;
2016-06-22 10:16:07 +02:00
export default NovaForm ;