Skip to content

Commit

Permalink
Full support for wizzard type forms
Browse files Browse the repository at this point in the history
  • Loading branch information
viktor.shevchenko committed Jan 17, 2018
1 parent 7ca3ee4 commit 0d0f9e9
Show file tree
Hide file tree
Showing 13 changed files with 163 additions and 97 deletions.
39 changes: 8 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ This requires more investigation....
## Installation

```
npm install reactive-mobx-form --save
npm install reactive-mobx-form --save // you can omit --save flag if using npm > 5
```

## Usage
Expand Down Expand Up @@ -101,15 +101,11 @@ class ContactForm extends Component {

const ContactFormReactive = reactiveMobxForm('contacts' [, formDefinition])(ContactForm); // 2nd parameter (formDefinition) is optional.

// Use 2nd parameter to specify
// - predefined initial values and validation rules, see format below.
// - configuration for validation mechanism
// - specify custom error messages
// If you get initial values from server, better pass them as 'schema' attribute to Form Component in parent component

export default ContactFormReactive;
```

Detailed explanation of [formDefinition](https://vict-shevchenko.github.io/reactive-mobx-form/#/api/reactiveMobxForm()) object

### Step 3
Use your form and enjoy

Expand All @@ -129,29 +125,8 @@ export default Page extends Component {
}
```

## formDefinition
`formDefinition` is an optional 2nd paramerer to `reactiveMobxForm` initialization function. Now it support 2 optional properties

```javascript
{
validator: {
errorMessages: {},
attributeNames: {}
},
schema: {},
}
```

## schema
`schema` is an object with a configuration for form fields, allowing to specify their initialValues and validation rules.

Syntax is next:

Wihtout validation:
`{firstName: 'Vikor'}` is same as `{firstName: ['Viktor']}` and same as `{firstName: ['Viktor', '']}`

With validation:
`{firstName: ['Viktor', 'required|string']}` uses **validatorjs** syntax.
## How form submition is happening.
When you call `submit` function form `props` passed to your form - submition is started. Your `submit` function (those you have passed into `onSubmit` parameter) will be called inside of promise(so it may be async). If your `submit` function returns a `resolved Promise` - `form.reset` will be called to drop form state to initial one. If your `submit` function returns `rejectedPromise` than `form.submitionError` flag is raised and form keeps its state untouched.

## Language Support
By default error messages are in English. But you can change them. `reactive-mobx-form` provides you with interface for this. Under the hood it uses [Validatorjs Language Support](https://github.com/skaterdav85/validatorjs#language-support)
Expand Down Expand Up @@ -213,4 +188,6 @@ const ContactFormReactive = reactiveMobxForm('contacts', {
}
}
})(ContactForm)
```
```

##[FAQ](https://github.com/vict-shevchenko/reactive-mobx-form/blob/master/docs/FAQ.md)
41 changes: 40 additions & 1 deletion docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,43 @@
### Inspirations
1. Redux form
2. Angular 2 reactive forms
3. Pain
3. Pain

### How form submition is happening.
When you call `submit` function form `props` passed to your form - submition is started. Your `submit` function (those you have passed into `onSubmit` parameter) will be called inside of promise(so it may be async). If your `submit` function returns a `resolved Promise` - `form.reset` will be called to drop form state to initial one. If your `submit` function returns `rejectedPromise` than `form.submitionError` flag is raised and form keeps its state untouched.

### I have an `input type="hidden"` and want to set its value after form was initialized.
The best solution I see here is use native Mobx capabilities. As Mobx is extremely good at deriving things from state.

Lets assume we have Control in our form

```javascript
<Control type="hidden" name="computedProperty" component="input" />
```

You can either update field value manually
```javascript
someOtherFieldOnBlur(event) {
const value = event.target.value.trim();

const form = this.props.formStore.getForm('myForm');
const computedPropertyField = form.getField(['computedProperty']);
computedPropertyField.onChange(value);
}
```

Or use `reaction` method from `mobx`

```javascript
componentWillMount() {
this.disposer = reaction(
() => this.props.myOtherStore.someComputedValue, // or value may arrive from server
(someComputedValue) => {
const form = this.props.formStore.getForm('myForm');
const computedPropertyField = form.getField(['computedProperty']);
computedPropertyField.onChange(someComputedValue);
},
);
}
// Do not forget to call this.disposer() in componentWillUnmount
```
3 changes: 3 additions & 0 deletions docs/api/props.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ Function to execute when form needs to be submitted. Will call **`onSubmit`** pa
**`reset` : function**
Function to return form to initial state. Input fiels are returned to their initial values. Control Arrays are returned to initial amount if were added.

