Skip to content

Commit

Permalink
feat(abstract-control): add pristine/dirty flags
Browse files Browse the repository at this point in the history
  • Loading branch information
makarov-roman committed Sep 21, 2018
1 parent f1850bf commit 4fdd504
Show file tree
Hide file tree
Showing 6 changed files with 1,338 additions and 541 deletions.
59 changes: 59 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Mobx Reactive Forms

_Implementation of Angular Reactive forms for mobx applications_

# Installation
`npm i mobx-reactive-forms`
# Usage

## IControlValueAccessor
Everything you need to do - is to wrap your input component with connectForm HOC and pass propagateChange and propagateTouch methods

```typescript jsx
import * as React from 'react'
import {observer} from 'mobx-react'
import {IControlValueAccessor, connectForm} from 'mobx-reactive-form'

@observer
class ReactiveInputComponent extends React.Component<IControlValueAccessor> {
public onChange = (event: SyntheticEvent<HTMLInputElement>) => {
this.props.propagateChange(event.currentTarget.value)
}

public render() {
const control: FormControl = this.props.formControl;
return <input
onChange={this.onChange}
onBlur={this.props.propagateTouch}
disabled={control.disabled}
value={control.value}
/>
}
}

export const ReactiveInput = connectForm(ReactiveInputComponent)
```

## Form
```typescript jsx
@observer
class ExampleForm extends React.Component {
loginForm = new FormGroup({
email: new FormControl('', [isValidEmail]),
password: new FormControl('', [notEmpty])
})
render() {
return <form>
<ReactiveInput formControl={this.loginForm.get('email')} />
<ReactiveInput formControl={this.loginForm.get('password')} />
</form>
}
}
```

## Not Implemented yet
1) AsyncValidators
2) FormArray

