Backbone.Facetr

npm npm Bower Build Status Coverage Status

CONTENTS

INTRODUCTION

Backbone.Facetr is an utility which enables filtering of Backbone collections through facets. It can be used to implement faceted search, which follows the faceted classification system.

It works flawlessly up to 2500 items; computation starts getting slower with 5000 - 10000 items. This is however an early version; optimizations may improve performance in future realeases.

INSTALLATION

Production bundles can be found in the dist folder. Facetr is distributed via bower and npm. Both AMD and CommonJS are supported.

npm

npm install backbone.facetr --save

bower

bower install Backbone.Facetr --save

build from source

git clone git@github.com:arillo/Backbone.Facetr.git # clone repo
cd Backbone.Facetr # navigate to local repo
npm install -g grunt-cli # install grunt globally
npm install # install dependencies
grunt # build to dist folder

Include in your code using any of the following methods

AMD

define('yourmodule', ['backbone.facetr'], function(Facetr){
    // ... 
});

CommonJS

var Facetr = require('backbone.facetr');

script tag

<!-- deps -->
<script src="path/to/libs/backbone.js"></script>
<script src="path/to/libs/underscore.js"></script>
<!-- facetr -->
<script src="path/to/libs/backbone.facetr.js"></script>

BASIC USAGE

// create a collection with few test items
var collection = new Backbone.Collection([
    {
        'Name'      : {
            'FirstName' : 'Bob',
            'LastName' : 'Smith'
        },
        'Age'       : 20,
        'Country'   : 'Australia',
        'Hobbies'   : ['fishing','painting','playing the ukulele'],
        'Profession': 'manager'
    },
    {
        'Name'      : {
            'FirstName' : 'Otto',
            'LastName'  : 'Von Braun'
        },
        'Age'       : 35,
        'Country'   : 'New Zealand',
        'Hobbies'   : ['drawing', 'painting', 'shopping'],
        'Profession': 'team manager'
    },
    {
        'Name'      : {
            'FirstName' : 'Sarah',
            'LastName'  : 'Smith'
        },
        'Age'       : 28,
        'Country'   : 'Ireland',
        'Hobbies'   : ['shopping','painting'],
        'Profession': 'project manager'
    }
]);

Facetr(collection).facet('Name.LastName').value('Smith'); // collection contains 'Sarah Smith' and 'Bob Smith'
Facetr(collection).facet('Hobbies').value('shopping'); // contains only 'Sarah Smith' 
Facetr(collection).facet('Name.LastName').removeValue('Smith'); // contains 'Sarah Smith' and 'Otto Von Braun'

// removes all facet values and restores original collection content
Facetr(collection).clearValues();

// if chaining is not your cup of tea, the following is equivalent to the above code
var facetCollection = Facetr(collection); // returns a FacetCollection object
var lastNameFacet = facetCollection.facet('Name.LastName'); // returns a Facet object
var hobbiesFacet = facetCollection.facet('Hobbies');

lastNameFacet.value('Smith'); // returns a FacetExp object
hobbiesFacet.value('shopping');
lastNameFacet.removeValue('Smith');

// read the API Reference section for more
// most examples will use the above collection reference to illustrate functionalities

DOT NOTATION

Syntax: PropertyName{1}(.PropertyName)*

In the context of Facetr, Dot Notation refers to the syntax used to define facets on a collection. Using the Facetr Dot Notation it is possible to define facets on properties of a Model, as well as on properties of its properties.

For example, consider the following model:

var model = new Backbone.Model({
    'Name' : {
        'FirstName' : 'John',
        'LastName' : ['Smith','White']
    },
    'City' : 'London',
    'Age' : 45,
    'FamilyMembers' : [
        { 'Name' : 'Robert' },
        { 'Name' : 'Margaret' }
    ]
});

To add a facet on property 'FirstName' the following expression in Dot Notation syntax can be used: 'Name.FirstName'.

Facetr(collection).facet('Name.FirstName');

Theoretically there is no depth limit for a Dot Notation expression (e.g. PropertyName1.PropertyName2...PropertyNameN), the only limitation being the common sense.

A facet can only be added on a Backbone Model or object property having any of the following value types:

It cannot be added on properties having the following value / composite types:

.. and in any other case not mentioned in the list of allowed value types

EXAMPLE

Facetr(collection).facet('City');               // valid, City is a string
Facetr(collection).facet('Age');                // valid, Age is a Number
Facetr(collection).facet('Name.LastName');      // valid, LastName is an Array of strings
Facetr(collection).facet('FamilyMembers.Name'); // valid, takes value of Name of each object in array FamilyMembers
Facetr(collection).facet('Name');               // error, Name is an object

