vue-mc Vue Models & Collections

Models and Collections for Vue.js

The relationship between data, component states, and the actions that affect them is a fundamental and unavoidable layer to manage when building a component or application. Vue does not provide a way to structure and encapsulate data, so most projects use plain objects and implement their own patterns to communicate with the server. This is perfectly fine for small applications, but can quickly become a lot to manage when the size of your project and team increases.

This library takes care of this for you, providing a single point of entry and a consistent API:

The basic concept is that of a Model and a Collection of models. Data and component state is managed automatically, and CRUD is built-in. A classic example would be a to-do list, where each task would be a model and the list of tasks would be a collection.

Installation

Install

Add the vue-mc package to your package dependencies:

npm install --save vue-mc

# Or

yarn add vue-mc

Basic Usage

You should extend the base classes to create appropriate models and collections for your data. The example we’ll use is a basic task list, consisting of a list (Collection) of tasks (Model).

Extending the base classes

import { Model, Collection } from 'vue-mc'
import { required, integer, minValue } from '@vuelidate/validators'

/**
 * Task model
 */
class Task extends Model {

    // Default attributes that define the "empty" state.
    defaults() {
        return {
            id:   null,
            name: '',
            done: false,
        }
    }

    // Attribute mutations.
    mutations() {
        return {
            id:   (id) => Number(id) || null,
            name: String,
            done: Boolean,
        }
    }

    // Attribute validation (Vuelidate: use @vuelidate/validators)
    validation() {
        return {
            id:   { integer, minValue: minValue(0) },
            name: { required },
            done: {},
        }
    }

    // Route configuration
    routes() {
        return {
            fetch: '/task/{id}',
            save:  '/task',
        }
    }
}

/**
 * Task collection
 */
class TaskList extends Collection {

    // Model that is contained in this collection.
    model() {
        return Task;
    }

    // Default attributes
    defaults() {
        return {
            orderBy: 'name',
        }
    }

    // Route configuration
    routes() {
        return {
            fetch: '/tasks',
        }
    }

    // Number of tasks to be completed.
    get todo() {
        return this.sum('done');
    }

    // Will be `true` if all tasks have been completed.
    get done() {
        return this.todo == 0;
    }
}

Creating a new instances

// Create a new empty task list.
let tasks = new TaskList(); // (models, options)

// Create some new tasks.
let task1 = new Task({name: 'Tests'});
let task2 = new Task({name: 'Documentation'});
let task3 = new Task({name: 'Publish'});

Adding tasks to the list

// Add the tasks to the collection.
tasks.add([task1, task2, task3]);

// You can add plain objects directly to the collection.
// They will automatically be converted to `Task` models.
let task1 = tasks.add({name: 'Tests'});
let task2 = tasks.add({name: 'Documentation'});
let task3 = tasks.add({name: 'Publish'});

// You can add multiple models at the same time.
let added = tasks.add([
    {name: 'Tests'},
    {name: 'Documentation'},
    {name: 'Publish'},
]);

Rendering a task input section

<div class="task-form" :class="{saving: task.saving}">

    <!-- Name and validation errors -->
    <input type="text" v-model="task.name">
    <span v-for="(error in task.errors.name)"></span>

    <!-- Done checkbox -->
    <input type="checkbox" v-model="task.done">

    <!-- Save button -->
    <button v-on:click="task.save">Save</button>
</div>

Rendering a task row

<div class="task">
    <p>{{ task.$.name }}</p>
    <p>{{ task.$.done }}</p>
</div>

Rendering the list

<div class="tasks">
    <task v-for="task in tasks.models" :task="task" :key="task.id"></task>
</div>

Models

Creating instances

Model instances can be created using the new keyword. The default constructor for a model accepts three optional parameters: attributes, collection, and options.

let model = new Model(attributes = {}, collection = null, options = {});

Attributes

Attributes will be merged with the default attributes as defined by the defaults() method. If no attributes are provided on construction, the model will represent an “empty” state.

Important: You should define a default value for every attribute.

// Create a new task with initial attributes.
let task = new Task({name: 'Task #1'});

task.name; // 'Task #1'
task.done; // false

Collection

The collection parameter allows you to specify one or more collections that the model belongs to. A common pattern where this comes in handy is when you are creating a new model that should be added to a collection when it is created on save.

