frintjs / frint

Modular JavaScript framework for building scalable and reactive applications
https://frint.js.org/
MIT License
756 stars 37 forks source link

Proposal: frint-data-validation #311

Closed fahad19 closed 6 years ago

fahad19 commented 7 years ago

Now that we have frint-data (#305), we can start thinking about validation for models.

This proposal aims to provide the use cases for form validation involving:

and, the proposed new package:

Model

A model can be created as follows:

// models/Todo.js
import { createModel, Types } from 'frint-data';

const Todo = createModel({
  schema: {
    title: Types.string,
  },
  setTitle(title) {
    this.title = title;
  },
});

export default Todo;

Connecting to a Component

Form handling:

// components/TodoForm.js
import React from 'react';
import { observe, streamProps } from 'frint-react';

import Todo from '../models/Todo';

function TodoForm(props) {
  const { 
    title, 
    updateTitle,
  } = props;

  return (
    <div>
      <input
        type="text"
        value={title}
        onChange={e => updateTitle(e.target.value)}
      />
    </div>
  );
}

export default observe(function () {
  const todo = new Todo({
    title: 'Hello World',
  });

  return streamProps()
    .set(todo.get$(), t => ({ todo: t }))
    .set('updateTitle', title => todo.setTitle(title))
    .get$();
})(TodoForm);

Validations

This is the missing layer that we can standardize with a package that works with frint-data:

// models/Todo.js
import { Types, createModel } from 'frint-data';
import { Rules } from 'frint-data-validation';

const Todo = createModel({
  schema: {
    title: Types.string
  },
});

// this is optional
Todo.validationRules = [
  // built-in rules for fields
  {
    rule: Rules.notEmpty,
    field: 'title',
    message: 'empty message',
    name: 'titleIsEmpty', // unique name for extracting error messages (optional)
  },

  // custom rules - not tied to fields
  {
    rule: function (model) {
      if (model.title.length === 0) {
        return false;
      }

      return true;
    },
    message: 'You are really empty',
    name: 'titleIsEmpty2', // unique name (optional)
  },
];

export default Todo;

Now to stream validation errors upon changes:

import { validate$ } from 'frint-data-validation';

import Todo from 'models/Todo';

const todo = new Todo({
  title: 'Hi',
});

// validate against default rules set at class level
validate$(todo).subscribe(errors => {
  console.log(errors);
  // [
  //   {
  //     name: 'titleIsEmpty',
  //     message: 'empty message',
  //   }
  // ]
});

// validate against a different set of rules:
validate$(todo, myRulesHere).subscribe(errors => console.log(errors));

Connect validation rules to Component

import React from 'react';
import { observe } from 'frint-react';
import { validate$ } from 'frint-data-validation';

function TodoForm(props) {
  const {
    title,
    validationErrors,
    updateTitle
  } = props;

  // ...

  return <JSX />;
}

export default observe(function () {
  const todo = new Todo({
    title: 'Hello World',
  });

  return streamProps()
    .set(
      todo.get$(), 
      t => ({ todo: t })
    )
    .set('updateTitle', title => todo.setTitle(title))
    .set(
      validate$(todo),
      validationErrors => ({ validationErrors })
    )
    .get$();
})(TodoForm);

Async validation

import { validate$ } from 'frint-data-validation';

import Todo from './models/Todo';

const todo = new Todo({
  title: 'Hello world',
});

validate$(todo, [
  {
    rule: function (model) {
      // return Promise or Observable (or only one?)
      return fetch(`/todo/exists?title=${todo.title}`)
        .then(res => res.json()) // { exists: true }
        .then(body => body.exists === false);
    },
    message: 'Todo of the same title exists in server'
  }
])
  .subscribe(function (errors) {
    console.log(errors);
  });
fahad19 commented 6 years ago

I have started on this in the frint-data-validation branch here: https://github.com/frintjs/frint/compare/frint-data-validation?expand=1

API will evolve further (for the better) based on the spec here.