// etc..

OPERATORS

Facets filtering can be combined using logical operators 'or' and 'and', both between different facets (external operator, see FacetCollection.facet method) and between values of one facet (internal operator, see Facet.value method and FacetExp).

External Operator

// create a facet with default external operator 'and'
var facetCountry = Facetr(collection).facet('Country');

// or set the external operator explicitly
var facetLastName = Facetr(collection).facet('Name.LastName', 'and');

// when filtering, such operator will be used to combine filters of the two facets
facetCountry.value('Australia');
facetLastName.value('Smith');

// collection contains all models with Country equal 'Australia' and LastName equal 'Smith'

Internal Operator

// add a value with default internal operator 'or'
facetCountry.value('Australia');

// or set the internal operator explicitly
facetCountry.value('Ireland', 'or');

// it is possible to use chaining syntax to combine values
facetCountry.value('Australia').or('Ireland');

// collection contains all models with Country equal either 'Australia' or 'Ireland'

API Reference

Facetr

Facetr(collection:Backbone.Collection, [id:string]) : FacetCollection

Initialize a Collection to be used with Facetr. The first building block of any Facetr expression. Returns the created FacetCollection instance for method chaining. An id can be associated with the collection.

Example

Facetr(collection);

// or also
Facetr(collection, 'myCollection');

// which enables the following syntax
Facetr('myCollection') === Facetr(collection); // true

FacetCollection

facet(dotNotationExpr:string, [externalOperator:string], [silent:boolean]) : Facet

Adds a Facet on the given collection using the property refered to by the Dot Notation expression (see Dot Notation section for more details). Valid external operator values are: 'or' and 'and' (default is 'and'). Returns the created Facet instance to allow method chaining. Triggers a facet event with the facetName passed to the callback, unless true is passed as last parameter.

Example

Facetr(collection).on('facet', function(facetName) {
    console.log('added facet', facetName);
});

// add facet on property 'Age' using default operator ('and')
Facetr(collection).facet('Age');

// add facet on property 'LastName' of object 'Name' using 'or' operator
Facetr(collection).facet('Name.LastName', 'or');

// console output would be
// added facet Age
// added facet Name.LastName
toJSON() : Array

Returns an array containing objects representing the status of the facets added to the collection. Useful for rendering out facets lists. Each object in the array is the result of invoking toJSON on each Facet (see Facet documentation below for the Facet.toJSON method).

Example

// create a collection with two models
var collection = new Backbone.Collection([
    {
        'Name'      : {
            'FirstName' : 'Bob',
            'LastName' : 'Smith'
        },
        'Age'       : 20
    },
    {
        'Name'      : {
            'FirstName' : 'Otto',
            'LastName'  : 'Von Braun'
        },
        'Age'       : 35
    }
]);

Facetr(collection).facet('Name.FirstName').label('First Name');
Facetr(collection).facet('Name.FirstName').value('Bob');

var json = Facetr(collection).toJSON();

// value of json will be an array of Facet data object with the following format:
//
// [
//      {
//          data : {
//              extOperator : 'and',
//              intOperator : 'or',
//              label       : 'First Name',
//              name        : 'Name.FirstName',
//              selected    : true,
//              sort        : {
//                  by        : 'value',
//                  direction : 'asc'
//              }
//              customData : {}
//          },
//          values : [
//              {
//                  active      : true,
//                  activeCount : 1,
//                  count       : 1,
//                  value       : 'Bob'
//              },
//              {
//                  active      : false,
//                  activeCount : 0,
//                  count       : 1,
//                  value       : 'Otto'
//              }
//          ]
//      }
// ]
clear([silent:boolean]) : FacetCollection

Removes all the facets added to the collection and unfilters it accordingly. Use this method to reset the original models in the collection. Triggers a clear event, unless true is passed as parameter.

Example

Facet(collection).on('clear', function() {
    console.log('All facets were removed');
});

Facet(collection).clear();

// console output:
// All facets were removed
remove() : undefined

Removes all the facets and the facetrid added on the collection id upon Facetr(collection) initialization. Resets the original items in the collection.

Example

Facetr(collection).remove()
sortBy(attribute:string, [silent]) : FacetCollection

Sorts the collection according to the given attribute name. By default ascendent sort is used. See asc() and desc() methods below to define sort direction. Triggers sort event unless true is passed as last parameter. This method automatically recognizes string, numeric or date values.

Example