For example, a user clicks a “New Task” button which shows a form to fill out a new task. If we create a new Task model and set tasks as its collection, it will automatically be added to tasks when saved successfully.

onCreateNew() {
    this.task = new Task({}, tasks);
}

Options

The options parameter allows you to set the options of a model instance. These can be any of the default options or something specific to your model. To get the value of an option, use getOption(name). You can also set an option later on using setOption(name, value) or setOptions(options).

You should define a model’s default options using the options() method:

class Task extends Model {
    options() {
        return {
            editable: false,
        }
    }
}

let task1 = new Task({}, null, {editable: true});
let task2 = new Task({}, null);

task1.getOption('editable'); // true
task2.getOption('editable'); // false

Available options

Option Type Default Description
methods Object See below HTTP request methods.
identifier String "id" The attribute that should be used to uniquely identify this model, usually a primary key like "id".
overwriteIdentifier Boolean false Whether this model should allow an existing identifier to be overwritten on update.
routeParameterPattern Regex /\{([^}]+)\}/ Route parameter matching pattern.
patch Boolean false Whether this model should perform a “patch” on update (only send attributes that have changed).
saveUnchanged Boolean true Whether this model should save even if no attributes have changed. If set to false and no changes have been made, the request will be considered a success.
useFirstErrorOnly Boolean false Whether this model should only use the first validation error it receives, rather than an array of errors.
validateOnChange Boolean false Whether this model should validate an attribute after it has changed. This would only affect the errors of the changed attribute and will only be applied if the value is not blank.
validateOnSave Boolean true Whether this model should be validated before it is saved. This will cause the request to fail if validation does not pass. This is useful when you only want to validate on demand.
validateRecursively Boolean true Whether this model should validate other objects within its attribute tree. The result is implicit recursion as each of those instances will also validate their trees, etc.
mutateOnChange Boolean false Whether this model should mutate a property as it is changed before it is set. This is a rare requirement because you usually don’t want to mutate something that you are busy editing.
mutateBeforeSync Boolean true Whether this model should mutate all attributes before they are synced to the “saved” state. This would include construction, on fetch, and on save success.
mutateBeforeSave Boolean true Whether this model should mutate all attributes before a “save” request is made.
Default request methods
{
    ...

    "methods": {
        "fetch":  "GET",
        "save":   "POST",
        "update": "POST",
        "create": "POST",
        "patch":  "PATCH",
        "delete": "DELETE",
    }
}

Identifiers

Models usually have an identifier attribute to uniquely identify them, like a primary key in the database. The default identifier attribute is "id", but you can override this with the identifier option.

class Task extends Model {
    options() {
        return {
            identifier: 'uuid',
        }
    }
}

Collections

Models can be registered to collections, which implies that the model “belongs to” that collection. If a model is created or deleted, it will automatically be added or removed from every collection it is registered to.

When you add a model to a collection, it will automatically be registered to that collection, but there may be cases where you want to manually register one or more collections. You can do this with the registerCollection method, which accepts either an instance or an array of collections.

Attempting to register the same collection more than once has no effect.

Attribute states

Models maintain three separate attribute states: empty, active, and saved.

Empty

The empty state is defined by the model’s defaults() method, and is automatically assigned to the active state when the model is created or cleared.

Active

The active state is accessed directly on the model, eg. task.name. This is also the attributes that will be sent to the server when the model is saved. You can mess with this data as much as you want, then either call clear() to revert back to the empty state, or reset() to revert back to the saved state.

Saved

The saved state is the “source of truth”, which usually reflects what’s in the database. You can use sync() to apply the active state to the saved state, which happens automatically when the model is saved successfully, and when constructed.

Attribute state flow diagram

Data access

Active attributes

You can access the active state directly on the instance, or with the get(attribute, default) method which allows you to specify a default value to fall back on if the attribute does not exist.

It’s safe to set the value of an existing attribute directly, or you can use set(attribute, value). Multiple attributes may be updated by passing an object to set().

Important: You must use set if you’re setting an attribute that doesn’t exist on the model yet.

let task = new Task({name: 'Write some tests!'})
Read
task.name;         // 'Write some tests!'
task.get('name');  // 'Write some tests!'

task.get('author', 'Unknown'); // 'Unknown'
Write
task.name = 'Write better tests';

// Set an attribute that doesn't exist on the model.
task.set('alias', 'Tests');

