Vue-model is a Javascript plugin for Vue.js that gives you the ability to transform your plain data into rich models with built-in and customizable HTTP actions.
This project started because I work in Vue relatively often and really really wanted to be able to call customer.save(), have it POST the data to the server, show the user feedback that the action was in progress, and then apply the server's results to the model.
So that's what this plugin does. And much more!
See more at aaronfrancis.com.
> npm install --save vue-model
Vue.use(require('vue-model'));Note: This is my first node.js package, so the module setup may not be quite perfect. Please feel free to submit pull-requests.
Here are a few quick examples to show you what you can do with vue-model.
<div v-if='!customer.$.editing'>
@{{ customer.name }}
<br>
<a href='#' @click.prevent='customer.$.edit()'>Edit</a>
</div>
<div v-if="customer.$.editing">
<input type='text' v-model='customer.name' :disabled='customer.$.inProgress'>
<br>
<a href='#' @click.prevent='customer.$.update()'>Save</a> or
<a href='#' @click.prevent='customer.$.cancel()'>Cancel</a>
</div><input type='text' v-model='customer.name' :disabled='customer.$.inProgress'>
<button @click.prevent='customer.$.update()' :disabled='customer.$.updateInProgress'>
<template v-if='customer.$.updateInProgress'>
<i class='fa fa-spinner fa-spin'></i>
Updating...
</template>
<template v-if='!customer.$.updateInProgress'>
Update Customer
</template>
</button><div v-for='customer in customers'>
@{{ customer.name }} (<a href='#' @click.prevent='customer.$.destroy()'>Delete</a>)
</div>new Vue({
el: 'body',
models: ['customer'],
data: {
customer: {
id: 1
}
},
events: {
'customer.fetch.success': function(data) {
console.log('Customer fetched!');
console.log(data.sent);
console.log(data.received);
}
}
})Before you can create models, you need to register them with vue-model. The registration process is simple using the Vue.models.register method.
Vue.models.register(type, options);The first argument is the type argument, which gives your model a "name". The second argument is a plain object that lets you define some options that are specific to your model. (We'll talk later on about all the ways to customize your model.)
Here's an example of registering a customer model that has a base route of /customers:
Vue.models.register('customer', {
baseRoute: '/customers',
});Now you're ready to start creating and using your models.
There are two different ways to create models in vue-model: You can create them manually whenever you please, or you can have vue-model create them automatically.
To manually create a model, use the $model() Vue Instance method.
Within a Vue Instance:
this.$model(type, data, options);The $model() method accepts 3 parameters:
type: (string) The type of model. This is the same key you used to register the modeldata: (object) The model dataoptions: (object) Any instance specific options
You can create the model wherever you please. For example, you can call the method inside the data function:
new Vue({
el: 'body',
data: function() {
return {
customer: this.$model('customer', {
name: 'Aaron'
})
};
}
});Or you can call it anywhere else! Here's an example where we instantiate a model within Vue's created lifecycle hook.
new Vue({
el: 'body',
data: {
customer: {
name: 'Aaron'
}
},
created: function() {
this.customer = this.$model('customer', this.customer);
}
});There may be times when you are using a model in a single place and don't want to register it, say in the case of a form. To create a model on-the-fly, just skip the type parameter. Your model's data becomes the first param, and the options become second.
new Vue({
el: 'body',
data: function() {
var formData = {
};
var formOptions = {
baseRoute: '/forms/something'
};
return {
form: this.$model(formData, formOptions)
};
}
});Manually creating models gives you ultimate flexibility, but sometimes you just want it to work right away. That's where automatic model creation comes into play.
To automatically create models, you simply need to add a models array to your Vue Instance. A models array element can take two forms. The first form is just a string:
new Vue({
el: 'body',
models: ['customer'],
data: {
customer: {
name: 'Aaron'
}
}
});When you pass a string in, the model type the data key must be the same. In the example above, the model type must be customer, and the data key must also be customer.
If you need more flexibility in naming, you can pass in a proper object.
new Vue({
el: 'body',
models: [{
type: 'customer'
dataKey: 'newCustomer'
}],
data: {
newCustomer: {
name: 'Aaron'
}
}
});In this example, the model type is still customer, but the actual data lives on the data key newCustomer.
Under the hood, vue-model adds a mixin that latches on to the
createdlifecycle event to create models automatically. Read more about the Vue Instance lifecycle
In the case where you need to pass options in, you can do that as well:
new Vue({
el: 'body',
models: [{
type: 'customer'
dataKey: 'newCustomer',
options: {
eventPrefix: 'new-customer'
}
}],
data: {
newCustomer: {
name: 'Aaron'
}
}
});In the case where you want to create many models at once, you can use the this.$models method. The second parameter should be an array of data and vue-model will loop through and create a model for each element.
new Vue({
el: 'body',
data: function() {
var customers = [{
// customer 1 data
},{
// customer 2 data
},{
// customer 3 data
}];
return {
customers: this.$models('customer', customers)
};
}
});You can definitely do this yourself using a
forloop and thethis.$modelmethod,this.$modelsis just a little more convenient.
Everything that vue-model provides lives on a single key on your data. By default, this key is $, although you can change it. Taking one of the model creation examples from above:
new Vue({
el: 'body',
models: ['customer'],
data: {
customer: {
name: 'Aaron'
}
}
});Your customer object now contains two properties:
name: the original property that was passed in (value ofAaron)$: the vue-model API
You may be (correctly) wondering why we're adding this new $ key instead of using prototypical inheritance like you might do traditionally. The reason we have to do that is because Vue.js requires that observed data be plain objects, which means we can't use object-like functions and their prototypes.
Performing HTTP Actions is the heart of vue-model. The whole purpose of this plugin is to make it painless for your models to interact with your application's backend.
All the actions are available on the vue-model key ($ by default). To perform an action, you just need to call the corresponding method.
Examples:
// Create a new customer
customer.$.create();
// Fetch this customer from the server
customer.$.fetch();
// Save this customer
customer.$.update();
// Delete this customer
customer.$.destroy();
// Retrieve a list of customers
customer.$.list()These are the 5 actions that vue-model ships with, but you are welcome to disable those and/or set up your own.
Actions are defined using the action key when you customize your model (which can be done in several places and will be covered in the Customizing Your Models section).
For simplicity, let's assume you are registering a video model and want to add two new actions: complete and uncomplete. That would be done as follows:
Vue.models.register('video', {
baseRoute: '/videos',
actions: {
complete: {
method: 'POST',
route: '/{id}/complete'
},
uncomplete: {
method: 'DELETE',
route: '/{id}/complete'
}
}
});All of your action's routes will be interpolated with your model's data. So if your model has an id of 10, a route of
/videos/{id}
becomes
/videos/10
You can do this with any attribute from your model. If your model's type has a value of watched, a route defined as
/videos/{type}/increment
would become
/videos/watched/increment
If you'd like to disable some of the default actions, you can do so by setting that action to false.
Example:
{
actions: {
list: false,
destroy: false
}
}The resulting model will only have the create, fetch, and update methods.
It's probable that you'll want to send different data to the server based on what action it is that's being executed. When you send a off a create request, you'll send all the data. But when you send a destroy request, you really shouldn't be sending any data at all. Vue-model accomplishes this through the its DataPipeline.
The DataPipeline comes with several useful methods by default:
none()- Don't send any data at allonly(keys)- Only send certainkeyswith(data)- Add additionaldatawithout(keys)- All the data, but without certainkeyscallback(fn)- Return whatever data you like from a callbackfnfunction
There are a couple of different ways to use the DataPipeline. The first is by defining it in your action definition:
Vue.models.register('video', {
baseRoute: '/videos',
actions: {
complete: {
method: 'POST',
route: '/{id}/complete',
pipeline: function(DataPipeline) {
// Don't post *any* data
DataPipeline.none();
}
}
}
});Now, every time you call video.$.complete(), the data will run through the action's pipeline which will strip all the data out.
The other option would be to define the pipleline inline by using the $.data object.
video.$.data
// Drop all model data
.none()
// Add some arbitrary data
.with({
forUser: 100
});
video.$.list();All DataPipeline methods return the DataPipeline, so you can chain them.
And if you want to do it really in line, your apiKey also lives on the data object so you can access your actions again.
video.$.data.none().$.complete();If you find yourself doing this too often, you should probably make that the default for the action.
Another great thing about vue-model is that you can automatically update your models with the response that comes back from the server.
If you define your action with apply = true, vue-model will take the response from the server, loop through all the data, and call Vue.set on the keys that have changed.
Vue.models.register('video', {
baseRoute: '/videos',
actions: {
complete: {
method: 'POST',
route: '/{id}/complete',
// Apply the returned data
apply: true
}
}
});If the server returns
{
completed: 1
}as its payload from the complete action, then the completed attribute on our model will automatically be updated.
video.$.complete();
// Once it finishes...
console.log(video.completed);
// > 1That lets us create toggle buttons very easily, all in HTML.
<button v-if="video.completed" @click.prevent="video.$.uncomplete()">
Completed
</button>
<button v-if="!video.completed" @click.prevent="video.$.complete()">
Mark as Complete
</button>By default, vue-model will prevent another action from being initiated while another action is running. If you want to turn this behavior off, you can pass false in for the preventSimultaneousActions option.
Often times you'll want to add or modify the HTTP headers that go out with your request, especially if you're using the Authorization header, for example.
We've made it easy to define headers in a couple of different ways. The first is to define headers that get applied to every action. This can be either a callback or just a plain object:
// Apply to every action, using a callback
Vue.use(require('vue-model'), {
headers: function(action) {
return {
'Authorization': getTokenFromStorage()
};
}
});
// Apply to every action, but using a plain object
Vue.use(require('vue-model'), {
headers: {
'foo': 'bar'
}
});You can see more about passing options in the Customizing Your Models section.
Alternatively, if you want to have action-specific headers, you can do that too using either a callback or an object.
Vue.models.register('video', {
actions: {
complete: {
method: 'POST',
route: '/{id}/complete',
headers: {
'foo': 'baz'
}
}
}
});Note: Action-specific headers will overwrite global headers that have the same key.
You'll often want to know when the model is busy, so that you can show loading indicators or prevent other actions. Vue-model provides two types of busy indicators: Global, and Action Specific.
The global busy indicator lives in the API object under the inProgress key.
For example, if you have a model named customer, you can observe the customer.$.inProgress attribute. This is helpful for showing/hiding elements or disabling buttons.
Here's one way you can disable a button, should the model be busy performing an HTTP action:
<button @click='video.$.complete()' :disabled='video.$.inProgress'>
Mark as Complete
</button>If you have loading indicators scattered across the page and only want to show the correct indicator based on the specific action, then you should use an action-specific busy indicator.
For every action, there is a corresponding property that indicates whether or not that action is currently in process. For example, if the action is named update, then the property would be named updateInProgress.
Consider a case where you have a complete action for a video model and would like to show a loading indicator on the button.
<button @click='video.$.complete()' :disabled='video.$.completeInProgress'>
<i v-if='video.$.completeInProgress' class='fa fa-spinner fa-spin'></i>
Mark as Complete
</button>This button will disable itself and show the lovely Font Awesome loading indicator () while the model finishes the complete action. This provides feedback and a good experience for your users. However, in this example if a different action is being performed, say a favorite action, the button will not show the loading indicator because it is bound to completeInProgress and not inProgress or favoriteInProgress.
When any of the action-specific loading indicators (
{action}InProgress) aretrue, the globalinProgressindicator will also betrue.
Vue-model emits several events that you can listen for and respond to, giving you many different ways to seamlessly tie your app into vue-model.
Vue-model events follow a naming scheme of {eventPrefix}.{action}.{result}. The eventPrefix can be set when you are registering or instantiating your models. (See Customizing Your Models for more information on how to set this.)
By default, if you don't pass in an eventPrefix while registering your model, vue-model will set it to the type of model you register.
// No eventPrefix, model type is 'customer'
Vue.models.register('customer', {
baseRoute: '/customers'
});
// --> eventPrefix is equal to 'customer'
// Explicit eventPrefix passed in
Vue.models.register('customer', {
baseRoute: '/customers',
eventPrefix: 'cst'
});
// --> eventPrefix is equal to 'cst'{action} is always equal to the name of the action on your API. If you call customer.$.update(), action will be equal to update.
{result} is one of the following:
before- Before the action takes placesuccess- Successful completion of the actionerror- Action failedcomplete- Action finished, regardless of outcomecanceled- Action canceled by because abeforecallback returnedfalseprevented- Action prevented because another action was still in progress
Putting it all together, the event name will look similar to the following examples:
customer.update.beforecustomer.destroy.successcustomer.fetch.errorcustomer.list.completecustomer.create.canceledcustomer.update.prevented
Each event comes with data payload:
-
{eventPrefix}.{action}.before- Before the action takes place{ // The object that is about to // be sent to the server sending: {} }
-
{eventPrefix}.{action}.success- Successful completion of the action{ // The object that was sent to the server sent: {}, // Data that was received from the server received: {} }
-
{eventPrefix}.{action}.error- Action failed{ // The object that was sent to the server sent: {}, // The failed XHR object received: {} }
-
{eventPrefix}.{action}.complete- Action finished, regardless of outcome{ // The object that was sent to the server sent: {}, // Data that was received from the server // OR an failed XHR, depending on success // or failure of the request received: {} }
-
{eventPrefix}.{action}.canceled- Action canceled by because abeforecallback returnedfalseNo data.
-
{eventPrefix}.{action}.prevented- Action prevented because another action was still in progress{ // The action that was prevented action: {} }
Vue-model also emits an
{eventPrefix}.preventedevent every time any action is prevented. For this event, thenameof the event is also attached.
{
// The name of the event (create, update, destroy, etc)
name: '',
// The action that was prevented
action: {}
}Vue-model needs to know how to emit events before it can actually do so. By default, vue-model uses the $emit method on the instance that you used to create your models. This lets you put your listeners right in your Vue instance
new Vue({
el: 'body',
models: ['customer'],
data: {
customer: {
id: 1
}
},
events: {
'customer.fetch.success': function(data) {
console.log('Got some new data from the server!');
console.log(data.received);
}
}
});If you don't want to use the $emit method, you can pass use Vue's $broadcast or $dispatch methods by passing in broadcast or dispatch, respectively. (Leave off the leading $.)
You could also pass in your own callback if you don't want to use any of Vue's methods.
// Emitter for your customer model
Vue.models.register('customer', {
emitter: function(action, data) {
// Pass the event on to...
// Pusher
// PubNub
// Websocket
// etc etc
}
});
// Emitter for *every* model
Vue.use(require('vue-model'), {
emitter: function(action, data) {
// Pass the event on to...
// Pusher
// PubNub
// Websocket
// etc etc
}
});See more in the Customizing Your Models section.
@TODO
@TODO
Vue-model has been created to be as configurable as possible, but still remain very easy to use. We've also included several places where you can introduce model customization, so that you can worry about it as infrequently as possible.
Since there are so many ways to customize your models, let's talk about order of importance.
Vue-model ships with a ModelDefaults.js file that defines all the possible defaults. This is the least important, but provides a solid base to get you started. (See below for a copy of the ModelDefaults.js)
If you have specific defaults that you'd like to apply to every model you ever create, you can pass in your own defaults that override the vue-model defaults. You do that when you call Vue.use.
For example, if you want all your models to use the underscore _ as the api key instead of the default $, you could easily do that one time and then forget about it:
Vue.use(require('vue-model'), {
apiKey: '_'
});Your new apiKey will override the vue-model default apiKey so that every model you create will have the api under _, making your actions look more like this:
video._.complete();When you register a model using Vue.models.register, you have the ability to pass in options as a third parameter. If, for example, you don't want a certain model to have the destroy action, you can disable it for a single model:
Vue.models.register('customer', {
actions: {
destroy: false
}
});With this configuration, every time you call this.$model('customer', {}), there will be no destroy action, because you declared it false upon registration.
The highest priority for options are instance specific options. Instance specific options can override every other option. Instance specific options are (optionally) declared when you create a model. For example, if you'd like to change the event emitter for a single instance, you can:
this.$model('customer', data, {
// This model will not emit events (noop)
emitter: function() {}
});If you are automatically creating models and want to pass in different options than the options you registered with, just make models a proper object and include an options object.
new Vue({
el: 'body',
models: [{
type: 'customer'
dataKey: 'newCustomer',
options: {
emitter: function() {}
}
}],
data: {
newCustomer: {
name: 'Aaron'
}
}
});This is the ModelDefaults.js file that vue-model ships with and contains all the available options.
{
// The key that contains vue-model API
apiKey: '$',
// Any keys we don't want to send up to the server
// or apply from the server. Often, this can be
// used for related models, etc.
excludeKeys: [],
// Prepended to each of the action routes
baseRoute: '',
// Prepended to each event that gets emitted. If
// you leave this blank when your register your
// models, vue-model will set eventPrefix equal
// to the `type` that you registered. Event
// naming schema: {eventPrefix}.{action}.{status}
// Eg: "customer.fetch.success"
eventPrefix: '',
// The function that emits events. You can pass
// a string name of one of the Vue.js instance
// event methods here and vue-model will convert
// it to a proper function using the Vue instance
// from which you instantiated the model.
// Allowed: 'emit', 'broadcast', 'dispatch', or
// a callback function.
emitter: 'emit',
// HTTP Headers that get set on each action.
// This can be a plain object or a callback
// that returns a plain object.
headers: {},
// Prevent an action from being invoked while
// another action is still running
preventSimultaneousActions: true,
// Default HTTP Actions that every model gets
actions: {
list: {
method: 'GET',
route: '',
pipeline: function(DataPipeline) {
DataPipeline.none();
}
},
create: {
method: 'POST',
route: '',
},
fetch: {
method: 'GET',
route: '/{id}',
apply: true,
pipeline: function(DataPipeline) {
DataPipeline.none();
}
},
update: {
method: 'PUT',
route: '/{id}',
apply: true
},
destroy: {
method: 'DELETE',
route: '/{id}',
pipeline: function(DataPipeline) {
DataPipeline.none();
}
}
},
// Base defaults for every action
actionDefaults: {
// Apply data that's returned
// from the server
apply: false,
// Load validation errors into the
// model if the server returns them
validation: true,
// Action specific headers
headers: {},
// Perform before the action. Return
// false to cancel the action
before: function() {
//
},
// Perform after the action completes
after: function(data) {
//
}
},
// Model validation errors coming from the server
validationErrors: {
// Function to determine whether or not an
// error response is a validation error.
// 422 is the correct status code, so if
// you use Laravel, no need to update this.
isValidationError: function(xhr) {
return xhr.status === 422;
},
// The error object should have the field names
// as the keys and an array of errors as the
// values. Laravel does this automatically.
transformResponse: function(xhr) {
return xhr.responseJSON;
}
}
}This is the API that vue-model appends to your object. By default, this is attached to your data under a $ key, although you can specify the key by declaring an apiKey for your model.
-
list()The
listHTTP action -
create()The
createHTTP action -
fetch()The
fetchHTTP action -
update()The
updateHTTP action -
destroy()The
destroyHTTP action -
copy()Returns a plain object copy of the model's
data, without any vue-model extras. -
edit()Copies the current
datainto a cache and sets theeditingflag totrue -
cancel()Applies the old
datathat was copied into the cache by theeditfunction, and sets theeditingflag back tofalse -
apply(newData)Load an object into the model's
data. (This is the same function that vue-model uses to apply the data from the server's response.) -
inProgressbooleanGlobal loading indicator -
listInProgressbooleanLoading indicator for thelistaction -
createInProgressbooleanLoading indicator for thecreateaction -
fetchInProgressbooleanLoading indicator for thefetchaction -
updateInProgressbooleanLoading indicator for theupdateaction -
destroyInProgressbooleanLoading indicator for thedestroyaction -
editingbooleanIndicator as to whether or not the model is in editing mode. -
errors-
hasAny()booleanWhether or not there are any errors -
has(field)booleanWhether or not there are errors forfield -
first(field)string|undefinedThe first error forfield -
get(field)array|undefinedAll the errors forfield -
clear(field)Clear the errors for a
field -
push(field, value)Add a new error
valueforfield -
set(collection)Completely overwrite all the errors with a new object. Keys should be field names and values should be arrays full of strings.
-
allA raw object of all the errors so Vue.js can observe and react to changes in errors.
-
-
data-
none()Drop all data
-
only(keys)Of all the attributes in your data, only keep ones that are in the
keysarray -
with(data)Add any additional data that you please
-
without(keys)Drop
keysout of your object -
callback(fn)Pass in any callback function
fnto process thedata. The first argument to yourfnwill be thedataas it currently exists. You can also pass args in tocallbackand they will be passed on to yourfn. Example:var processData = function(data, foo, bar) { // In this example: // foo === 'foo-arg' // bar === 'bar-arg' // Do something with the data... return data; }; video.$.data.callback(processData, 'foo-arg', 'bar-arg');
-
forAction(name)Returns the
datathat would be sent for an action. Useful for debugging.// Get the data that would be posted for the 'update' action var dataToBePosted = video.$.data.with({test: 1}).forAction('update'); // Inspect the data console.log(dataToBePosted);
-
$(or whatever yourapiKeyis)A reference back to your API object
video.$.data.none().$.complete();
Allows you to get back up a level from your data pipeline operations.
-