Facetr(collection).on('sort', function(attr, dir) {
    console.log('Sorting by ' + attr + ' ' + dir);
});

Facetr(collection).sortBy('Age');

// console output:
// Sorting by Age asc
asc([silent:boolean]) : FacetCollection

Sorts the collection in ascendent order by the attribute selected using sortBy method. If sortBy was not invoked before, this method has no effect. Triggers sort event unless true is passed as parameter.

Example

Facetr(collection).sortBy('Age').asc();
desc([silent:boolean]) : FacetCollection

Sorts the collection in descendent order by the attribute selected using sortBy method. If sortBy was not invoked before, this method has no effect. Triggers sort event unless true is passed as parameter.

Example

Facetr(collection).sortBy('Age').desc();
addFilter(filterName:string, filter:function, [silent:boolean]) : FacetCollection

Adds a filter which is used to filter the collection by testing each model against it. Triggers Backbone.Collection reset unless true is passed as last parameter. Multiple filters can be added as long as they have different names. Adding two filters with the same name will result in the first being overwritten by the second.

Example

Facetr(collection).addFilter('AgeFilter', function(model) {
    return model.get('Age') >= 20 && model.get('Age') < 60; 
});
removeFilter(filterName:string, [silent:boolean]) : FacetCollection

Removes the filter with the given name from the collection and unfilters it accordingly. Triggers reset unless true is passed as last parameter.

Example

Facetr(collection).removeFilter('AgeFilter');
clearFilters([silent:boolean]) : FacetCollection

Removes all the filters previously added to the collection and unfilters it accordingly. Triggers reset unless true is passed as parameter.

Example

Facetr(collection).clearFilters();
clearValues([silent:boolean]) : FacetCollection

Removes all the currently selected values from all the facets, bringing the collection to its initial state. Triggers a clearValues event unless true is passed as parameter.

Example

Facetr(collection).clearValues();
facetsOrder(facetNames:Array, [silent:boolean]) : FacetCollection

Sometimes it is convinient to give the facets list a predefined order. This method can be used to achieve this by passing an array of facet names which corresponds to the order to be given to the facets in the list. Triggers a 'facetsOrder' event with the facetNames array passed to the event handler, unless true is given as last parameter.

Example

Facetr(collection).facet('Age');
Facetr(collection).facet('Name.FirstName');

// Facetr(collection).toJSON() has facet 'Age' at index 0 and 'Name.FirstName' at index 1

Facetr(collection).facetsOrder(['Name.FirstName', 'Age']);

// Facetr(collection).toJSON() has facet 'Name.FirstName' at index 0 and 'Age' at index 1
collection() : Backbone.Collection

Returns the reference to the Backbone.Collection. Useful for cases where the reference is needed but it was declared in another scope and is no more accessible.

Example

Facetr(collection).collection() === collection; // true
origLength() : Number

Returns the length of the collection before any faceted filtering was applied.

Example

var collection = new Backbone.Collection([
    {
        Name : 'John'
        Age  : 19
    },
    {
        Name : 'Sarah',
        Age  : 35
    }
]);

collection.length() // 2

Facetr(collection).facet('Age').value(35);

collection.length(); // 1
Facetr(collection).origLength(); // 2
facets() : Array

Returns an array containing all the Facet instances created on this FacetCollection.

settingsJSON() : object

Returns an object representation of the current state of the Facetr collection which can be used to reload the same state in future using the initFromSettingsJSON method.

Example

Facetr(collection).facet('Name.FirstName').label('First Name');
Facetr(collection).sortBy('Name.FirstName').asc();
Facetr(collection).facet('Name.FirstName').value('Bob');

var json = Facetr(collection).settingsJSON();

// value of json will be the following:
//  
// {
//      sort : {
//          by  :   "Name.FirstName",
//          dir :   "asc"
//      },
//      facets : [
//          {
//              attr    :   "Name.FirstName",
//              eop     :   "and",
//              iop     :   "or",
//              vals    :   [ "Bob" ]
//          }
//      ]
//  }
initFromSettingsJSON(json:object) : FacetCollection

Initializes the Facetr collection using a settings object generated using the settingsJSON method. Triggers an "initFromSettingsJSON" event.

Facet

value(value:string, [internalOperator:string], [silent:boolean]) : FacetExp

Adds a value to the facet. This will result in the collection being filtered by { FacetName : 'Value' }. Valid internal operator values are: 'or' and 'and' (default is 'or'). Triggers a 'filter' event on the FacetCollection passing facetName and facetValue to the handler and a 'value' event on the Facet passing the facetValue to the handler, unless true is passed as last parameter.