// Set multiple attributes
task.set({ name: 'My Task', description: 'Do something'});

Saved attributes

You can access the saved state with the saved(attribute, default) method or directly on the instance using the $ accessor. This is useful when you want to display a saved value while editing its active equivalent, for example when you want to show a task’s saved name in the list while editing the name (which is bound to the active state). If you don’t bind using $ when rendering the list, the task’s name will change on-the-fly as you type.

Important: You should never write to the saved state directly.

let task = new Task({name: 'Get some sleep?'})

// Update the active state.
task.name = 'Do more work!';

task.$.name;         // 'Get some sleep?'
task.saved('name');  // 'Get some sleep?'

task.saved('author', 'Unknown'); // 'Unknown'

Changed attributes

If you’d like to know which fields have changed since the last time the model was synced, you can call changed(), which returns either a list of attributes names or false if no values have changed.

v-model

You should always use the active state with v-model.

<input id="name" type="text"     v-model="task.name">
<input id="done" type="checkbox" v-model="task.done">

v-bind

You can use v-bind with either the active or the saved state.

<label>{{ task.$.name }}</label>

<!-- Edit the task's name here without affecting the label above. -->
<input id="name" type="text" v-model="task.name">

Mutators

You can define functions for each attribute to pass through before they are set on the model, which makes things like type coercion or rounding very easy.

Mutators are defined by a model’s mutations() method, which should return an object mapping attribute names to their mutator functions. You can use an array to create a pipeline, where each function will receive the result of the previous.

Mutator functions should accept value and model, and return the mutated value.

mutations() {
    return {
        id:   (id) => _.toNumber(id) || null,
        name: [(name, model) => _.trim(_.toString(name || model.get('name')))],
        done: (done) => Boolean(done),
    }
}

For mutations that should only run after a successful fetch, define afterFetchMutations() with the same format:

afterFetchMutations() {
    return {
        // Applied after fetch response data is assigned to the model.
        name: [_.toString, _.trim],
        domain: (domain, model) => domain || model.project?.domain,
    }
}

See options that determine when mutations should be applied.

Validation

Validation in vue-mc is powered by Vuelidate (@vuelidate/core and @vuelidate/validators). Rules are defined on the model via the validation() method in Vuelidate format; errors are synced to model.errors so your template can show them without using Vuelidate components directly.

Defining rules

Override validation() to return an object of rules per attribute, using validators from @vuelidate/validators. See the Vuelidate validators docs for the full list (e.g. required, email, integer, minValue, maxLength, etc.).

import { Model } from 'vue-mc'
import { required, integer, minValue } from '@vuelidate/validators'

class Task extends Model {
    defaults() {
        return { id: null, name: '', done: false }
    }

    validation() {
        return {
            id:   { integer, minValue: minValue(0) },
            name: { required },
            done: {},
        }
    }

    options() {
        return {
            validateOnChange: false,
            validateOnSave: true,
            validateRecursively: true,
        }
    }
}

API

Validation runs when you call save() (if validateOnSave is true), or when you call validate() / validateAttribute() yourself. Errors are stored on the model as model.errors (e.g. model.errors.name → array of strings).

Options

Option Default Description
validateOnSave true If true, the model is validated before save; save fails if validation fails.
validateOnChange false If true, the changed attribute is validated when its value changes.
validateRecursively true If true, nested models/collections with a validate method are validated as well.
useFirstErrorOnly false If true, only the first error per attribute is kept in model.errors.
$vuelidateContext — Optional Vue component instance (e.g. getCurrentInstance()) so useVuelidate works correctly inside a component.

Nested validation

If validateRecursively is true, attributes whose value has a validate method (e.g. another Model or Collection) are validated when you call validate(). If any nested validation fails, the parent’s validation also fails and errors are merged into the parent’s errors.

For custom messages and i18n, configure Vuelidate as described in the Vuelidate documentation.

Routes

Routes are defined in a model’s routes() method. Expected but optional route keys are fetch, save, and delete. Route parameters are returned by getRouteParameters(). By default, route values are URL paths that support parameter interpolation with curly-brace syntax, where the parameters are the model’s active attributes and options.

class Task extends Model {
    routes() {
        save: '/task/{id}',
    }
}

Note: You can use getURL(route, parameters = {}) to resolve a route’s URL directly.

