A missing model layer for modern JavaScript
🧐 What is Loco-JS-Model?
Loco-JS-Model is one of the Loco framework components. It is a model layer for JavaScript that can be used separately.
Loco framework is a concept that simplifies communication between front-end and back-end. The back-end part can be implemented in other languages and frameworks as well.
I am a Rails programmer. That's why I created Loco for Rails.
Visualization of the Loco framework:
Loco Framework
|
|--- Loco-Rails (back-end part)
| |
| |--- Loco-Rails-Core (logical structure for JS / can be used separately with Loco-JS-Core)
|
|--- Loco-JS (front-end part)
|
|--- Loco-JS-Core (logical structure for JS / can be used separately)
|
|--- Loco-JS-Model (model part / can be used separately)
|
|--- other built-in parts of Loco-JS
Loco-JS-UI - connects models with UI elements (a separate library)
Loco-JS-Model works well as a part of the modern JavaScript ecosystem alongside libraries such as React and Redux.
This 🎁example🎁 presents how to combine Loco-JS-Model with React and Redux (+ other neat tools).
This repo is also a good starting point if you want to start hack on a multi-static-page app powered by React, Redux, React, React-Router, Webpack, Babel, etc.
Especially if you are looking for something pre-configured and more straightforward than Create React App.
📡 Model Layer
I liked ActiveRecord throughout the years of using Rails. This layer stands between the business logic of your app and a database and does a lot of useful things. One of them is providing validations of objects to ensure that only valid ones are saved in a database. It also provides several finder methods to perform certain queries on a database without writing raw SQL.
But what does model mean when it comes to an app working inside the browser? 🤔
Well, you have at least 2 ways to persist your data:
- You can save them in local storage.
- You can send them to a server using the API endpoint. Data are then stored in a database.
So we can assume that validating data before they reach the destination can be useful in both cases.
But when it comes to persistence - Loco-JS-Model gravitates towards communication with a server. It provides methods that facilitate both: persisting data and fetching them from a server.
📥 Installation
$ npm install --save loco-js-model
🤝 Dependencies
🎊 Loco-JS-Model has no dependencies. 🎉
Although babel-plugin-transform-class-properties may be helpful to support static class properties, which are useful in defining models.
Loco-JS-Model uses Promises, so remember to polyfill them❗️
⚙️ Configuration
import { Config } from "loco-js-model";
Config.authorizationHeader = "Bearer XXX";
Config.protocolWithHost = "http://localhost:3000";
Config.cookiesByCORS = true;
Config.locale = "pl";
Config.scope = "admin";
🎮 Usage
Anatomy of the model 💀
An exemplary model can look like this:
import { Models } from "loco-js-model";
class Coupon extends Models.Base {
static identity = "Coupon";
static protocolWithHost = "https://myapp.test";
static resources = {
url: "/user/coupons",
admin: {
url: "/admin/plans/:planId/coupons",
paginate: { per: 100, param: "current-page" }
}
};
static attributes = {
stripeId: {
remoteName: "stripe_id",
type: "String",
validations: {
presence: true,
format: {
with: /^my-project-([0-9a-z-]+)$/
}
}
},
percentOff: {
remoteName: "percent_off",
type: "Integer",
validations: {
presence: { if: o => o.amountOff == null },
numericality: {
greater_than_or_equal_to: 0,
less_than_or_equal_to: 100
}
}
},
amountOff: {
remoteName: "amount_off",
type: "Decimal",
validations: {
presence: { if: o => o.percentOff == null },
numericality: {
greater_than_or_equal_to: 0
}
}
},
duration: {
type: "String",
validations: {
presence: true,
inclusion: {
in: ["forever", "once", "repeating"]
}
}
},
redeemBy: {
remoteName: "redeem_by",
type: "Date"
}
};
static validate = ["futureRedeemBy"];
constructor(data = {}) {
super(data);
}
futureRedeemBy() {
if (this.redeemBy === null) return;
if (this.redeemBy <= new Date()) {
this.addErrorMessage("should be in the future", { for: "redeemBy" });
}
}
}
export default Coupon;
Fetching a collection of resources 👨👩👧👦
Specifying a scope 🔎
You can fetch resources from a given scope in 3 ways:
- by specifying a scope as
resource
in method calls e.g. Coupon.get("all", {resource: "admin"})
- setting up default scope at the configuration stage (see Configuration)
- if you use Loco-JS you can set scope by calling
setScope("<scope name>")
controller's instance method. It's done in a namespace controller most often.
Response formats 𝌮
Loco-JS-Model can handle responses in 2 JSON formats.
1. an array of resources
[
{
"id":101,
"stripe_id":"my-project-20-dollar-off",
"amount_off":20,
"currency":"USD",
"duration":"once",
"percent_off":null,
"redeem_by":null,
"created_at":"2017-12-19T14:42:18.000Z",
"updated_at":"2017-12-19T14:42:18.000Z"
},
...
]
To fetch all resources, you have to specify a total number of records by using total or count keys.
Coupon.get("all", {resource: "admin", planId: 6, total: 603}).then(coupons => {});
2. with resources and count keys
{
"resources": [
{
"id":101,
"stripe_id":"my-project-20-dollar-off",
"amount_off":20,
"currency":"USD",
"duration":"once",
"percent_off":null,
"redeem_by":null,
"created_at":"2017-12-19T14:42:18.000Z",
"updated_at":"2017-12-19T14:42:18.000Z"
},
...
],
"count": 603
}
To fetch all resources, you don't have to specify a total number of records in this case, because API does it already.
Coupon.get("all", {resource: "admin", planId: 6}).then(res => {
res.resources;
res.count;
});
Fetching resources from other API endpoints
Just pass the name of the endpoint instead of "all".
This example also contains how to pass additional parameters to the request.
Coupon.get("used", {total: 211, foo: "bar", baz: 13}).then(coupons => {});
Fetching a specific page
Just pass a page
param.
Coupon.get("recent", {
resource: "admin",
planId: 6,
total: 414,
page: 4,
foo: 10
}).then(coupons => {});
Fetching a single resource 💃
Loco-JS-Model provides find
static method for fetching a single resource. The server's response should be in a plain JSON format with remote names of attributes as keys.
find
returns null
if server responds with 404 HTTP status code.
Coupon.find(25).then(coupon => {});
Coupon.find({id: 25}).then(coupon => {});
Coupon.find({id: 25, resource: "admin", planId: 8, foo: 12, bar: "baz"}).then(coupon => {});
Sending requests 🏹
Every model inherits from Models.Base
static and instance methods for sending get
post
put
patch
delete
requests to the server.
Coupon.patch("used", {resource: "admin", planId: 9, ids: [1,2,3,4]}).then(resp => {});
Coupon.find({id: 25, resource: "admin", planId: 8}).then(coupon => {
coupon.planId = 8;
coupon.patch("use", {foo: "bar", baz: 102}).then(resp => {});
});
Validations ✅
If attributes' validations are specified, you can use the isValid
/ isInvalid
methods to check whether the model instance is valid or not.
const coupon = new Coupon;
coupon.isValid();
coupon.isInvalid();
coupon.errors;
Loco-JS-Model implements almost all built-in Rails validators, except for uniqueness. And you can use them nearly identically.
You can also look at source code if you are looking for all available configuration options. They are pretty straightforward to decipher.
Saving ✍️
Loco-JS-Model provides the save
method that facilitates persisting resources on the server. This method requires responses in a specific JSON format. I recommend using the format below. But if you don't plan to use UI.Form
from Loco-JS-UI for handling forms, the only requirement is a specified format of the errors key to having errors assigned to the object.
const coupon = new Coupon({
resource: "admin",
planId: 19,
percentOff: 50
});
coupon.save().then(resp => {
resp;
coupon.errors;
});
Reloading ♻️
Loco-JS-Model provides a convenient method for reloading an object. The following example is quite self-explanatory.
Coupon.find({id: 25, resource: "admin", planId: 8}).then(coupon => {
coupon.planId = 8;
coupon;
setTimeout(() => {
coupon.reload().then(coupon => {
coupon;
});
}, 5000);
});
💥 Dirty object 🧙🏽♂️
This feature looks like pure magic when you look at how this works for the first time.
The Dirty object is an ability of model instances to express how attribute values have been changed between 2 moments in time - when an object was initialized and their current value on the server.
It is especially useful when you use Connectivity
features from Loco-JS.
Just look at the example below and bear in mind the order of things 💥
Coupon.find({id: 25, resource: "admin", planId: 8}).then(coupon => {
coupon;
setTimeout(() => {
coupon.changes();
coupon.applyChanges();
coupon;
}, 6000);
});
setTimeout(() => {
Coupon.find({id: 25, resource: "admin", planId: 8}).then(coupon => {
coupon;
});
}, 3000);
Useful methods 🔧
Models.Base
instance methods
-
assignAttr(name, val)
- it converts val
to a given type defined in attributes
property before assigning
-
clone
- it clones and returns a model instance.
🇵🇱 i18n
Loco-JS-Model supports internationalization. The following example shows how to display errors in a different language.
First, create a translation of the base English file.
const pl = {
variants: {},
attributes: {},
errors: {
messages: {
blank: "nie może być puste",
inclusion: "nie jest na liście dopuszczalnych wartości",
invalid: "jest nieprawidłowe",
}
}
};
export default pl;
Loco-JS-Model must have all translations assigned to the I18n
object.
import { Config, I18n } from "loco-js-model";
import pl from "locales/pl";
Object.assign(I18n, {
pl
});
Config.locale = "pl";
const coupon = new Coupon({ percentOff: 50 });
coupon.isValid();
coupon.errors;
👩🏽🔬 Tests
$ npm run test
Loco-JS-Model has been extracted from Loco-JS. Loco-JS is a front-end part of the whole Loco framework, along with Loco-Rails (the back-end part).
Both Loco-JS and Loco-Rails are pretty well tested. And because they work in cooperation with each other, they must be tested as one library (Loco-Rails has a suite of integration / "end to end" tests).
So every change made to Loco-JS-Model must be tested with Loco-JS' unit tests and then together as Loco framework, it must be tested against Loco-Rails' integration test suite.
📈 Changelog
Major releases 🎙
2.0.0 (2022-02-03)
- Ability to set an individual
protocolWithHost
for a given model Config.authorizationHeader
getter / setterConfig
setters don't return a value
1.1.1 (2020-12-09)
find
method reacts on 404 HTTP response- a URL generation was fixed when
Config.protocolWithHost
is used
1.1 (2020-09-06)
- Ability to receive & send cookies by a CORS request
1.0 (2020-05-19)
- Breaking changes:
Base
is no longer exported. You must use Models.Base
0.3.1
- 🎉 officially announced version 🎉
Informations about all releases are published on Twitter
📜 License
Loco-JS-Model is released under the MIT License.
👨🏭 Author
Zbigniew Humeniuk from Art of Code