import React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import * as schemaActions from 'state/schema/actions'; 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'; import fetch from 'isomorphic-fetch'; import es6promise from 'es6-promise'; es6promise.polyfill(); // Using this to map field types to components until we implement dependency injection. 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 'Text': // Textarea field (not implemented) return null; case 'HTML': // HTML editor field (not implemented) return null; case 'Integer': // Numeric field (not implemented) return null; case 'Decimal': // Numeric field (not implemented) return null; case 'MultiSelect': // Radio field (not implemented) return null; case 'SingleSelect': // Dropdown field (not implemented) return null; case 'Date': // DateTime field (not implemented) return null; case 'DateTime': // DateTime field (not implemented) return null; case 'Time': // DateTime field (not implemented) return null; case 'Boolean': // Checkbox field (not implemented) return null; case 'Custom': return this.components.GridField; default: return null; } }, }; export class FormBuilderComponent extends SilverStripeComponent { constructor(props) { super(props); this.formSchemaPromise = null; this.state = { isFetching: false }; } componentDidMount() { 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 = []; if (this.state.isFetching === true) { return this.formSchemaPromise; } if (schema === true) { headerValues.push('schema'); } if (state === true) { headerValues.push('state'); } this.formSchemaPromise = fetch(this.props.schemaUrl, { headers: { 'X-FormSchema-Request': headerValues.join() }, credentials: 'same-origin', }) .then(response => response.json()) .then(json => { this.setState({ isFetching: false }); this.props.actions.setSchema(json); }); this.setState({ isFetching: true }); 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. const props = { attributes: field.attributes, data: field.data, description: field.description, extraClass: field.extraClass, fields: field.children, id: field.id, name: field.name, }; // Structural fields (like TabSets) are not posted back to // the server and don't receive some props. if (field.type !== 'Structural') { props.rightTitle = field.rightTitle; props.leftTitle = field.leftTitle; props.readOnly = field.readOnly; props.disabled = field.disabled; props.customValidationMessage = field.customValidationMessage; } // Dropdown and Radio fields need some options... if (field.type === 'MultiSelect' || field.type === 'SingleSelect') { props.source = field.source; } return ; }); } render() { const schema = this.props.schemas[this.props.schemaUrl]; // If the response from fetching the initial data // hasn't come back yet, don't render anything. if (!schema) { return null; } const formProps = { actions: schema.schema.actions, attributes: schema.schema.attributes, data: schema.schema.data, fields: schema.schema.fields, mapFieldsToComponents: this.mapFieldsToComponents, }; return ; } } FormBuilderComponent.propTypes = { actions: React.PropTypes.object.isRequired, schemaUrl: React.PropTypes.string.isRequired, schemas: React.PropTypes.object.isRequired, }; function mapStateToProps(state) { return { schemas: state.schemas, }; } function mapDispatchToProps(dispatch) { return { actions: bindActionCreators(schemaActions, dispatch), }; } export default connect(mapStateToProps, mapDispatchToProps)(FormBuilderComponent);