Using a custom route resolver

A route resolver translates a route value to a URL. The default resolver assumes that route values are URL paths, and expects parameters to use curly-brace syntax. You can use a custom resolver by overriding getRouteResolver(), returning a function that accepts a route value and parameters.

For example, if you are using laroute, your base model might look like this:

class Task extends Model {
    routes() {
        save: 'task.save', // Defined in Laravel, eg. `/task/save/{id}`
    }

    getRouteResolver() {
        return laroute.route;
    }
}

Events

You can add event listeners using on(event, listener). Event context will always consist of at least target, which is the model that the event was emitted by. Event names can be comma-separated to register a listener for multiple events.

task.on('reset', (event) => {
   // Handle event here.
})

task.on('reset, sync', (event) => {
   // Handle event here.
})

reset

When the active state is reset to the saved state.

sync

When the active state is applied to the saved state.

change

When a value is set on the model that is different to what it was before. This event will also be emitted when a model is constructed with initial values.

create

After a model is saved successfully, where the model was created.

update

After a model is saved successfully, where the model was updated.

fetch

Before a model’s data is fetched. The request will be cancelled if any of the listeners return false, or skipped if the request is part of a batch within a collection.

Events will also be fired after the request has completed:

save

Before a model is saved. The request will be cancelled if any of the listeners return false, or skipped if the request is part of a batch within a collection.

Events will also be fired after the request has completed:

delete

Before a model is deleted. The request will be cancelled if any of the listeners return false, or skipped if the request is part of a batch within a collection.

Events will also be fired after the request has completed:

Requests

Models support three actions: fetch, save and delete. These are called directly on the model, and return a Promise. The resolve callback will receive a response which could be null if a request was cancelled. The reject callback will receive an error which should always be set.

// These are the default options, which will be merged with those provided.
let options = {
    url     : this.getSaveURL(),
    method  : this.getSaveMethod(),
    data    : this.getSaveData(),
    params  : this.getSaveQuery(),
    headers : this.getSaveHeaders(),
};

model.save(options).then((response) => {
    // Handle success here
}).catch((error) => {
    // Handle failure here
})

Fetch

Model data is usually either already in scope or part of a collection of data, but you can also fetch attribute data for a specific model. Response data is expected to be an object of attributes which will replace the active attributes and sync the saved state.

When a fetch request is made, loading will be true on the model, and false again when the data has been received and assigned (or if the request failed). This allows the UI to indicate a loading state.

The request will be ignored if loading is already true when the request is made.

Save

Saving a model either performs a create or update, depending on whether the model is already persisted. The default criteria that qualifies a response as created is to check if the model now has an identifier but didn’t before, or if the response status was a 201 Created.

If the model was created, it will automatically be added to all registered collections.

If you’d like to enable patching you can override shouldPatch(). The request will then only consist of attributes that have changed, and abort early if no changes have been made.

When a save request is made, saving will be true on the model, and false again when the response has been handled (or if the request failed).

The request will be ignored if saving is already true when the request is made, which prevents the case where clicking a save button multiple times results in more than one request.

Response

In most case you would return the saved model in the response, so that server-generated attributes like date_created or id can be applied.

Response data should be either an object of attributes, an identifier, or nothing at all.

If an object is received, it will replace the active attributes and sync the saved state.

If an identifier is returned it will only set the identifier. You can override identifier handling with parseIdentifier(data) and isValidIdentifier(identifier).

If the response is empty, it will be assumed that the active state is already the source of truth. This makes sense when the model doesn’t use any server-generated attributes and doesn’t use an identifier.

Validation

Validation errors are automatically set on a model and can be accessed as model.errors. They are set when a save request receives a 422 Unprocessable Entity response (or the status configured via the validationErrorStatus option). You can adjust how this is determined by overriding isBackendValidationError(error).

Backend validation errors should be an object of arrays keyed by attribute name, for example:

{
    email: [
        "Should be a valid email address",
    ],
}

Important: This structure is applied to model.errors so it matches what you get from client-side Vuelidate validation.

Delete

When a delete request is made, deleting will be true on the model, and false again when the response has been handled (or if the request failed).

The request will be ignored if deleting is already true when the request is made.

The model will automatically be removed from all registered collections if the request is successful.

Custom

You can also create custom requests to perform custom actions.