**`destroy` : function**
Function to manually unregister form in `formStore`.

**`submitting` : boolean**
Flag that is raised when form is submitting, useful if your submission function is async

Expand Down
38 changes: 30 additions & 8 deletions docs/api/reactiveMobxForm().md
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,36 @@ High Order Component that is used to create reactiveMobXForm, set its form param
## Usage
```javascript
import {reactiveMobxForm } from 'reactive-mobx-form';

...

export reactiveMobxForm('myForm')(MyFormComponent);
...
// Basic Usage
export reactiveMobxForm('myForm')(MyFormComponent);

//Advances usage
export reactiveMobxForm('myForm' [,formDefinition])(MyFormComponent);
```


## Parmeters

`reactiveMobxForm` accepts 2 parameters string `name` and `formDefiniton` object.
`reactiveMobxForm` accepts 2 parameters
1. Required `name`
2. Optional `formDefiniton` object.

### Required

`name : String` - The name of your form, which will be used as a key to store your form data in `FormStore`

### Optional

`formDefiniton : Object` object with additonal form paramters. Shape looks like
`formDefiniton : Object` object with additional form parameters.
Use this parameter to specify:
- predefined initial values and validation rules, see format below.
- configuration for validation mechanism
- specify custom error messages
- specify parameters for form behavior

Shape looks like:

```javascript
{
Expand All @@ -34,8 +47,11 @@ High Order Component that is used to create reactiveMobXForm, set its form param
attributeNames: {}
},
schema: {},
destroyFormStateOnUnmount: true,
destroyControlStateOnUnmount: true
}
```

#### `validator`

