From 0589507a1d875132a6bc55a7a79fb86730e530eb Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 20 Apr 2017 16:08:22 -0700 Subject: [PATCH] Add initial store implementation --- docs/src/components/DemoPage.tsx | 58 +++------ docs/src/components/FieldOptionEditor.tsx | 5 + docs/src/components/FieldSelector.tsx | 5 + docs/src/components/FormBuilder.tsx | 5 + docs/src/state/store.ts | 4 + src/components/FormBuilder.tsx | 6 +- src/state/connect.tsx | 42 +++++++ src/state/connectFieldOptionEditor.ts | 18 +++ src/state/connectFieldSelector.ts | 15 +++ src/state/connectFormBuilder.tsx | 18 +++ src/state/index.ts | 4 + src/state/store.ts | 136 ++++++++++++++++++++++ 12 files changed, 272 insertions(+), 44 deletions(-) create mode 100644 docs/src/components/FieldOptionEditor.tsx create mode 100644 docs/src/components/FieldSelector.tsx create mode 100644 docs/src/components/FormBuilder.tsx create mode 100644 docs/src/state/store.ts create mode 100644 src/state/connect.tsx create mode 100644 src/state/connectFieldOptionEditor.ts create mode 100644 src/state/connectFieldSelector.ts create mode 100644 src/state/connectFormBuilder.tsx create mode 100644 src/state/index.ts create mode 100644 src/state/store.ts diff --git a/docs/src/components/DemoPage.tsx b/docs/src/components/DemoPage.tsx index db750a2..aff0e33 100644 --- a/docs/src/components/DemoPage.tsx +++ b/docs/src/components/DemoPage.tsx @@ -1,27 +1,27 @@ import * as React from 'react'; import { Link } from 'react-router-dom'; import { Panel, FormControl, Grid, Row, Col } from 'react-bootstrap'; +import store from '../state/store'; import HTML5Backend from 'react-dnd-html5-backend'; import { DragDropContext } from 'react-dnd'; import { IField, IFieldContext, IFormError, - FieldOptionEditor, - FieldSelector, - FormBuilder, FormInput, FormDisplay, } from 'react-dynamic-formbuilder'; +import FormBuilder from './FormBuilder'; +import FieldSelector from './FieldSelector'; +import FieldOptionEditor from './FieldOptionEditor'; + import './DemoPage.css'; import { FieldRegistry } from './constants'; interface IState { fields: IField[], - editingField: IField, - editingContext: IFieldContext; value: any, error: IFormError, } @@ -31,37 +31,22 @@ class DemoPage extends React.Component { constructor() { super() - this.onChangeFields = this.onChangeFields.bind(this); - this.onFieldEditing = this.onFieldEditing.bind(this); - this.onDeleteField = this.onDeleteField.bind(this); - this.onFieldOptionChanged = this.onFieldOptionChanged.bind(this); - this.onValueChanged = this.onValueChanged.bind(this); this.onBeforeAddField = this.onBeforeAddField.bind(this); + this.onValueChanged = this.onValueChanged.bind(this); + this.getStoreState = this.getStoreState.bind(this); this.state = { - fields: [], - editingField: null, - editingContext: null, + fields: store.getFields(), value: {}, error: null, }; } - private onFieldEditing(field: IField, fieldContext: IFieldContext, done: (field: IField) => void) { - this.setState({ editingField: field, editingContext: fieldContext } as IState); - this.fieldEdited = done; - } - - private onDeleteField(fields: IField[]) { - this.setState({ fields } as IState); - } - - private onChangeFields(fields: IField[]) { - this.setState({ fields } as IState); + componentDidMount() { + store.subscribe(this.getStoreState); } - private onFieldOptionChanged(field: IField) { - this.setState({ editingField: field } as IState); - this.fieldEdited(field); + private getStoreState() { + this.setState({fields: store.getFields()} as IState); } private onValueChanged(value: any, error: IFormError) { @@ -85,34 +70,21 @@ class DemoPage extends React.Component { Field Selector - + Form Builder
- +
Option Editor - + diff --git a/docs/src/components/FieldOptionEditor.tsx b/docs/src/components/FieldOptionEditor.tsx new file mode 100644 index 0000000..a769789 --- /dev/null +++ b/docs/src/components/FieldOptionEditor.tsx @@ -0,0 +1,5 @@ +import store from '../state/store'; +import { connectFieldOptionEditor } from '../../../src/state/connectFieldOptionEditor'; +import { FieldOptionEditor } from 'react-dynamic-formbuilder'; + +export default connectFieldOptionEditor(store)(FieldOptionEditor); \ No newline at end of file diff --git a/docs/src/components/FieldSelector.tsx b/docs/src/components/FieldSelector.tsx new file mode 100644 index 0000000..dc49b7d --- /dev/null +++ b/docs/src/components/FieldSelector.tsx @@ -0,0 +1,5 @@ +import store from '../state/store'; +import { connectFieldSelector } from '../../../src/state/connectFieldSelector'; +import { FieldSelector } from 'react-dynamic-formbuilder'; + +export default connectFieldSelector(store)(FieldSelector); \ No newline at end of file diff --git a/docs/src/components/FormBuilder.tsx b/docs/src/components/FormBuilder.tsx new file mode 100644 index 0000000..0ba5d10 --- /dev/null +++ b/docs/src/components/FormBuilder.tsx @@ -0,0 +1,5 @@ +import store from '../state/store'; +import { connectFormBuilder } from '../../../src/state/connectFormBuilder'; +import { FormBuilder } from 'react-dynamic-formbuilder'; + +export default connectFormBuilder(store)(FormBuilder); \ No newline at end of file diff --git a/docs/src/state/store.ts b/docs/src/state/store.ts new file mode 100644 index 0000000..d55c97b --- /dev/null +++ b/docs/src/state/store.ts @@ -0,0 +1,4 @@ +import { FieldRegistry } from '../components/constants'; +import { createStore } from '../../../src/state/store'; + +export default createStore({registry: FieldRegistry}); \ No newline at end of file diff --git a/src/components/FormBuilder.tsx b/src/components/FormBuilder.tsx index d42d23c..c1c4080 100644 --- a/src/components/FormBuilder.tsx +++ b/src/components/FormBuilder.tsx @@ -6,7 +6,9 @@ import { FormBuilderEditable as Editable } from './FormBuilderEditable'; import { FormBuilderDroppable as Droppable } from './FormBuilderDroppable'; import { FormBuilderContext } from './FormBuilderContext'; -export interface IFormBuilderProps { +export interface IFormBuilderProps extends IFormBuilderRequiredProps, IFormBuilderOptionalProps { +} +export interface IFormBuilderRequiredProps { fields: data.IField[]; // registry contains a map of field types to classes. FormBuilder @@ -19,7 +21,9 @@ export interface IFormBuilderProps { // fieldEditing is called when the user want to edit field options. onFieldEditing: (field: data.IField, fieldContext: data.IFieldContext, done: (field: data.IField) => void) => void; +} +export interface IFormBuilderOptionalProps { // onBeforeAddField is called before add the new field into the array. // If this method returns false, onChange will not be called. onBeforeAddField?: (field: data.IField) => boolean; diff --git a/src/state/connect.tsx b/src/state/connect.tsx new file mode 100644 index 0000000..85eeb70 --- /dev/null +++ b/src/state/connect.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { IStore } from './store'; + +export interface MapStateFunc { + (store: IStore): T; +} + +export interface ConnectComponent { + (WrappedComponent: React.ComponentClass): React.ComponentClass; +} + +// connect works similar to react-redux. It takes in a store and a function +// to map the state. This function should not be consumed directly because. +// Instead use a helper function: connectFormBuilder, connectFieldSelector, connectFieldOptionEditor. +export function connect(store: IStore, mapState: MapStateFunc): ConnectComponent { + return (WrappedComponent: React.ComponentClass) => { + return class Connected extends React.Component { + constructor() { + super(); + this.getState = this.getState.bind(this); + this.state = mapState(store) as WrappedComponentProps; + store.subscribe(this.getState); + } + + private getState() { + this.setState(mapState(store)); + } + + componentDidMount() { + store.subscribe(this.getState); + } + + componentWillUnmount() { + store.unsubscribe(this.getState); + } + + render() { + return ; + } + } + } +} diff --git a/src/state/connectFieldOptionEditor.ts b/src/state/connectFieldOptionEditor.ts new file mode 100644 index 0000000..b6bb792 --- /dev/null +++ b/src/state/connectFieldOptionEditor.ts @@ -0,0 +1,18 @@ +import { connect } from './connect'; +import { IStore } from './store'; +import { IFieldOptionEditorComponentProps } from '../components/FieldOptionEditor'; + +export function connectFieldOptionEditor(store: IStore) { + return (FieldOptionEditor: React.ComponentClass): React.ComponentClass => { + return connect(store, connectStoreToFieldOptionEditor)(FieldOptionEditor) as React.ComponentClass; + } +} + +export function connectStoreToFieldOptionEditor(store: IStore): IFieldOptionEditorComponentProps { + return { + field: store.getEditingField(), + fieldContext: store.getEditingContext(), + registry: store.getRegistry(), + onChange: store.onFieldChanged, + } +} \ No newline at end of file diff --git a/src/state/connectFieldSelector.ts b/src/state/connectFieldSelector.ts new file mode 100644 index 0000000..f79319b --- /dev/null +++ b/src/state/connectFieldSelector.ts @@ -0,0 +1,15 @@ +import { connect } from './connect'; +import { IStore } from './store'; +import { IFieldSelectorProps } from '../components/FieldSelector'; + +export function connectFieldSelector(store: IStore) { + return (FieldSelector: React.ComponentClass): React.ComponentClass => { + return connect(store, connectStoreToFieldSelector)(FieldSelector) as React.ComponentClass; + } +} + +export function connectStoreToFieldSelector(store: IStore): IFieldSelectorProps { + return { + registry: store.getRegistry(), + } +} \ No newline at end of file diff --git a/src/state/connectFormBuilder.tsx b/src/state/connectFormBuilder.tsx new file mode 100644 index 0000000..e58180e --- /dev/null +++ b/src/state/connectFormBuilder.tsx @@ -0,0 +1,18 @@ +import { connect } from './connect'; +import { IStore } from './store'; +import { IFormBuilderOptionalProps, IFormBuilderRequiredProps, IFormBuilderProps } from '../components/FormBuilder'; + +export function connectFormBuilder(store: IStore) { + return (FormBuilder: React.ComponentClass): React.ComponentClass => { + return connect(store, connectStoreToFormBuilder)(FormBuilder) as React.ComponentClass; + } +} + +export function connectStoreToFormBuilder(store: IStore): IFormBuilderRequiredProps { + return { + fields: store.getFields(), + registry: store.getRegistry(), + onChange: store.onChangeFields, + onFieldEditing: store.onFieldEditing, + } +} \ No newline at end of file diff --git a/src/state/index.ts b/src/state/index.ts new file mode 100644 index 0000000..d53da70 --- /dev/null +++ b/src/state/index.ts @@ -0,0 +1,4 @@ +export * from './connectFormBuilder'; +export * from './connectFieldSelector'; +export * from './connectFieldOptionEditor'; +export * from './store'; \ No newline at end of file diff --git a/src/state/store.ts b/src/state/store.ts new file mode 100644 index 0000000..e221a4a --- /dev/null +++ b/src/state/store.ts @@ -0,0 +1,136 @@ +import { IFieldContext, IField, FieldRegistry } from '../data'; + +export interface ISubscriber { + (): void +} + +export interface FieldEditingHandler { + (field: IField): void; +} + +// createStore is a helper function for initializing +// the store. +export function createStore(props: IStoreProps) { + return new Store(props); +} + +export interface IStore { + // subscribe pushes the subscriber into a list and is called + // via notify() until unsubscribed. + subscribe(subscriber: ISubscriber): void; + + // unsubscribe removes the subscriber. + unsubscribe(subscriber: ISubscriber): void; + + // getFields returns the list of fields that is being developed. + getFields(): IField[]; + + // getEditingField returns the field that is selected for editing. + getEditingField(): IField; + + // getEditingContext returns a structure that contains the fields + // that are parent to the editing field. + getEditingContext(): IFieldContext; + + // getRegistry returns the field registry. + getRegistry(): FieldRegistry; + + // onChangeFields will change the internal state of the fields + // any notify subscribers. + onChangeFields(fields: IField[]): void; + + // onFieldEditing takes in the field and context selected for editing + // and also keeps a reference to the done callback. + onFieldEditing(field: IField, context: IFieldContext, done: FieldEditingHandler): void; + + // onFieldChanged should be called whenever the option editor has updated + // a field. It will notify subscribers. + onFieldChanged(field: IField): void; +} + +export class Store implements IStore { + // done is the callback that should be called once + // the option editor has modified a field + private done: FieldEditingHandler; + + // subscribers is a list of functions to call + // when a change has been made to the state. + private subscribers: ISubscriber[] = []; + + // registry is a field registry that can be consumed + // by any components that need to lookup by type. + private registry: FieldRegistry; + + // fields contains a list of fields for representing + // the form. + private fields: IField[] = []; + + // editingField is the field selected for editing. + private editingField: IField = null; + + // editingContext holds list of fields the editingFields + // are apart of. + private editingContext: IFieldContext = { + fields: [] + }; + + constructor(props: IStoreProps) { + const { registry } = props; + this.registry = registry; + this.onChangeFields = this.onChangeFields.bind(this); + this.onFieldEditing = this.onFieldEditing.bind(this); + this.onFieldChanged = this.onFieldChanged.bind(this); + } + + public getFields(): IField[] { + return this.fields; + } + + public getEditingField(): IField { + return this.editingField; + } + + public subscribe(subscriber: ISubscriber): void { + this.subscribers.push(subscriber); + } + + public unsubscribe(subscriber: ISubscriber): void { + const index = this.subscribers.indexOf(subscriber); + if (index >= 0) { + this.subscribers.splice(index, 1); + } + } + + public getEditingContext(): IFieldContext { + return this.editingContext; + } + + public getRegistry(): FieldRegistry { + return this.registry; + } + + public onChangeFields(fields: IField[]): void { + this.fields = fields; + this.notify(); + } + + public onFieldEditing(field: IField, context: IFieldContext, done: FieldEditingHandler) { + this.editingContext = context; + this.editingField = field; + this.done = done; + this.notify(); + } + + public onFieldChanged(field: IField): void { + this.editingField = field; + this.done(field); + } + + private notify(): void { + this.subscribers.forEach(notify => notify()); + } +} + +export interface IStoreProps { + registry: FieldRegistry; +} \ No newline at end of file