class Channel {
    ...

    options() {
        return {
            methods: {
                subscribe: 'POST',
            }
        }
    }
    
    routes() {
        return {
            subscribe: 'channel.subscribe.user',
        }
    }

    subscribe(user) {
        let method = this.getOption('methods.subscribe');
        let route  = this.getRoute('subscribe');
        let params = this.getRouteParameters();
        let url    = this.getURL(route, params);
        let data   = {user: user.id},
        
        return this.createRequest({method, url, data}).send();
    }
}

let channel = new Channel({id: 1});
let user    = new User({id: 1});

channel.subscribe(user).then(() => {
    // Handle success here
}).catch((error) => {
    // Handle failure here
});

getRouteParameters will return the model’s attributes by default, but you can adjust this in the custom request method or override the method in your model. It should return an object to be used for route parameter interpolation.

Events

Events for save, fetch, and delete will be emitted on the model after a request has completed:

model.on('save', (event) => {
    // event.error will be set if the action failed
})

Collections

Creating collections

Collection instances can be created using the new keyword. The default constructor for a collection accepts two optional parameters: models and options.

let collection = new Collection(models = [], options = {});

Models

You can provide initial models as an array of either model instances or plain objects that should be converted into models. This follows the same behaviour as add, where a plain object will be passed as the attributes argument to the collection’s model type.

Options

The options parameter allows you to set the options of a collection instance. To get the value of an option, use getOption(name). You can also set an option later on using setOption(name, value) or setOptions(options).

You should define a collection’s default options using the options() method:

class TaskList extends Collection {
    options() {
        return {
            model: Task,
            useDeleteBody: false,
        }
    }
}

Available options

Option Type Default Description
model Class Model The class/constructor for this collection’s model type.
methods Object   HTTP request methods.
routeParameterPattern Regex /\{([^}]+)\}/ Route parameter group matching pattern.
useDeleteBody Boolean true Whether this collection should send model identifiers as JSON data in the body of a delete request, instead of a query parameter.
Default request methods
{
    ...

    "methods" {
        "fetch":  "GET",
        "save":   "POST",
        "delete": "DELETE",
    }
}

The collection’s model type can also be determined dynamically by overriding the model method, which by default returns the value of the model option.

class TaskList extends Collection {
    model() {
        return Task;
    }
}

Attributes

The attributes parameter allows you to set custom attributes on the the collection, much like a model. You can also use get and set to manage attributes. This is useful for route parameters or custom states like “editing”.

Attributes are included as route parameters by default.

You should define a collection’s default attributes using the defaults() method:

class TaskList extends Collection {
    defaults() {
        return {
            orderBy: 'name',
        }
    }
}

let list = new TaskList();

// Read
list.get('orderBy'); // "name"

// Write
list.set('orderBy', 'id');

Models

Models in a collection is an array that can be accessed as models on the collection.

let task1 = {name: '1'};
let task2 = {name: '2'};

let tasks = new TaskList([task1, task2]);

tasks.models; // [<Task>, <Task>]

Add

You can add one or more models to a collection using add(model), where model can be a model instance, plain object, or array of either. Plain objects will be used as the attributes argument of the collection’s model option when converting the plain object into a model instance.

Adding a model to a collection automatically registers the collection on the model, so that the model can be removed when deleted successfully.

If no value for model is given, it will add a new empty model.

The added model will be returned, or an array of added models if more than one was given.

let tasks = new TaskCollection();

// Adding a plain object.
let task1 = tasks.add({name: 'First'});

// Adding a model instance.
let task2 = tasks.add(new Task({name: 'Second'}));

// Adding a new empty model
let task3 = tasks.add();

let added = tasks.add([{name: '#4'}, {name: '#5'}]);
let task4 = added[0];
let task5 = added[1];

tasks.models; // [task1, task2, task3, task4, task5]

Remove

You can remove a model instance using the remove(model) method, where model can be a model instance, plain object, array, or function. Passing a plain object or function will use filter to determine which models to remove, and an array of models will call remove recursively for each element.

All removed models will be returned as either a single model instance or an array of models depending on the type of the argument. A plain object, array or function will return an array, where removing a specific model instance will only return that instance, if removed.

// Remove all tasks that have been completed.
let done = tasks.remove({done: true});

// Adding a model instance.
let task2 = tasks.add(new Task({name: 'Second'}));