Property is responsible to set up how form validation will be performed, and is represented by 2 properties. Both are applied per form instance.
Expand All @@ -46,7 +62,7 @@ Property is responsible to set up how form validation will be performed, and is
You can find usege in [examples](/reactive-mobx-form/#/examples).
#### `schema`

An object with a configuration for form fields, allowing to specify their initialValues and validation rules on a form creation stage. (you can also do this on form rendering sate by passing `schema` parameter to your `ReactiveForm` component).
An object with a configuration for form fields, allowing to specify their initialValues and validation rules on a form creation stage. (you can also do this on form rendering sate by passing `schema` parameter to your `ReactiveForm` component). Rule syntax is taken from awesome [validatorjs](https://github.com/skaterdav85/validatorjs) library.

```javascript
// this three forms of schema definition are equal,
Expand All @@ -60,6 +76,12 @@ An object with a configuration for form fields, allowing to specify their initia
{firstName: ['Viktor', 'required|string']}
```

#### `unregisterOnUnmount : boolean`
**Hint: If you get initial values from server, better pass them as 'schema' attribute to generated ReactiveForm Component in parent component**

#### `destroyFormStateOnUnmount : boolean`

Should or should not the form state be cleaned from `formStore` when Form component is unmounted. It may be useful if you have some part of form appear dynamically. And would like not to remove all form State, when this part disappear. For example Wizard form. Defaults to `true`.

#### `destroyControlStateOnUnmount : boolean`

Should or should not the form state be cleaned form `formStore` when form component is unmounted
Should or should not the Control state be cleaned from FormState when Control component is unmounted. `reactive-mobx-form` is designed in a way, that onSubmit, your formState contains only fields that are presented on screen. That allows to omit writing a model of a form in JavaScript (like in Angular 2) and actually construct a model on a fly, based on Control components that are rendered. Defaults to `true`.
4 changes: 2 additions & 2 deletions src/FieldArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ export class FieldArray {
this.subFields.clear();
}

@action public removeSubField(index: string) {
@action public removeField(index: string) {
this.subFields.splice(parseInt(index, 10), 1);
}

public getField(index: string): formField | undefined {
public selectField(index: string): formField | undefined {
// Avoid mobx.js:1905 [mobx.array] Attempt to read an array index (0) that is out of bounds (0).
// Please check length first. Out of bound indices will not be tracked by MobX
return (this.subFields.length > parseInt(index, 10)) ? this.subFields[index] : undefined;
Expand Down
4 changes: 2 additions & 2 deletions src/FieldSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ export class FieldSection {
this.subFields.values().forEach(subField => subField.reset());
}

@action public removeSubField(index: string) {
@action public removeField(index: string) {
this.subFields.delete(index);
}

public getField(index: string) {
public selectField(index: string) {
return (this.subFields as ObservableMap<formField>).get(index);
}

Expand Down
75 changes: 44 additions & 31 deletions src/Form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class Form {
return new Validator(this.values, this.rules, this.errorMessages);
}

// todo: values are recomputed each time field is registered, think if this is good begavior for form initialization
// todo: values are recomputed each time field is registered, think if this is good behavior for form initialization
@computed get values() {
// return this.fields.entries().map(entry =>
// ({ [entry[0]]: entry[1].value })).reduce((val, entry) => Object.assign(val, entry), {});
Expand All @@ -59,62 +59,66 @@ export class Form {
return this.fields.values().reduce((rules, field) => Object.assign(rules, field.rules), {});
}

@action public reset() {
this.fields.forEach(field => field.reset());
}

@action public registerField(field: formField): void {
const fieldPath = objectPath(field.name);

try {
const existField = this.findFieldInHierarchy(fieldPath);
const parentField = this.findFieldInHierarchy(fieldPath.slice(0, fieldPath.length - 1));
const existField = this.getField(fieldPath);
const fieldParent = this.getFieldParent(fieldPath);

if (existField) {
throw (new Error(`Field with name ${(existField as formField).name} already exist in Form. `));
}
else {
(parentField as FieldArray | FieldSection | Form).addField(field);
(fieldParent as FieldArray | FieldSection | Form).addField(field);
}

}
catch (e) {
console.log(`Field ${field.name} can't be registred. Check name hierarchy.`, e); // tslint:disable-line
console.warn(`Field ${field.name} can't be registered. Check name hierarchy.`, e); // tslint:disable-line
}
}

// in React ComponentWillUnmount is fired from parent to child, so if no parent exist -> it was already unmounted.
// No need to clean-up children
@action public unregisterField(fieldName: string) {
const fieldPath = objectPath(fieldName),
lastIndex = fieldPath.length - 1,
lastNode = fieldPath[lastIndex],
fieldParent = this.getFieldParent(fieldPath);

if (fieldParent) {
(fieldParent as Form | FieldArray | FieldSection).removeField(lastNode);
}
else {
console.log('Attempt to remove field on already removed parent field', fieldName) // tslint:disable-line
}
}

@action public addField(field: formField): void {
this.fields.set(field.name, field);
}

@action public removeField(fieldName: string) {
const fieldPath = objectPath(fieldName);

if (fieldPath.length === 1) { // this is form.fields first child
(this.fields.get(fieldName) as formField).setAutoRemove();
this.fields.delete(fieldName);
@action public getField(fieldPath: string | string[]): formField {
try {
return this.findFieldInHierarchy(Array.isArray(fieldPath) ? fieldPath : objectPath(fieldPath));
}
else { /* tslint:disable: indent */ // this is some nested field
const lastIndex = fieldPath.length - 1,
lastNode = fieldPath[lastIndex],
parentField = this.findFieldInHierarchy(fieldPath.slice(0, lastIndex));
/* tslint:enable: indent */
// in React ComponentWillUnmount is fired from parent to child, so if no parent exist -> it was already unmounted.
// No need to clean-up children
if (parentField) {
(parentField as FieldArray | FieldSection).removeSubField(lastNode);
}
catch (e) {
console.warn(`Field can't be selected. Check name hierarchy. Probably some field on the chain does not exist`, e); // tslint:disable-line
}
}

@action public reset() {
this.fields.forEach(field => field.reset());
}

public getField(index: string): formField {
return (this.fields as ObservableMap<formField>).get(index);
@action public removeField(fieldName: string): void {
(this.fields.get(fieldName) as formField).setAutoRemove();
this.fields.delete(fieldName);
}

public findFieldInHierarchy(path: string[]): Form | formField {
// todo: f ? f.getField(node) : f - is super stupid check for parent was removed,
// just pass udefined for all suc childrens
return path.reduce((f: Form | FieldArray | FieldSection, node) => f ? f.getField(node) : f, this);
public selectField(fieldName: string): formField {
return this.fields.get(fieldName);
}

public registerValidation() {
Expand All @@ -129,4 +133,13 @@ export class Form {
}
);
}

private getFieldParent(path: string[]): Form | FieldArray | FieldSection {
return path.length === 1 ? this : (this.getField(path.slice(0, path.length - 1)) as FieldArray | FieldSection);
}

private findFieldInHierarchy(path: string[]): formField {
// f is Form initially, and formField after, can`t handle type error
return path.reduce((f: any, node) => (f as Form | FieldArray | FieldSection).selectField(node), this);
}
}
Loading

0 comments on commit 0d0f9e9

Please sign in to comment.