## Won't implement
1) FormBuilder
27 changes: 16 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mobx-reactive-forms",
"version": "0.0.2",
"version": "0.0.0-development",
"description": "Mobx implementation of angular reactive forms",
"keywords": [
"mobx",
Expand All @@ -21,7 +21,7 @@
},
"license": "MIT",
"engines": {
"node": ">=6.0.0"
"node": ">=8.0.0"
},
"scripts": {
"lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'",
Expand All @@ -36,7 +36,10 @@
"commit": "git-cz",
"semantic-release": "semantic-release",
"semantic-release-prepare": "ts-node tools/semantic-release-prepare",
"precommit": "lint-staged"
"precommit": "lint-staged",
"prepush": "npm run test:prod && npm run build",
"commitmsg": "commitlint -E GIT_PARAMS",
"travis-deploy-once": "travis-deploy-once"
},
"lint-staged": {
"{src,test}/**/*.ts": [
Expand Down Expand Up @@ -86,7 +89,7 @@
"devDependencies": {
"@commitlint/cli": "^7.0.0",
"@commitlint/config-conventional": "^7.0.1",
"@types/jest": "^22.0.0",
"@types/jest": "23.3.2",
"@types/node": "^10.0.3",
"@types/react": "^16.4.14",
"colors": "^1.1.2",
Expand All @@ -95,26 +98,28 @@
"cross-env": "^5.0.1",
"cz-conventional-changelog": "^2.0.0",
"husky": "^0.14.0",
"jest": "^22.0.2",
"jest": "23.6.0",
"lint-staged": "^7.1.3",
"lodash.camelcase": "^4.3.0",
"mobx": "^5.1.2",
"prettier": "^1.13.4",
"prompt": "^1.0.0",
"replace-in-file": "^3.0.0-beta.2",
"rimraf": "^2.6.1",
"rollup": "^0.59.2",
"rollup": "0.66.2",
"rollup-plugin-commonjs": "^9.0.0",
"rollup-plugin-json": "^3.0.0",
"rollup-plugin-node-resolve": "^3.0.0",
"rollup-plugin-sourcemaps": "^0.4.2",
"rollup-plugin-typescript2": "^0.13.0",
"semantic-release": "^15.0.0",
"ts-jest": "^22.0.0",
"ts-node": "^6.0.0",
"rollup-plugin-typescript2": "0.17.0",
"semantic-release": "^15.9.16",
"travis-deploy-once": "^5.0.8",
"ts-jest": "23.10.1",
"ts-node": "7.0.1",
"tslint": "^5.8.0",
"tslint-config-prettier": "^1.1.0",
"tslint-config-standard": "^7.0.0",
"typedoc": "^0.11.0",
"typedoc": "0.12.0",
"typescript": "^3.0.1"
},
"peerDependencies": {
Expand Down
1 change: 1 addition & 0 deletions src/connectForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const connectForm = <IOriginalProps extends {}>
return class FormConnector extends React.Component<resultProps> {
public childProps: IControlValueAccessor
public propagateChange = (value: any) => {
this.props.formControl.markAsDirty()
this.props.formControl.setValue(value)
}
public propagateTouch = () => {
Expand Down
130 changes: 96 additions & 34 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ export interface IValidationError {
}

export abstract class AbstractControl {
constructor(validators: ValidatorFn<AbstractControl>[]) {
if (!Array.isArray(validators)) {
throw new TypeError(`Expected array of validators. Got ${typeof validators}`)
}
this.validators = observable(validators)
}

@observable
public value: any

Expand All @@ -34,7 +41,7 @@ export abstract class AbstractControl {
* The parent control.
*/
@observable
public parent: AbstractControl | null = null
public parent: FormGroup | null = null

/**
* Returns any errors generated by failing validation. If there
Expand All @@ -50,44 +57,66 @@ export abstract class AbstractControl {
* a `blur` event on it.
*/
@observable
public touched = false
public touched: boolean = false

constructor(validators: ValidatorFn<AbstractControl>[]) {
if (!Array.isArray(validators)) {
throw new TypeError(`Expected array of validators. Got ${typeof validators}`)
}
this.validators = observable(validators)
/**
* A control is `untouched` if the user has not yet triggered
* a `blur` event on it.
*/
@computed
public get untouched(): boolean {
return !this.touched
}

/**
* A control is `valid` when its `status === VALID`.
* Marks the control as `touched`.
*
* In order to have this status, the control must have passed all its
* validation checks.
* This will also mark all direct ancestors as `touched` to maintain
* the model.
*/
@computed
public get valid() {
return this.status === ControlStatusEnum.VALID
@action
public markAsTouched(onlySelf?: boolean): void {
this.touched = true

if (this.parent && !onlySelf) {
this.parent._updateTouched(onlySelf)
}
}

/**
* A control is `invalid` when its `status === INVALID`.
* Marks the control as `untouched`.
*
* In order to have this status, the control must have failed
* at least one of its validation checks.
* If the control has any children, it will also mark all children as `untouched`
* to maintain the model, and re-calculate the `touched` status of all parent
* controls.
*/
@computed
public get invalid() {
return this.status === ControlStatusEnum.INVALID
@action
public markAsUntouched(onlySelf?: boolean): void {
this.touched = false

this._forEachChild((control: AbstractControl) => {
control.markAsUntouched(true)
})

if (this.parent && !onlySelf) {
this.parent._updateTouched(onlySelf)
}
}

/**
* A control is `untouched` if the user has not yet triggered
* a `blur` event on it.
* A control is marked `pristine` unless the user has triggered
* a `change` event on it.
*/
@observable
public pristine: boolean = true

/**
* A control is marked `pristine` once the user has triggered
* a `change` event on it.
*/
@computed
public get untouched() {
return !this.touched
get dirty(): boolean {
return !this.pristine
}

/**
Expand All @@ -97,11 +126,11 @@ export abstract class AbstractControl {
* the model.
*/
@action
public markAsTouched(onlySelf?: boolean): void {
this.touched = true
public markAsPristine(onlySelf?: boolean): void {
this.pristine = true

this._forEachChild((control: AbstractControl) => {
control.markAsTouched(true)
control.markAsPristine(true)
})

if (this.parent && !onlySelf) {
Expand All @@ -117,18 +146,37 @@ export abstract class AbstractControl {
* controls.
*/
@action
public markAsUntouched(onlySelf?: boolean): void {
this.touched = false

this._forEachChild((control: AbstractControl) => {
control.markAsUntouched(true)
})
public markAsDirty(onlySelf?: boolean): void {
this.pristine = false

if (this.parent && !onlySelf) {
this.parent._updateTouched(onlySelf)
this.parent.markAsDirty(onlySelf)
}
}

/**
* A control is `valid` when its `status === VALID`.
*
* In order to have this status, the control must have passed all its
* validation checks.
*/

@computed
public get valid(): boolean {
return this.status === ControlStatusEnum.VALID
}

/**
* A control is `invalid` when its `status === INVALID`.
*
* In order to have this status, the control must have failed
* at least one of its validation checks.
*/
@computed
public get invalid() {
return this.status === ControlStatusEnum.INVALID
}

/**
* A control is `disabled` when its `status === DISABLED`.
*
Expand Down Expand Up @@ -312,7 +360,7 @@ export abstract class AbstractControl {
}

@action
public setParent(parent: AbstractControl): void {
public setParent(parent: FormGroup): void {
this.parent = parent
}

Expand Down Expand Up @@ -343,6 +391,20 @@ export abstract class AbstractControl {
return this._anyControls((control: AbstractControl) => control.touched)
}

/** @internal */
public _anyControlsDirty(): boolean {
return this._anyControls((control: AbstractControl) => control.dirty)
}

/** @internal */
public _updatePristine(opts: { onlySelf?: boolean } = {}): void {
this.pristine = !this._anyControlsDirty()

if (this.parent && !opts.onlySelf) {
this.parent._updatePristine(opts)
}
}

/**
* Sets the value of the control. Abstract method (implemented in sub-classes).
*/
Expand Down
6 changes: 0 additions & 6 deletions test/mobx-reactive-forms.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import DummyClass from '../src/mobx-reactive-forms'

/**
* Dummy test
*/
describe('Dummy test', () => {
it('works if true is truthy', () => {
expect(true).toBeTruthy()
})

it('DummyClass is instantiable', () => {
expect(new DummyClass()).toBeInstanceOf(DummyClass)
})
})
Loading

0 comments on commit 4fdd504

Please sign in to comment.