// Adding a new empty model
let task3 = tasks.add();

let added = tasks.add([{name: '#4'}, {name: '#5'}]);
let task4 = added[0];
let task5 = added[1];

tasks.models; // [task1, task2, task3, task4, task5]

Replace

You can replace the models of a collection using replace(models), which is effectively clear followed by add(models). Models are replaced when new data is fetched or when the constructor is called.

Routes

Like models, routes are defined in a collection’s routes() method. Expected but optional route keys are fetch, save, and delete. Route parameters are returned by getRouteParameters(). By default, route values are URL paths that support parameter interpolation with curly-brace syntax, where the parameters are the collection’s attributes and current page.

class Task extends Model {
    routes() {
        save: '/task/{id}',
    }
}

Note: You can use getURL(route, parameters = {}) to resolve a route’s URL directly.

Using a custom route resolver

A route resolver translates a route value to a URL. The default resolver assumes that route values are URL paths, and expects parameters to use curly-brace syntax. You can use a custom resolver by overriding getRouteResolver(), returning a function that accepts a route value and parameters.

For example, if you are using laroute, your base collection might look like this:

class TaskList extends Collection {
    routes() {
        save: 'tasks.save', // Defined in Laravel, eg. `/tasks`
    }

    getRouteResolver() {
        return laroute.route;
    }
}

Events

You can add event listeners using on(event, listener). Event context will always consist of at least target, which is the collection that the event was emitted by. Event names can be comma-separated to register a listener for multiple events.

tasks.on('add', (event) => {
   console.log("Added", event.model);
})

tasks.on('remove', (event) => {
   console.log("Removed", event.model);
})

add

When a model has been added.

remove

When a model has been removed.

fetch

A fetch event will be emitted when a collection has fetched its model data, for successful and failed requests. The event context will have an error attribute, which is null if the request was successful and an Error if it failed.

save

A save event will be emitted when a collection has fetched its model data, for successful and failed requests. The event context will have an error attribute, which is null if the request was successful and an Error if it failed.

delete

A delete event will be emitted when a collection has fetched its model data, for successful and failed requests. The event context will have an error attribute, which is null if the request was successful and an Error if it failed.

Requests

Collections support three actions: fetch, save and delete. These are called directly on the collection, and return a Promise. The resolve callback will receive a response which could be null if a request was cancelled. The reject callback will receive an error which should always be set.

collection.save().then((response) => {
    // Handle success here
}).catch((error) => {
    // Handle failure here
})

Fetch

You can fetch model data that belongs to a collection. Response data is expected to be an array of attributes which will be passed to replace.

When a fetch request is made, loading will be true on the collection, and false again when the data has been received and replaced (or if the request failed). This allows the UI to indicate a loading state.

The request will be ignored if loading is already true when the request is made.

Save

Instead of calling save on each model individually, a collection will encode its models as an array, using the results of each model’s getSaveBody() method, which defaults to the model’s attributes. You can override getSaveBody() on the collection to change this behaviour.

When a save request is made, saving will be true on the collection, and false again when the response has been handled (or if the request failed).

The request will be ignored if saving is already true when the request is made, which prevents the case where clicking a save button multiple times results in more than one request.

Response

In most case you would return an array of saved models in the response, so that server-generated attributes like date_created or id can be applied.

Response data should be either an array of attributes, an array of model identifiers, or nothing at all.

If an array is received, it will apply each object of attributes to its corresponding model in the collection. A strict requirement for this to work is that the order of the returned data must match the order of the models in the collection.

If the response is empty, it will be assumed that the active state of each model is already its source of truth. This makes sense when a model doesn’t use any server-generated attributes and doesn’t use an identifier.

Validation

Validation errors must be either an array of each model’s validation errors (empty if there were none), or an object that is keyed by model identifiers. These will be applied to each model in the same way that the model would have done if it failed validation on save.

// An array of errors for each model.
[
    {},
    {},
    {name: ['Must be a string']},
    {},
    ...
]

// An object of errors keyed by model identifier
{
    4: {name: ['Must be a string']},
}

Delete

Instead of calling delete on each model individually, a collection collects the identifiers of models to be deleted. If the useDeleteBody option is true, identifiers will be sent in the body of the request, or in the URL query otherwise.

When a delete request is made, deleting will be true on the collection, and false again when the response has been handled (or if the request failed).

