Skip to content

Commit

Permalink
FEAT: initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
royriojas committed Jun 15, 2016
0 parents commit 02003a4
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"presets": ["es2015", "react", "stage-0"],

"plugins": [
"add-module-exports",
"transform-decorators-legacy",
"transform-react-display-name"
]
}
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.idea/
node_modules/
dist/
*.iml
webpack-stats.json
npm-debug.log
*.orig
coverage/
*.DS_Store
*.py
.cache/
phantomjsdriver.log
lib/
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# [Mobx](https://www.npmjs.com/package/mobx)-Form

Simple helper for state management

TODO:

- Add examples
- Add unit tests
- Add better documentation

## Example:

```javascript
const model = this.model = createModel({
email: '', // initial value for email
password: '', // initial value for password
}, {
// validator for email
email: {
interactive: false,
// validator function
fn(field) {
const email = trim(field.value);
// super simple and naive email validation
if (!email || !(email.indexOf('@') > 0)) {
return Promise.reject({
error: 'Please provide an error message'
});
}
},
},
// validator for password
password: {
interactive: false,
// validator function
fn(field) {
if (!trim(field.value)) {
return Promise.reject({
error: 'Please provide your password'
});
}
},
},
});
```
39 changes: 39 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "mobx-form",
"version": "1.0.0",
"description": "A simple form helper for mobx",
"main": "lib/FormModel.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "babel src/ -d lib/"
},
"repository": {
"type": "git",
"url": "git+https://github.com/royriojas/mobx-form.git"
},
"keywords": [
"form",
"model",
"mobx"
],
"author": "royriojas",
"license": "MIT",
"bugs": {
"url": "https://github.com/royriojas/mobx-form/issues"
},
"homepage": "https://github.com/royriojas/mobx-form#readme",
"dependencies": {
"coalescy": "^1.0.0",
"debouncy": "^1.0.7"
},
"devDependencies": {
"babel-cli": "^6.10.1",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-polyfill": "^6.9.1",
"babel-preset-es2015": "^6.9.0",
"babel-preset-react": "^6.5.0",
"babel-preset-stage-0": "^6.5.0",
"mobx": "^2.3.1"
}
}
97 changes: 97 additions & 0 deletions src/Field.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { observable, computed, isObservableArray } from 'mobx';
import debounce from 'debouncy';
import clsc from 'coalescy';

export default class Field {
@observable _value;
@observable _interacted;
@observable _valid = true;
@observable errorMessage;
@observable interactive = true;

_originalErrorMessage;

@computed get valid() {
return this._valid;
}

get value() {
if (isObservableArray(this._value)) {
return [].slice.call(this._value);
}
return this._value;
}

set value(val) {
if (!this._interacted) {
this._interacted = true;
}

if (this._value === val) {
return;
}

this._value = val;

if (this.interactive) {
this._debouncedValidation();
} else {
this._debounceClearValidation();
}
}

clearValidation() {
this._valid = true;
this.errorMessage = '';
}

validate(force = false) {
if (!this._validateFn) {
return;
}

if (!force && !this._interacted) {
// if we're not forcing the validation
// and we haven't interacted with the field
// we asume this field pass the validation status
this._valid = true;
this.errorMessage = '';
return;
}
const res = this._validateFn(this, this.model.fields);

// if the function returned a boolean we assume it is
// the flag for the valid state
if (typeof res === 'boolean') {
this._valid = res;
this.errorMessage = res ? '' : this._originalErrorMessage;
return;
}

// otherwise we asumme we have received a promise
const p = Promise.resolve(res);
return new Promise((resolve) => { // eslint-disable-line consistent-return
p.then(
() => {
this._valid = true;
this.errorMessage = '';
resolve(); // we use this to chain validators
},
({ error } = {}) => {
this.errorMessage = (error || '').trim() || this._originalErrorMessage;
this._valid = false;
resolve(); // we use this to chain validators
});
});
}

constructor(model, value, validatorDescriptor = {}) {
this.model = model;
this._originalErrorMessage = validatorDescriptor.errorMessage;
this._validateFn = validatorDescriptor.fn || (() => Promise.resolve());
this._debouncedValidation = debounce(this.validate, 300, this);
this._debounceClearValidation = debounce(this.clearValidation, 300, this);
this._value = value;
this.interactive = clsc(validatorDescriptor.interactive, true);
}
}
68 changes: 68 additions & 0 deletions src/FormModel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { observable, computed, extendObservable } from 'mobx';
import Field from './Field';

class FormModel {
@observable fields = {};
@observable validating = false;
@computed get valid() {
if (this.validating) {
return false; // consider the form invalid until the validation process finish
}
const keys = Object.keys(this.fields);
return keys.reduce((seq, key) => {
const field = this.fields[key];
seq = seq && field.valid; // eslint-disable-line no-param-reassign
return seq;
}, true);
}

fieldKeys() {
return Object.keys(this.fields);
}

@computed get summary() {
return this.fieldKeys().reduce((seq, key) => {
const field = this.fields[key];
if (field.errorMessage) {
seq.push(field.errorMessage);
}
return seq;
}, []);
}

validate() {
this.validating = true;

const p = this.fieldKeys().reduce((seq, key) => {
const field = this.fields[key];
return seq.then(() => field.validate(true));
}, Promise.resolve());

p.then(() => (this.validating = false));

return p;
}

toJSON() {
const keys = Object.keys(this.fields);
return keys.reduce((seq, key) => {
const field = this.fields[key];
seq[key] = field.value; // eslint-disable-line no-param-reassign
return seq;
}, {});
}

constructor(initialState = {}, validators = {}) {
const keys = Object.keys(initialState);

keys.forEach((key) => {
extendObservable(this.fields, {
[key]: new Field(this, initialState[key], validators[key]),
});
});
}
}

export function createModel(initialState, validators) {
return new FormModel(initialState, validators);
}

0 comments on commit 02003a4

Please sign in to comment.