2016-03-22 00:25:23 +01:00
|
|
|
import React from 'react';
|
|
|
|
import { connect } from 'react-redux';
|
|
|
|
import { bindActionCreators } from 'redux';
|
2016-03-26 06:41:24 +01:00
|
|
|
import * as schemaActions from 'state/schema/actions';
|
2016-03-30 05:38:34 +02:00
|
|
|
import SilverStripeComponent from 'silverstripe-component';
|
|
|
|
import FormComponent from 'components/form/index';
|
|
|
|
import TextField from 'components/text-field/index';
|
|
|
|
import HiddenField from 'components/hidden-field/index';
|
|
|
|
import GridField from 'components/grid-field/index';
|
2016-03-29 04:38:48 +02:00
|
|
|
import fetch from 'isomorphic-fetch';
|
2016-04-07 12:35:52 +02:00
|
|
|
import deepFreeze from 'deep-freeze';
|
2016-03-29 04:38:48 +02:00
|
|
|
|
|
|
|
import es6promise from 'es6-promise';
|
|
|
|
es6promise.polyfill();
|
2016-03-22 00:25:23 +01:00
|
|
|
|
|
|
|
// Using this to map field types to components until we implement dependency injection.
|
2016-03-30 23:45:54 +02:00
|
|
|
const fakeInjector = {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Components registered with the fake DI container.
|
|
|
|
*/
|
|
|
|
components: {
|
|
|
|
TextField,
|
|
|
|
GridField,
|
|
|
|
HiddenField,
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the component matching the passed component name.
|
|
|
|
* Used when a component type is provided bt the form schema.
|
|
|
|
*
|
|
|
|
* @param string componentName - The name of the component to get from the injector.
|
|
|
|
*
|
|
|
|
* @return object|null
|
|
|
|
*/
|
|
|
|
getComponentByName(componentName) {
|
|
|
|
return this.components[componentName];
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Default data type to component mappings.
|
|
|
|
* Used as a fallback when no component type is provided in the form schema.
|
|
|
|
*
|
|
|
|
* @param string dataType - The data type provided by the form schema.
|
|
|
|
*
|
|
|
|
* @return object|null
|
|
|
|
*/
|
|
|
|
getComponentByDataType(dataType) {
|
|
|
|
switch (dataType) {
|
|
|
|
case 'String':
|
|
|
|
return this.components.TextField;
|
|
|
|
case 'Hidden':
|
|
|
|
return this.components.HiddenField;
|
|
|
|
case 'Custom':
|
|
|
|
return this.components.GridField;
|
|
|
|
default:
|
|
|
|
return null;
|
2016-03-22 00:25:23 +01:00
|
|
|
}
|
2016-03-30 23:45:54 +02:00
|
|
|
},
|
|
|
|
};
|
2016-03-22 00:25:23 +01:00
|
|
|
|
|
|
|
export class FormBuilderComponent extends SilverStripeComponent {
|
|
|
|
|
2016-03-30 23:45:54 +02:00
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.formSchemaPromise = null;
|
2016-04-07 07:15:14 +02:00
|
|
|
this.state = { isFetching: false };
|
|
|
|
}
|
2016-03-30 23:45:54 +02:00
|
|
|
|
2016-04-07 07:15:14 +02:00
|
|
|
componentDidMount() {
|
2016-03-30 23:45:54 +02:00
|
|
|
this.fetch();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetches data used to generate a form. This can be form schema and or form state data.
|
|
|
|
* When the response comes back the data is saved to state.
|
|
|
|
*
|
|
|
|
* @param boolean schema - If form schema data should be returned in the response.
|
|
|
|
* @param boolean state - If form state data should be returned in the response.
|
|
|
|
*
|
|
|
|
* @return object - Promise from the AJAX request.
|
|
|
|
*/
|
|
|
|
fetch(schema = true, state = false) {
|
|
|
|
const headerValues = [];
|
|
|
|
|
2016-04-07 07:15:14 +02:00
|
|
|
if (this.state.isFetching === true) {
|
2016-03-30 23:45:54 +02:00
|
|
|
return this.formSchemaPromise;
|
2016-03-22 00:25:23 +01:00
|
|
|
}
|
|
|
|
|
2016-03-30 23:45:54 +02:00
|
|
|
if (schema === true) {
|
|
|
|
headerValues.push('schema');
|
2016-03-22 00:25:23 +01:00
|
|
|
}
|
|
|
|
|
2016-03-30 23:45:54 +02:00
|
|
|
if (state === true) {
|
|
|
|
headerValues.push('state');
|
2016-03-22 00:25:23 +01:00
|
|
|
}
|
|
|
|
|
2016-03-30 23:45:54 +02:00
|
|
|
this.formSchemaPromise = fetch(this.props.schemaUrl, {
|
|
|
|
headers: { 'X-FormSchema-Request': headerValues.join() },
|
|
|
|
credentials: 'same-origin',
|
|
|
|
})
|
|
|
|
.then(response => response.json())
|
|
|
|
.then(json => {
|
2016-04-07 07:15:14 +02:00
|
|
|
this.setState({ isFetching: false });
|
2016-03-30 23:45:54 +02:00
|
|
|
this.props.actions.setSchema(json);
|
|
|
|
});
|
|
|
|
|
2016-04-07 07:15:14 +02:00
|
|
|
this.setState({ isFetching: true });
|
2016-03-30 23:45:54 +02:00
|
|
|
|
|
|
|
return this.formSchemaPromise;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Maps a list of schema fields to their React Component.
|
|
|
|
* Only top level form fields are handled here, composite fields (TabSets etc),
|
|
|
|
* are responsible for mapping and rendering their children.
|
|
|
|
*
|
|
|
|
* @param array fields
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
mapFieldsToComponents(fields) {
|
|
|
|
return fields.map((field, i) => {
|
|
|
|
const Component = field.component !== null
|
|
|
|
? fakeInjector.getComponentByName(field.component)
|
|
|
|
: fakeInjector.getComponentByDataType(field.type);
|
|
|
|
|
|
|
|
if (Component === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Props which every form field receives.
|
2016-04-07 12:35:52 +02:00
|
|
|
// Leave it up to the schema and component to determine
|
|
|
|
// which props are required.
|
|
|
|
const props = deepFreeze(field);
|
2016-03-30 23:45:54 +02:00
|
|
|
|
|
|
|
return <Component key={i} {...props} />;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
2016-04-07 11:29:52 +02:00
|
|
|
const formSchema = this.props.schemas[this.props.schemaUrl];
|
2016-03-30 23:45:54 +02:00
|
|
|
|
|
|
|
// If the response from fetching the initial data
|
|
|
|
// hasn't come back yet, don't render anything.
|
2016-04-07 11:29:52 +02:00
|
|
|
if (!formSchema) {
|
2016-03-30 23:45:54 +02:00
|
|
|
return null;
|
|
|
|
}
|
2016-03-22 00:25:23 +01:00
|
|
|
|
2016-04-10 23:16:32 +02:00
|
|
|
// Map form schema to React component attribute names,
|
|
|
|
// which requires renaming some of them (by unsetting the original keys)
|
|
|
|
const attributes = Object.assign({}, formSchema.schema.attributes, {
|
|
|
|
class: null,
|
|
|
|
className: formSchema.schema.attributes.class,
|
|
|
|
enctype: null,
|
|
|
|
encType: formSchema.schema.attributes.enctype,
|
|
|
|
});
|
|
|
|
|
2016-03-30 23:45:54 +02:00
|
|
|
const formProps = {
|
2016-04-07 11:29:52 +02:00
|
|
|
actions: formSchema.schema.actions,
|
2016-04-10 23:16:32 +02:00
|
|
|
attributes,
|
2016-04-07 11:29:52 +02:00
|
|
|
data: formSchema.schema.data,
|
|
|
|
fields: formSchema.schema.fields,
|
2016-03-30 23:45:54 +02:00
|
|
|
mapFieldsToComponents: this.mapFieldsToComponents,
|
|
|
|
};
|
2016-03-22 00:25:23 +01:00
|
|
|
|
2016-03-30 23:45:54 +02:00
|
|
|
return <FormComponent {...formProps} />;
|
|
|
|
}
|
2016-03-22 00:25:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
FormBuilderComponent.propTypes = {
|
2016-03-30 23:45:54 +02:00
|
|
|
actions: React.PropTypes.object.isRequired,
|
|
|
|
schemaUrl: React.PropTypes.string.isRequired,
|
|
|
|
schemas: React.PropTypes.object.isRequired,
|
2016-03-22 00:25:23 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
function mapStateToProps(state) {
|
2016-03-30 23:45:54 +02:00
|
|
|
return {
|
|
|
|
schemas: state.schemas,
|
|
|
|
};
|
2016-03-22 00:25:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
function mapDispatchToProps(dispatch) {
|
2016-03-30 23:45:54 +02:00
|
|
|
return {
|
|
|
|
actions: bindActionCreators(schemaActions, dispatch),
|
|
|
|
};
|
2016-03-22 00:25:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(FormBuilderComponent);
|