Example

Facetr(collection).on('filter', function(facetName, facetValue) {
    console.log('filtered by '+ facetName + ' with value equal ' + facetValue);
});

var facet = Facetr(collection).facet('Name.FirstName');

facet.on('value', function(facetValue){
    console.log('the following value was added to the facet: ' + facetValue);
});

facet.value('Bob');

// console output: "filtered by Name.FirstName with value Bob"
// collection contains only models with FirstName = 'Bob'
removeValue(value:string, [silent:boolean]) : FacetExp

Removes the given value from the facet and resets the collection to the state previous of the filtering caused by the removed value. Triggers a 'unfilter' event on the FacetCollection passing facetName and facetValue to the handler and a 'removeValue' event on the Facet passing the facetValue to the handler, unless true is passed as last parameter.

Example

Facetr(collection).on('unfilter', function(facetName, facetValue) {
    console.log('unfiltered by '+ facetName + ' with value equal ' + facetValue);
});

var facet = Facetr(collection).facet('Name.FirstName');

facet.on('removeValue', function(value){
    console.log('the following value was removed from the facet: ' + value);
});

facet.removeValue('Bob');

// console output: "unfiltered by Name.FirstName with value Bob"
// collection contains again also models with FirstName = 'Bob'
toJSON() : object

Returns an object representation of the current facet data and values. Useful for rendering the facet to the page.

Example

// create a collection with two models
var collection = new Backbone.Collection([
    {
        'Name'      : {
            'FirstName' : 'Bob',
            'LastName' : 'Smith'
        },
        'Age'       : 20
    },
    {
        'Name'      : {
            'FirstName' : 'Otto',
            'LastName'  : 'Von Braun'
        },
        'Age'       : 35
    }
]);

Facetr(collection).facet('Name.FirstName').label('First Name');
Facetr(collection).facet('Name.FirstName').value('Bob');

var json = Facetr(collection).facet('Name.FirstName').toJSON();

// json is equal to:
//
//  {
//      data : {
//          extOperator : 'and',
//          intOperator : 'or',
//          label       : 'First Name',
//          name        : 'Name.FirstName',
//          selected    : true,
//          sort        : {
//              by        : 'value',
//              direction : 'asc'
//          }
//          customData : {}
//      },
//      values : [
//          {
//              active      : true,
//              activeCount : 1,
//              count       : 1,
//              value       : 'Bob'
//          },
//          {
//              active      : false,
//              activeCount : 0,
//              count       : 1,
//              value       : 'Otto'
//          }
//      ]
//  }
label(label:string) : Facet

Use this method to set a human readable label for the facet. This can be used when rendering the facet on the page.

Example

Facetr(collection).facet('Name.FirstName').label('First Name');

Facetr(collection).facet('Name.FirstName').toJSON();

// the property data.label has value 'First Name'
// while the property data.name stays 'Name.FirstName'
sortByCount() : Facet

Sorts the facet values by their count. The count is the number of models in the original collection with an attribute having as name the facet name and value the given value.

Example

// we use the colleciton defined in Basic Usage section
var facet = Facetr(collection).facet('Hobbies');

facet.value('painting'); // painting count = 3  
facet.value('drawing');  // drawing count = 1

// facet.toJSON().data.values[0] = 'drawing'
// facet.toJSON().data.values[1] = 'painting'

facet.sortByCount();

// facet.toJSON().data.values[0] = 'painting'
// facet.toJSON().data.values[1] = 'drawing'
sortByActiveCount() : Facet

Sorts the facet values by their active count. The active count is the number of models in the current filtered collection with an attribute having as name the facet name and value the given value.

Example

var facet = Facetr(collection).facet('Hobbies');

facet.value('fishing');
facet.value('shopping');

// fishing active count = 1
// shopping active count = 2

// facet.toJSON().data.values[0] = 'fishing'
// facet.toJSON().data.values[1] = 'shopping'

facet.sortByActiveCount();

// facet.toJSON().data.values[0] = 'shopping'
// facet.toJSON().data.values[1] = 'fishing'
sortByValue() : Facet

Sorts the facet values by their value. This is the default sort.

asc() : Facet

Sets the direction of the values sort to ascendent.

Example

Facetr(collection).facet('Name.FirstName').asc();
desc() : Facet

Sets the direction of the values sort to descendant.

Example

Facetr(collection).facet('Name.FirstName').sortByCount().desc();
remove([silent:boolean]) : undefined