The request will be ignored if deleting is already true when the request is made.

Custom

You can also create custom requests to perform custom actions.

let config = {
    // url
    // method
    // data
    // params
    // headers
};

return this.createRequest(config).send().then(() => {
    // Handle success here
}).catch((error) => {
    // Handle failure here
});

Events

Events for save, fetch, and delete will be emitted on the collection after a request has completed:

collection.on('save', (event) => {
    // event.error will be set if the action failed
})

Pagination

You can enable pagination using the page(integer) method, which sets the current page. If you want to disable pagination, pass null as the page. The page method returns the collection so you can chain fetch if you want to.

A collection’s page will automatically be incremented when a paginated fetch request was successful. Models will be appended with add rather than replaced.

You can use isLastPage() or isPaginated() to determine the current status. A collection is considered on its last page if the most recent paginated fetch request did not return any models. This is reset again when you call page to set a new page. A paginated fetch will be cancelled if the collection is on its last page.

Infinite scrolling can be achieved by calling page(1) initially, then fetch() while isLastPage() is false.

collection.page(1).fetch(() => {
    // Done.
});

Methods

There are some convenient utility methods that makes some aggregation and processing tasks a bit easier.

first

Returns the first model in the collection, or undefined if empty.

last

Returns the last model in the collection, or undefined if empty.

shift

Removes and returns the first model of this collection, or undefined if the collection is empty.

pop

Removes and returns the last model of this collection, or undefined if the collection is empty.

find

Returns the first model that matches the given criteria, or undefined if none could be found. See _.find.

// Returns the first task that has not been completed yet.
tasks.find({done: false});

// Returns a task that is done and has a specific name.
tasks.find((task) => {
   return task.done && _.startsWith(task.name, 'Try to');
});

has

Returns true if the collection contains the model or has at least one model that matches the given criteria. This method effectively checks if indexOf returns a valid index.

sort

Sorts the models of the collection in-place, using _.sortBy. You will need to pass an attribute name or function to sort the models by.

// Sorts the task list by name, alphabetically.
tasks.sort('name');

sum

Returns the sum of all the models in the collection, based on an attribute name or mapping function. See _.sumBy.

// Returns the number of tasks that have been completed.
tasks.sum('done');

// Returns the total length of all task names.
tasks.sum((task) => {
    return task.name.length;
});

map

Returns an array that contains the returned result after applying a function to each model in this collection. This method does not modify the collection’s models, unless the mapping function does so. See _.map.

// Returns an array of task names.
tasks.map('name');

// Returns an array of the words in each task's name.
tasks.map((task) => _.words(task.name));

count

Returns an object composed of keys generated from the results of running each model through iteratee. The corresponding value of each key is the number of times the key was returned by iteratee. See _.countBy.

// Count how many tasks are done, or not done.
tasks.count('done'); // { true: 2, false: 1 }

// Adjust the keys.
tasks.count((task) => (task.done ? 'yes' : 'no')); // { "yes": 2, "no": 1 }

reduce

Reduces the collection to a value which is the accumulated returned result of each model passed to a callback, where each successive invocation is supplied the return value of the previous. See _.reduce.

If initial is not given, the first model of the collection is used as the initial value.

The callback will be passed three arguments: result, model, and index.

// Returns a flat array of every word of every task's name.
tasks.reduce((result, task) => _.concat(result, _.words(task.name)), []);

each

Iterates through all models, calling a given callback for each one. The callback function receives model and index, and the iteration is stopped if you return false at any stage. See _.each.

// Loop through each task, logging its details.
tasks.each((task, index) => {
    console.log(index, task.name, task.done);
})

filter

Creates a new collection of the same type that contains only the models for which the given predicate returns true for, or matches by property. See _.filter.

// Create two new collections, one containing tasks that have been completed,
// and another containing only tasks that have not been completed yet.
let done = tasks.filter((task) =>   task.done);
let todo = tasks.filter((task) => ! task.done);

// You can also use an attribute name only.
let done = tasks.filter('done');

where

Returns the models for which the given predicate returns true for, or models that match attributes in an object. This is very similar to filter, but doesn’t create a new collection, it only returns an array of models.

indexOf

Returns the first index of a given model or attribute criteria, or -1 if a model could not be found. See _.findIndex.



Back to the top