Fluxo is a simple, lightweight (~300LOC) and dependency free entity foundation to your javascript app. It's inspired on Backbone.js models/collections and other good ideas.
☑️ Read the Getting started.
☑️ Check the TodoMVC implementation with Fluxo.
##Usage
###Installation
Install with bower or npm and include on your app with some module loader (browserify/webpack/require.js) or include directly on your app through script tag.
$ bower install --save fluxo
or
$ npm install --save fluxo-js
####Using with CommonJS module loaders (Webpack/Browserify)
var Fluxo = require("fluxo-js");
var jon = new Fluxo.ObjectStore({ name: "John Doe" });
Javascript's literal objects most of the time can be the best tool for your app and you probably will hold the state of your app on one of then, but working with bare objects sometimes can generates some boilerplate. You may need to compute attributes, emit events, manipulate collections and so on. On apps that use some pattern like Facebook's Flux these kind of things is crucial.
So Fluxo is a foundation to "entities" with useful capabilities with a tiny dependency amount on your application. These entities objects we call store.
#Summary
- Fluxo.ObjectStore
- Fluxo.CollectionStore
- toJSON
- CID
- Events
- Computed properties
- Using with React.js
- License
##Fluxo.ObjectStore
For objects that represents a "single entity", you must instantiate the class Fluxo.ObjectStore
passing on the first constructor argument the store's attributes, like this:
var jon = new Fluxo.ObjectStore({ name: "John Doe" });
##Reading and Updating your store data
To read your store's data, you need access the data
property. To update your
data, you should avoid change the data
property directly, because Fluxo will
run some important tasks, like emits the change events and parse values.
#####setAttribute
setAttribute(attributeName, newValue)
Change a store attribute. This emits change
and change:<ATTRIBUTE-NAME>
events.
jon.setAttribute("name", "John Doe");
jon.data.name //=> "John Doe"
#####set
set(attributesSet)
Similar to setAttribute
, but receives an object and allows change multiple
attributes at the same time. Notice that set
merges the actual data
with the
attributes passed to the method.
jon.set({ "name": "John Bar" });
jon.data.name //=> "John Bar"
#####reset
reset(attributes)
Similar to set, but removes the other attributes that aren't included on the attributes object and restore the default values.
jon.set({ "name": "John Bar" });
jon.reset({ "age": 30 });
jon.data.name // => undefined
jon.data.age // => 30
#####clear
clear()
Remove all attributes of your store.
jon.set({ "name": "John Bar", age: 25 });
jon.clear();
jon.data.name // => undefined
jon.data.age // => undefined
#####unsetAttribute
unsetAttribute(attributeName)
Remove an attribute, this emit change:<ATRIBUTE-NAME>
.
jon.unsetAttribute("age");
jon.data.age // => undefined
##Extending a Store
You may want to attach some custom behaviours to your stores, you can do this
creating your own ES6 class and extending Fluxo.ObjectStore
or Fluxo.CollectionStore
.
class Person extends Fluxo.ObjectStore {
sayHello () {
return "hello";
}
};
##Attributes Contracts
Attributes contracts allow you to define default values and custom parsing/dumping logics to your store's attributes. To use them, you need to specify the contracts on the class property attributes
with the following properties:
defaultValue
: the default value used on the store initialization and the #reset method.parser
: the function that receives the value on the argument and returns the value parsed.dump
: the function that receives the value on the argument and returns the result to #toJSON method. The default dump logic callstoJSON
on the value.required
(boolean): warning on the console when an attribute is required and is missing on the initialization or #unsetAttribute method.
Take a look at the examples below:
class Comment extends Fluxo.ObjectStore {}
Comment.attributes = {
title: { defaultValue: "This is my title" },
content: { defaultValue: "This is my default comment" }
};
var comment = new Comment({ content: "This is my comment" });
comment.data // => { title: "This is my title", content: "This is my comment" }
Using the parsers:
class Person extends Fluxo.ObjectStore {};
Person.attributes = {
age: { parser: function (value) { return parseInt(value); } }
};
var jon = new Person();
jon.setAttribute("age", "30");
jon.data.age //=> 30 (integer)
You can also use the attribute parsers to easily transform objects into nested stores if you need this.
class Author extends Fluxo.ObjectStore {};
class Post extends Fluxo.ObjectStore {};
Post.attributes = {
author: {
defaultValue: {},
parser: function (value) {
return (value instanceof Author) ? value : new Author(value);
}
}
};
var post = new Post({ content: "My Post", author: { name: "John" } });
post.data.author //=> <a instance of Author store>
You can specify how the attribute should be generated to #toJSON method.
class Post extends Fluxo.ObjectStore {};
Post.attributes = {
date: {
dump: function (value) {
return value.customToJSONMethod();
}
}
};
##Fluxo.CollectionStore
Sometimes you'll need stores that behaves like collections of stores, so you
can manipulate them, emit events, compute attributes based on your children objects and
so on, for this task you may use the Fluxo.CollectionStore
.
Before anything keep in mind that Fluxo.CollectionStore
is a extension of
Fluxo.ObjectStore
, so all the features available to object stores are available
to Fluxo collection stores.
The usage is pretty like the object store usage, but on the first constructor's argument, you should place an array with your children objects and on the second, you should place the collection store's data.
var people =
new Fluxo.CollectionStore(
[{ name: "John Doe" }, { name: "Ruth Anderson" }],
{ peopleCountry: "Brazil" }
);
Every object on the first constructor's argument will be transformed on Fluxo object store.
people.stores[0].data.name //=> "John Doe"
people.stores[1].data.name //=> "Ruth Anderson"
###Updating your collection
You should avoid add or remove items on your stores
property directly, instead, you
should use the following methods:
#####addStore
addStore(object|Fluxo.ObjectStore, options)
Add a single store. If it isn't a Fluxo store it will be parsed. This emits change
and add
events.
You can specify in what index you want your store with the option atIndex
(zero based).
people.addStore({ name: "Neo" });
people.stores[0].data.name //=> "Neo"
#####addStores
addStores(array[object|Fluxo.ObjectStore])
Same that addStore
, but accepts an array with objects to add at the same time.
people.addStores([{ name: "Neo" }, { name: "John" }]);
people.stores[0].data.name //=> "Neo"
people.stores[1].data.name //=> "John"
#####setStores
setStores(array[object|Fluxo.ObjectStore], options={ removeMissing: false })
Like addStores
, but if a store on the parameter is already added it will be updated.
You can specify the option removeMissing: true
to remove all stores that aren't specified on the payload.
var collection =
new Fluxo.CollectionStore([
{ id: 1, name: "Foo", gender: "m" },
{ id: 2, name: "Bar" }
]);
collection.setStores([{ id: 1, name: "FooBar" }], { removeMissing: true });
collection.stores[0].data.name // => FooBar
collection.stores[0].data.gender // => m
collection.stores[1] // => undefined
#####removeAll
removeAll()
Remove all stores. Emits change
and remove
events.
#####remove
remove(Fluxo.ObjectStore)
Remove a single store. Emits change
and remove
events.
var jon = new Fluxo.ObjectStore({ name: "John" });
people.addStore(jon);
people.stores[0] //=> <the store>
people.remove(jon);
people.stores[0] //=> undefined
#####resetStores
resetStores(array[object|Fluxo.ObjectStore])
Remove all previous children stores and add the argument's stores.
people.addStores([{ name: "Neo" }, { name: "John" }]);
people.resetStores([{ name: "Neo" }]);
people.stores[0].data.name //=> "Neo"
people.stores[1] //=> undefined
###Children stores extension
You can specify what class the children stores may have when it's added on
collection on the key store
of your collection class, like this:
class Person extends Fluxo.ObjectStore {
sayHello () {
return this.data.name;
}
};
class People extends Fluxo.CollectionStore {};
People.store = Person;
people.addStore({ name: "John" });
people.stores[0].sayHello(); //=> "John"
###Searching
Sometimes you will want get some stores of your collection, to this you can use the follow methods:
#####find
find(cid|id)
Returns the first store with the matching id
attribute or matching cid
(what's cid?)
#####findWhere
findWhere(criteriaObject)
Returns the first matching store. See #where for more about criteriaObject.
#####where
where(criteriaObject)
It returns an array containing the matching stores. You must pass on first argument an object with search criteria.
var jon = new Fluxo.ObjectStore({ name: "John", age: 30 }),
ruth = new Fluxo.ObjectStore({ name: "Ruth", age: 31 });
people.addStores([jon, ruth]);
people.where({ age: 30 }); //=> [jon];
###Ordering
If you want the stores sorted, you must use a computed property that sorts them, i.e.:
class People extends Fluxo.CollectionStore {
static computed = {
sorted: ["add", "remove", "stores:change:name"]
};
sorted () {
return [...this.stores].sort((a, b) => a.data.name.localeCompare(b.data.name));
}
};
people = new People;
people.addStore({name: 'zach'});
people.addStore({name: 'abraham'});
console.log(people.data.sorted.map(person => person.name));
//=> ['abraham', 'zach']
Note: You must use spread operator to copy the array because Array.prototype.sort mutates the caller and we don't want to mutate this.stores.
###Quick Dumb Collections
You can create collections without create a new class just to specify a different store class.
Just define the store key on the third argument of the Fluxo.CollectionStore
constructor with the children store class.
class Person extends Fluxo.ObjectStore {}
var people =
new Fluxo.CollectionStore([{ name: "John" }], {}, { store: Person });
people.stores[0] //=> instance of Person;
##Collection children delegations
When you are manipulating collections, you usually will manipulate the children
stores and you can do this accessing the store directly on the stores
property, but
it breaks the law of demeter, so, to
avoid this you can use the children delegations.
Children delegations, allows you to invoke methods on your children stores through the
collection. To delegate, you need to declare on the childrenDelegate
class property an
array with the name of methods that you want proxy, like this:
class Todo extends Fluxo.ObjectStore {
setAsDone () {
this.setAttribute("done", true);
}
};
class Todos extends Fluxo.CollectionStore {};
Todos.store = Todo;
Todos.childrenDelegate = ["setAsDone"];
var todos = new Todos([{ id: 2, done: "false" }]);
todos.setAsDone(2);
todos.stores[0].data.done // true
The first argument on the delegated method should be the ID of the child that you want to invoke the method (you can use the cid too), the rest will be passed to child's method.
The last line of our example above will call setAsDone
on the todo with id 2.
##toJSON
Eventually, you will pass the state that you are holding on your store to other
parts of your app, like the view layer. You should avoid pass the data
or stores
properties directly, the toJSON
is the right tool for this job.
On object stores the toJSON
will include all the properties of your data and the cid
,
like this:
{
cid: "FS:1",
name: "John Doe"
}
On collection stores the toJSON
will include the same things of object store
toJSON, but it'll also include the stores
property with all data of your children
stores (it will call toJSON
on its children), like this:
{
cid: "FS:2",
doneCount: 1,
pending: [
{ cid: "FS:4", done: false, content: "Buy a car!" }
],
stores: [
{ cid: "FS:3", done: true, content: "Buy milk!" },
{ cid: "FS:4", done: false, content: "Buy a car!" }
]
}
#toJSON
method returns a new JSON copy everytime that your store changes, if not,
the same copy is returned. It can be very useful for things like React's PureRenderMixin
##CID
Every Fluxo store has an internal CID (client ID) that are used to many internal tasks,
but you can use to reference your object on your interface as well. The CID value
is placed on the store's cid
property.
jon.cid //=> "FS:1"
##Events
Stores can emit and react on events using the on
, triggerEvent
and triggerEvents
methods.
The on
method accepts two arguments, the first is an array of events names
and the second is the callback.
To cancel the event subscription, you can invoke the returned canceler on the event subscription.
// Registers the event
var eventCanceler = jon.on(["change:name"], function (store) {
alert("wow, you changed the name");
});
// Cancels the event
eventCanceler.call();
The first argument method of triggerEvents
is an array with the events names that
you want to trigger, the other arguments are passed to the signed callbacks. If you
want to trigger only one event you can use the triggerEvent
.
myStore.triggerEvents(["myCustomEvent", "change"], "myCustomArgument");
###Nested Stores event bubbling
Every nested Fluxo store bubbles its events to its parents, like the example below:
var author = new Fluxo.ObjectStore({ name: "John" });
var post = new Fluxo.ObjectStore({ content: "My post", author: author });
post.data.author.setAttribute("name", "Ruth");
// Triggered Events on "post" store:
// * change:author:name
// * change:author
// * change
###Collections and event bubbling
The Fluxo collections propagates all the children store event's with the prefix
stores:
, so, if you trigger an event like "sayHello" on a store of a collection,
your collection will trigger the stores:sayHello
event. Take a look on the example
below:
var john = new Fluxo.ObjectStore({ name: "John" });
var people = new Fluxo.CollectionStore([john]);
john.setAttribute("name", "John Doe");
// Triggered Events on "people" store:
// * stores:change:name
// * stores:change
// * change
###Wildcard event
Every triggered event emits a wildcard event.
Example: if you trigger the event sayHello
on your object, the event called *
will be triggered, but the arguments to the callback for this event is diferrent
of other events. The first argument will be the name of event that was triggered,
the rest is the same of the other events.
jon.on(["sayHello"], function (store) {
//...
});
jon.on(["*"], function (eventName, store) {
// triggering the "sayHello" event will trigger this event with the
// argument "eventName" with value "sayHello".
});
jon.triggerEvent("sayHello");
##Computed properties
Fluxo object store and collection stores can have computed properties like Ember.js computed properties, this feature allows you to declare attributes that are computed on some events of your store.
Computed properties are very great to normalize the access of store's information
(you don't want to deal with store.fullName()
, right?)
and "caching" the computed results helping to avoid possible not necessary expensive
recomputations.
Looks the example below:
class Person extends Fluxo.ObjectStore {
fullName: function () {
return (this.data.firstName + " " + this.data.lastName);
}
};
Person.computed = {
fullName: ["change:firstName", "change:lastName"]
};
var person = new Person({ firstName: "John", lastName: "Doe" });
person.data.fullName; // => "John Doe"
person.setAttribute("lastName", "Anderson");
person.data.fullName; // => "John Anderson"
Or a more complex example with collection store:
class Todos extends Fluxo.CollectionStore {
doneCount () {
return this.stores.filter(todo => todo.data.done).length;
}
}
Todos.computed = {
doneCount: ["add", "remove", "stores:change:done"]
};
var todos = new Todos([{ done: true }, { done: false }]);
todos.data.doneCount; // => 1
todos.stores[1].setAttribute("done", true);
todos.data.doneCount; // => 2
##Using with React.js
Fluxo is view layer agnostic, you can use whatever you want, but we highly recommend the React.js. If you choose the React.js, we already created a way to connect your stores on your React.js components.
Read more: https://github.com/fluxo-js/fluxo-react-connect-stores
##License Fluxo is released under the MIT License.