Removes the facet and all its values and unfilters the collection accordingly. It triggers a "removeFacet" event on the facet collection, unless true is passed as parameter.

var facetCollection = Facetr(collection);
var facet = facetCollection.facet('Age');

facetCollection.on('removeFacet', function(facetName){
    console.log('removed facet', facetName);
});

facet.remove();

// console output would be
// removed facet Age
clear([silent:boolean]) : Facet

Unselects all the values from the facet. It triggers a 'clear' event unless true is passed as parameter.

customData(key:string, [value:object]) : Facet

This method can be used to add arbitrary data to pass to the templates. Data added using this method is included in the object returned by the toJSON() method in the data.customData property. To retrieve previously set data, just pass the key parameter without any value.

Example

var facet = Facetr('myCollection').facet('Hobbies').customData('sublabel', 'Available hobbies');

// facet.customData('sublabel') returns 'Available hobbies'
// facet.toJSON().data.customData = { sublabel : 'Available hobbies' }
isSelected : Boolean

Returns true if any value is selected from this facet, false otherwise.

Example

var facet = Facetr('myCollection').facet('Hobbies');

facet.value('fishing');

// facet.isSelected() returns true

facet.clear(); // remove all selected values

// facet.isSelected() returns false
hierarchy(hierarchySettings:Array) : Facet

This method creates a hierarchical representation of facet values, based on the settings object given as parameter. Once this method is used, the facet becomes a "hierarchical" facet. You can then use the facet as you usually would, with the following differences:

groupedValues

[
    {
        value: "manager",
        label: "Manager",
        active: false,
        activeCount: 7,
        count: 7,
        groups: [
            {
                value: "team manager",
                label: "Team Manager",
                active: false,
                activeCount: 5,
                count: 5,
                groups: [
                    { ... other groups values ... }
                ]
            },
             {
                value: "manager",
                label: "Manager",
                active: false,
                activeCount: 5,
                count: 5 // no groups property means that this value has no children groups
            },
        ]
    }
]

The hierarchySettings argument is an array with different "hierarchical value" objects, where it is possible to specify the hierarchy of values along with a label. See the Example below to understand the hierarchySettings format.

Example

    var hierarchySettings = [
        {               
            value: "manager",
            label: "Manager",
            groups: [
                {
                    value: "project manager",
                    label: "Project Manager",
                    groups: [
                        {
                            value: "team manager",
                            label: "Team Manager"
                        }
                    ]
                }
            ]
        }
    ];

    var profession = Facetr(collection).facet('Profession').hierarchy(hierarchySettings);

    var json = profession.toJSON();

    // value of json variable would be:
    //
    // {
    //  "data": {
    //      "name":"Profession",
    //      "hierarchical":true,
    //      "label":"Profession",
    //      "extOperator":"and",
    //      "intOperator":"or",
    //      "sort":{
    //          "by":"activeCount",
    //          "direction":"desc"
    //      },
    //      "selected":false,
    //      "customData":{}
    //  },
    //  "values": [ ... stays as the normal facets ... ],
    //  "groupedValues":[
    //      {
    //          "value":"manager",
    //          "label":"Manager",
    //          "active":false,
    //          "activeCount":3,
    //          "count":3,
    //          "groups":[
    //              {
    //                  "value":"project manager",
    //                  "label":"Project Manager",
    //                  "active":false,
    //                  "activeCount":2,
    //                  "count":2,
    //                  "groups":[
    //                      {
    //                          "value":"team manager",
    //                          "label":"Team Manager",
    //                          "active":false,
    //                          "activeCount":1,
    //                          "count":1
    //                      }
    //                  ]
    //              }
    //          ]
    //      }
    //  ]
    // }

FacetExp

FacetExp objects are returned from Facet value and removeValue methods. They can be used to coincisely define multiple values on a facet, using different operators.

Example

Facetr(collection).facet('Age').value(12, 'and');
Facetr(collection).facet('Age').value(15, 'or');
Facetr(collection).facet('Age').value(39, 'and');

// can also be expressed with the following syntax

Facetr(collection).facet('Age').value(12, 'and').or(15).and(39);
and(value:string, [silent:boolean]) : Facet

Equivalent to facet.value('Value', 'and'), but can be used for FacetExp chains.

or(value:string, [silent:boolean]) : Facet

Equivalent to facet.value('Value', 'or'), but can be used for FacetExp chains.

EXAMPLES

This section contains a list of websites / projects using Facetr.

LICENSE

Backbone.Facetr may be freely distributed under the MIT license.

Copyright (C) 2012 Arillo GmbH http://arillo.net

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.