Reactive data modelling package for Frint
With npm:
$ npm install --save frint-data
Via unpkg CDN:
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.0/Rx.min.js"></script>
<script src="https://unpkg.com/frint-data@latest/dist/frint-data.min.js"></script>
<script>
// available as `window.FrintData`
</script>
Model
: Object-like data structuresCollection
: Array-like data structuresSchema
: The structure of a ModelType
: Type of individual keys in a Model's SchemaLet's first import the dependencies:
import { Types, createModel, createCollection } from 'frint-data';
// Individual Todo items can be Models
const Todo = createModel({
// schema
schema: {
title: Types.string, // or, Types.string.isRequired
completed: Types.bool, // or, Types.bool.defaults(false)
},
// custom methods
setTitle(newTitle) {
this.title = newTitle;
},
});
// a group of Todo models can be put in a Todos collection
const Todos = createCollection({
model: Todo,
addTodo(todo) {
return this.push(todo);
},
extractLast() {
return this.pop();
},
});
// model
const todo = new Todo({
title: 'First task',
completed: false,
});
// collection
const todos = new Todos();
todos.addTodo(todo);
// access properties
console.log(todo.title); // `First task`
// properties are immutable
todo.title = 'First task title changed';
console.log(todo.title); // still `First task`
// mutate them via methods only
todo.setTitle('First task [updated]');
console.log(todo.title); // `First task [updated]`
// lets add the model to collection
todos.addTodo(todo);
console.log(todos.length); // `1`
todos.addTodo(new Todo({
title: 'My second task',
completed: false
}));
console.log(todos.length); // `2`
// let's take the last model out of the collection
const lastTodo = todos.extractLast();
console.log(lastTodo); // `My second task`
console.log(todos.length); // `1`
import { map } from 'rxjs/operators/map';
// model
const todoTitle$ = todo.get$()
.pipe(
map(model => model.title)
);
todoTitle$.subscribe(function (title) {
console.log(title); // will stream as the Model changes
});
// collection
todos.get$().subscribe(function (collection) {
console.log('collection was changed');
});
One of the main features of frint-data
is that it is strictly based on typed values.
A list of type expressions are already shipped with the package and you can import them as follows:
import { Types } from 'frint-data';
Some of the types include:
Types.string
Types.number
Types.enum
If you want to check the type of a value that you expect to be a string:
const checkIfString = Types.string;
Now the checkIfString
function would return a string if a correct valid string was passed to it, otherwise it would throw a new TypesError
.
const str = checkIfString('Hello World'); // returns `Hello World`
const foo = checkIfString([1, 2, 3]); // throws TypesError
Type expressions can also be chained:
const checkIfString = Types.string.isRequired;
checkIfString('hello world'); // returns `hello world`
checkIfString(); // throws TypesError: value is undefined
Available chained expressions:
isRequired
defaults(defaultValue)
Models are objects that represent data. A model can hold data in the form of regular strings, booleans, and even embed other Models and Collections.
When creating a Model class, we need to pass a schema (formed of Types
):
import { Types, createModel } from 'frint-data';
const Todo = createModel({
schema: {
title: Types.string.isRequired,
completed: Types.bool.defaults(false),
},
});
Here, we are creating a new Model class for Todo, and we are providing a schema to it saying title
is a required string, and completed
is a boolean value that defaults to false
.
const todo = new Todo({
title: 'My new todo item'
});
Since completed
would default to false, we don't need to pass it during instantiation. But if we didn't provide the title
, it would throw a new TypesError
.
You can get the values from your model instance just like you would do with a regular plain object:
const title = todo.title; // `My new todo item`
Model properties are immutable by default. And you can only change them via methods that you define while creating the Model class in the beginning.
todo.title = 'Changing the title'; // has no impact
console.log(todo.title); // still `My new todo item`
To change any property values, lets extend your Model class a bit more, by passing a new method in createModel()
:
const Todo = createModel({
schema: {
title: Types.string.isRequired,
completed: Types.bool.defaults(false),
},
// custom method
setTitle(newTitle) {
this.title = newTitle;
},
});
Now from instance level, we can change the title:
todo.setTitle('Changing the title');
console.log(todo.title); // `Changing the title`
Collections are arrays of Models, and to be more precise, they contain Models of a specific single class.
For example a Todos
collection consisting of only Todo
models.
import { createCollection } from 'frint-data';
const Todos = createCollection({
model: Todo, // passing the Todo model class
});
// empty instance
const todos = new Todos();
// instance with some models data
const todos = new Todos([
{ title: 'First task' }, // will be converted to Todo instances
{ title: 'Second task'}
]);
// instance with some model instances
const todos = new Todos([
new Todo({ title: 'First task' }),
new Todo({ title: 'Second task' })
]);
You can make new methods available to your collection instances as follows:
const Todos = createCollection({
model: Todo,
// custom method
doSomething() {
return true;
},
});
Collection instances also come with built-in methods like map
, filter
, reduce
just like Array
. See more in API Reference.
Collections are immutable by default. If you want to use built-in methods that mutate the collection, then you have to do them by defining custom methods first:
const Todos = createCollection({
model: Todo,
addTodo(todo) {
// `push` and other mutating methods are only available inside custom methods
return this.push(todo);
},
});
const todos = new Todos();
todos.addTodo(new Todo({ title: 'First task' })); // works
// this will NOT work
todos.push(new Todo({ title: 'Another task' }));
Models can embed other Models and Collections, and this can go as many levels deep as the data structure demands.
Let's say we have an individual Address
model:
import { Types, createModel } from 'frint-data';
const Address = createModel({
schema: {
street: Types.string.isRequired,
city: Types.string.isRequired,
},
setStreet(street) {
this.street = street;
},
});
And we also have a Person
model:
const Person = createModel({
schema: {
name: Types.string.isRequired,
},
});
If we wish to embed Address
model in a new schema key address
in Person
model, we can do:
const Person = createModel({
schema: {
name: Types.string.isRequired,
address: Types.model.of(Address).isRequired,
},
});
Next, if we instantiate a new Person
:
const person = new Person({
name: 'Sirius Black',
address: {
street: '12 Grimmauld Place',
city: 'London'
}
});
// `person` is an instance of Person
// `person.address` is an instance of Address
To change the street name of the address, we would access the method as:
person.address.setStreet('New street name');
From previous examples, we already have a Person
model. Now let's say, a Person
has a collection of Books
.
We can define the classes as follows:
import { Types, createModel, createCollection } from 'frint-data';
const Book = createModel({
schema: {
title: Types.string.isRequired,
},
});
const Books = createCollection({
model: Book,
});
const Person = createModel({
schema: {
name: Types.string.isRequired,
books: Types.collection.of(Books),
},
});
When instantiating a Person
, we can optionally pass books data too:
const person = new Person({
name: 'Bathilda Bagshot',
books: [
{ title: 'A History of Magic' }
]
});
// `person` is an instance of Person
// `person.books` is an instance of Books
// `person.books.at(0)` is an instance of Book
You could now add more books to the list as:
person.books.push(new Book({
name: 'Hogwarts: A History'
}));
The API is highly inspired by Tydel, and this package aims to be a reactive version of it using RxJS.
Types
Type expressions for your Models' schema.
Available types:
Types.string
const Todo = createModel({
schema: {
title: Types.string
}
});
Types.bool
const Todo = createModel({
schema: {
completed: Types.bool
}
});
Types.date
const Todo = createModel({
schema: {
createdAt: Types.date
}
});
Types.number
const Person = createModel({
schema: {
age: Types.number
}
});
Types.enum
If you want the value to be one of the pre-defined list of values:
const Book = createModel({
schema: {
category: Types.enum([
'history',
'fiction',
'romance'
])
}
});
And if you want the enum to be of specific types, you can use enum.of:
const Book = createModel({
schema: {
category: Types.enum.of([
Types.string,
Types.number
])
}
});
Types.uuid
const Book = createModel({
schema: {
id: Types.uuid
}
});
Example UUID value: 27961a0e-f4e8-4eb3-bf95-c5203e1d87b9
Types.model
Models can embed other models too:
const Person = createModel({
schema: {
address: Types.model
}
});
If you want to be more strict about which Model class can be embedded, use model.of
:
const Address = createModel({
schema: {
street: Types.string,
city: Types.string
}
});
const Person = createModel({
schema: {
address: Types.model.of(Address)
}
});
Types.collection
Collections can also be embedded in models:
const Author = createModel({
schema: {
books: Types.collection
}
});
If you want to be more strict about which Collection class can be embedded, use collection.of
:
const Book = createModel({
schema: {
title: Types.string
}
});
const Books = createCollection({
model: Book
});
const Author = createModel({
schema: {
books: Types.collection.of(Books)
}
});
The following Types are available, but not recommended for use since these do not support observing them for changes. Consider embedding Models or Collections instead:
Types.object
Types.array
Types.any
createModel(options)
Returns a Model
class based on the schema and methods that are provided.
options
(Object
):options.schema
(Object
): Schema object with keys having field values based on Types
expressionsoptions.initialize
(Function
): Called when the Model is constructedoptions.*
(Function
): Custom methodsModel class.
createCollection(options)
options
(Object
):options.model
(Model
): Model class that this Collection is ofoptions.initialize
(Function
): Called when the Collection is constructedoptions.*
(Function
): Custom methodsCollection class.
Next to the custom methods, Models also expose some built-in methods.
Some methods also support streaming the results with an Observable. Look for methods ending with $
:
getIn(paths)
getIn$(paths)
Returns the value in given path.
For example:
const firstBookTitle = author.getIn(['books', 0, 'title']);
// same as:
// author.books.at(0).title;
get()
get(path)
get$(path)
If no argument provided, then results self.
The path
can either be key according to the model's schema, or a dot separated path targeting some nested child.
person.get('books.0.title');
// same as:
// person.getIn(['books', 0, 'title']);
toJS()
toJS$()
Returns a plain JavaScript object from all its properties, as well as nested Models and Collections.
destroy()
Destroys the model, and cleans up its watchers.
The Collection instance tries to imitate the native Array
as much as possible.
Most methods also support supporting streaming the results as they change. Look for methods ending with $
in examples.
length
The lengh of the Collection.
const length = collection.length;
collection.at(n)
collection.at$(n)
Returns the model at specific index
collection.push(model)
Pushes the model, and adds it to the end of the collection.
every(iteratorFn)
every$(iteratorFn)
Tests whether all models in the collection pass the test implemented by the provided function.
filter(iteratorFn)
filter$(iteratorFn)
Creates a new array with all models that pass the test implemented by the provided function.
find(iteratorFn)
find$(iteratorFn)
Returns a model in the collection, if a model in the array satisfies the provided testing function. Otherwise undefined is returned.
forEach(iteratorFn)
Executes provided function once per model in the collection.
includes(model)
includes$(model)
Determines whether colelction includes a certain model, returning true or false as appropriate.
indexOf(model)
indexOf$(model)
Returns the first index at which a given model can be found in the collection, or -1 if it is not present.
map(fn)
map$(fn)
Creates a new array with the results of calling the provided function on every model in this collection.
reduce(fn, initialValue)
reduce$(fn, initialValue)
Applies the function against an accumulator and each model of the collection (from left-to-right) to reduce it to a single value.
some(iteratorFn)
some$(iteratorFn)
Tests whether some model in the collection passes the test implemented by the provided function.
pop()
Removes the last model from the collection and returns that model. This method changes the length of the collection.
shift()
Removes the first model from the collection and returns that model. This method changes the length of the collection.
unshift(model)
Adds one or more models to the beginning of the collection and returns the new length of the collection.
remove(model)
Removes model from the collection.
removeFrom(n)
Removes model from the given n index.
first()
first$()
Gets the first model of the collection.
last()
last$()
Gets the last model of the collection.
take(n = 1)
take$(n = 1)
Creates a slice of array with n models taken from the beginning.
takeRight(n = 1)
takeRight$(n = 1)
Creates a slice of array with n models taken from the end.
destroy()
Destroys the collection and its watchers.
toJS()
toJS$()
Converts the collection to a plain array, and also converting the models into plain objects recursively.
get$()
Returns an Observable of the collection, as it keeps on changing.
isModel(object)
object
(Object
): The argument to check againstBoolean
: True if the given object is a valid Model instance, false otherwise.
isCollection(object)
object
(Object
): The argument to check againstBoolean
: True if the given object is a valid Collection instance, false otherwise.
TypesError
Thrown when Type checking has failed.
MethodError
Thrown when executing a custom method has resulted in an error.
CollectionError
Thrown when a Collection has experienced an error.