npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

backbone.prism

v1.3.2

Published

Flux-like architecture for Backbone.js

Downloads

23

Readme

Backbone.Prism

Build Status

Flux architecture for Backbone.js

Backbone.Prism features a Flux based architecture combining Backbone.js and React.

bower install backbone.prism --save

npm install backbone.prism --save

// Prism.Store is a 'viewable' Backbone.Collection let store = new Prism.Store([ { name: 'Eiffel Tower', location: 'France' }, { name: 'Taj Mahal', location: 'India' }, { name: 'Louvre Museum', location: 'France' }, { name: 'Machu Picchu', location 'Peru' } ]);

// Create a view only holding a particular set of data let view = store.createView({ name: 'france', filter: model => { return model.get('location') === 'France'; } });

// Make models available in all views store.publish();

console.log(view.length); // prints '2'


<br>
When a `Store` instance calls the `publish` method, all `store views` will start listening for changes. Any element added/removed/modified on the store will trigger a sync routine.

<br>
```javascript
// Adding an element to a store will trigger an event
store.add({
  name: 'Arc de Triomphe',
  location: 'France'
});

// Views will listen for these types of event and sync their data again
console.log(view.length); // prints '3'

class MyComponent extends React.Component { // ... }

// Builds a wrapping component listening to the 'view' prop export default Prism.compose(MyComponent, ['view']);


<br>
This simplifies the process of binding a component to a view. In order to use this component we need to provide a valid `model view` as the `view` prop.

<br>
```javascript
// file: MainComponent.jsx
import React from 'react';
import store from './store';
import MyComponent from './MyComponent.jsx';

class MainComponent extends React.Component {
  componentWillMount() {
    this.defaultView = store.getDefaultView();
  }
  
  componentDidMount() {
    store.publish();
  }
  
  componentWillUnmount() {
    this.defaultView.destroy();
  }
  
  render() {
    return (<div>
      <MyComponent view={this.defaultView} />
    </div>);
  }
}

export default MainComponent;

let store = new Prism.Store([ { name: 'Eiffel Tower', location: 'France' }, { name: 'Taj Mahal', location: 'India' }, { name: 'Machu Picchu', location 'Peru' }, { name: 'Statue of Liberty', location: 'USA' }, { name: 'The Great Wall', location: 'China' }, { name: 'Brandenburg Gate', location: 'Germany' } ]);

export default store;


<br>
The first component will represent the app itself. It will be responsible of generating a default view for the list component.

<br>
```javascript
// file: DemoApp.jsx
import React from 'react';
import store from './demostore';
import LandmarkList from './LandmarkList.jsx';

class DemoApp extends React.Component {
  componentWillMount() {
    this.defaultView = store.getDefaultView();
  }
  
  componentDidMount() {
    store.publish();
  }
  
  componentWillUnmount() {
    this.defaultView.destroy();
  }
  
  render() {
    return (<div>
      <h3>Landmarks of the World</h3>
      <LandmarkList view={this.defaultView} />
    </div>);
  }
}

export default DemoApp;

class LandmarkList extends React.Component { render() { let list = this.props.view; let render = model => { return ({model.get('name')} ~ {model.get('location')}); };

return (<ul>{list.map(render)}</ul>);

} }

export default Prism.compose(LandmarkList, ['view']);


<br>
Finally, we render our app using `react-dom`.

<br>
```javascript
import React from 'react';
import ReactDOM from 'react-dom';
import DemoApp from './DemoApp.jsx';

ReactDOM.render(<DemoApp />, document.getElementById('app'));

class LandmarkList extends React.Component { render() { let list = this.props.view; let render = model => { return ({model.get('name')} ~ {model.get('location')}); };

// Check if data is available
if (!list.isInitialized()) {
  return (<div>Fetching data...</div>);
}

return (<ul>{list.map(render)}</ul>);

} }

export default Prism.compose(LandmarkList, ['view']);


<br>
We can simulate this process by delaying the call to `publish` in the main component.

<br>
```javascript
  componentDidMount() {
    setTimeout(() => store.publish(), 3000);
  }

let view = store.getDefaultView(); view.name === 'default'; // true


<br>
Both this method and `createView` accept an object containing a set of options. This object can contain the following properties:

<br>
 * name: A name that identifies this view. You can obtain a view by name through the `getView` method.
 * comparator: A function or property name used to sort the collection.
 * filter: A function used for filtering models in a collection.
 * size: The amount of elements to hold.
 * offset: The amount of elements to omit from the beginning.
 
<br>
View configuration
====

<br>
Views provide an easy mechanism for changing configuration options through `configs`. A `ViewConfig` object sets a particular list of options in a view and then notifies the view through an event (the `set` event). The next example implements a component that defines the amount of elements to show on a list.

<br>
```javascript
import React from 'react';

class ListSizeSelector extends React.Component {
  constructor(props) {
    super(props);
    
    // Initialize component state
    this.state = {
      size: 5
    };
  }
}

export default ListSizeSelector;

componentWillUnmount() { this.config.destroy(); }


<br>
The `createConfig` method expects a context object, generally the component itself, and a configuration callback. This callback gets invoked after calling the `apply` method and uses the context provided during the initialization. The configuration object returned by this callback is then merged against the view configuration. We need to make sure we destroy the configuration object once the component is unmounted.

<br>
```javascript
  render() {
    let options = [3, 5, 10];
    let render = value => {
      return (<option key={value} value={value}>{value}</option>);
    };
    
    return (<select value={this.state.size} onChange={this.onOptionChange.bind(this)}>{options.map(render)}</select>);
  }

class ListOrderSelector extends React.Component { constructor(props) { super(props);

// Initialize component state
this.state = {
  field: 'name',
  ascending: true
};

}

componentWillMount() { // Setup comparator this.comparator = this.props.view.createComparator(this, () => { let field = this.state.field; let ascending = this.state.ascending;

  return (model1, model2) => {
    if (model1.get(field) < model2.get(field)) {
      return ascending ? -1 : 1;
    } else if (model1.get(field) > model2.get(field)) {
      return ascending ? 1 : -1;
    }
  
    return 0;
  };
});

}

componentWillUnmount() { this.comparator.destroy(); }

handleFieldChange(e) { // Update state and apply comparator let value = e.target.value; this.setState({field: value}, this.comparator.eval()); }

handleOrderChange(e) { let value = e.target.value == 'Ascending'; this.setState({ascending: value}, this.comparator.eval()); }

render() { let fields = ['name', 'location']; let options = ['Ascending', 'Descending'];

return (<div>
  <p>
    <em>Order by:</em>
    <select value={this.field} onChange={this.handleFieldChange.bind(this)}>
      {fields.map(field => {
        return (<option key={field} value={field}>{field.substring(0,1).toUpperCase() + field.substring(1)}</option>);
      })}
    </select>
  </p>
  <p>
    <em>Sorting order:</em>
    <select value={this.state.ascending ? 'Ascending' : 'Descending'} onChange={this.handleOrderChange.bind(this)}>
      {options.map(order => {
        return (<option key={order} value={order}>{order}</option>);
      })}
    </select>
  </p>
</div>);

} }

export default ListOrderSelector;

<br>
Paginators
====

<br>
Paginators offers a simple way of separating a big list of elements into smaller sets. We begin by calling the `createPaginator` method passing the component instance, the page size and the initial page. Once done, we simply update the page number through `setPage` and apply the new configuration. Keep in mind that pagination components still need to listen for changes in the view that contains the elements we want to paginate. These kind of components are an example of components that listen to a view but apply modifications to another.

<br>
```javascript
// file: ListPaginator.jsx
import React from 'react';
import Prism from 'backbone.prism';
import _ from 'underscore';

class ListPaginationBar extends React.Component {
  constructor(props) {
    super(props);
    
    // Initialize component state
    this.state = {
      page: 1
    };
  }
  
  componentWillMount() {
    // Setup pagination
    this.paginator = this.props.paginateOn.createPaginator(this, this.props.pageSize, this.state.page);
  }
  
  componentWillUnmount() {
    this.paginator.destroy()
  }
  
  handlePageClick(e) {
    e.preventDefault();
    
    // Update component state and apply pagination
    let page = +e.target.innerHTML;
    this.paginator.setPage(page);
    this.setState({page}, this.paginator.eval());
  }
  
  render() {
    // Get amount of pages available
    let totalPages = this.paginator.getTotalPages(this.props.view.length);
    let render = counter => {
      return (<a href="#" key={counter} onClick={this.handlePageClick.bind(this)}>{counter + 1}</a>)
    };
    
    return (<div>
      {_(totalPages).times(render)}
      <small>Showing page {this.state.page} of {totalPages}</small>
    </div>);
  }
}

export default Prism.compose(ListPaginationBar, ['view']);

class DemoApp extends React.Component { componentWillMount() { this.defaultView = store.createDefaultView();

// Create paginated subview
this.paginatedView = this.defaultView.createView({
  name: 'paginated',
  listenTo: 'sync'
});

}

componentDidMount() { store.publish(); }

componentWillUnmount() { this.defaultView.destroy(); }

render() { return ( Landmarks of the World ); } }

export default DemoApp;


<br>
Filters
====

<br>
Filters are pretty straightforward. This time we invoke the `createFilter` method passing a context object and a callback. Callbacks can return either a filter function or an object setting a specific criteria. This example sets a filter combining regex matching and the [debounce](http://underscorejs.org/#debounce) function utility.

<br>
```javascript
// file: ListFilter.jsx
import React from 'react';
import Prism from 'backbone.prism';
import _ from 'underscore';

class ListFilter extends React.Component {
  constructor(props) {
    super(props);
    
    // Initialize filter state
    this.state = {
      filter: ''
    };
  }
  
  componentWillMount() {
    // Initialize filter
    this.filter = this.props.filterOn.createFilter(this, () => {
      let value = this.state.filter;
      let regex = new RegExp(value.replace(/([.*+?^${}()|\[\]\/\\])/g, "\\$1"), 'i');
      return model => value === '' ? true : model.get('name').match(regex);
    });
    
    // Build a debounced callback to avoid any blocking behavior
    this.filterCallback = _.debounce(this.filter.eval(), 250);
  }
  
  componentWillUnmount() {
    this.filter.destroy();
  }
  
  handleInputChange(e) {
    let value = e.target.value;
    this.setState({filter: value}, this.filterCallback);
  }

  render() {
    return (<div>
      <input onChange={this.handleInputChange.bind(this)} value={this.state.filter} />
    </div>);
  }
}

export default ListFilter;

class ChannelComponent extends React.Component { componentWillMount() { this.channel = new Prism.Channel(); this.channel.reply('initialize', { clicked: 0 }); }

componentWillUnmount() {
  this.channel.destroy();
}

render() {
  return (
      <div>
        <EmitterComponent channel={this.channel} />
        <ListenerComponent channel={this.channel} />
      </div>
  );
}

}

export default MainComponent;


<br>
Whenever a new state is applied, we communicate it to the listener component. In this case we use the `trigger` method to send the amount of clicks registered.


<br>
```javascript
// file: EmitterComponent.jsx
import React from 'react';

class EmitterComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = this.props.channel.request('initialize');
    }
    
    handleClick(e) {
      e.preventDefault();
      
      let channel = this.props.channel;
      let clicked = this.state.clicked + 1;
      this.setState({clicked}, () => {
          channel.trigger('update:clicked', clicked);
      });
    }
    
    render() {
      return (
        <button onClick={this.handleClick.bind(this)}>Click me</button>
      );
    }
}

export default EmitterComponent;

class ListenerComponent extends React.Component { constructor(props) { super(props); this.state = this.props.channel.request('initialize'); }

componentDidMount() {
  var self = this;
  this.props.channel.on('update:clicked', clicked => {
    self.setState({clicked});
  });
}

render() {
  return (
      <span>Clicks: {this.state.clicked}</span>
  );
}

}

export default ListenerComponent;


<br>
Communicating between components
====

<br>
Let's go back to our demo app. We're goig to add a channel to the main component so both the pagination component and the filter can communicate efficiently.

<br>
```javascript
// file: DemoApp.jsx
import React from 'react';
import store from './demostore';
import LandmarkList from './LandmarkList.jsx';
import ListOrderSelector from './ListOrderSelector.jsx';
import ListPaginationBar from './ListPaginationBar.jsx';
import ListFilter from './ListFilter.jsx';

class DemoApp extends React.Component {
  componentWillMount() {
    this.defaultView = store.createDefaultView();
    
    // Create paginated subview
    this.paginatedView = this.defaultView.createView({
      name: 'paginated',
      listenTo: 'sync'
    });
    
    // Create channel instance
    this.channel = new Prism.Channel();
  }
  
  componentDidMount() {
    store.publish();
  }
  
  componentWillUnmount() {
    this.defaultView.destroy();
  }
  
  render() {
    return (<div>
      <h3>Landmarks of the World</h3>
      <ListOrderSelector view={this.defaultView} />
      <ListFilter filterOn={this.defaultView} channel={this.channel} />
      <LandmarkList view={this.paginatedView} />
      <ListPaginationBar view={this.defaultView} paginateOn={this.paginatedView} channel={this.channel}/>
    </div>);
  }
}

export default DemoApp;
  let value = this.state.filter;
  let regex = new RegExp(value.replace(/([.*+?^${}()|\[\]\/\\])/g, "\\$1"), 'i');
  return model => value === '' ? true : model.get('name').match(regex);
});

// Build a debounced callback to avoid any blocking behavior
this.filterCallback = _.debounce(this.filter.eval(), 250);

}


<br>
The `ListPaginationBar` component will listen to this event and update accordingly.

<br>
```javascript
  componentWillMount() {
    // Setup pagination
    this.paginator = this.props.paginateOn.createPaginator(this, this.props.pageSize, this.state.page);
    
    // Listen `page:reset` event
    this.props.channel.on('page:reset', () => {
      this.paginator.setPage(1);
      this.setState({page: 1}, this.paginator.eval());
    }, this);
  }
// Set parent state vars to force re-render
this.setState({lastUpdate: (new Date()).getTime()});

}


<br>
In order to obtain a parent state var we'll use the `$value` method available in the props object.

<br>
```javascript
  render() {
    let lastUpdate = this.props.$value('lastUpdate');
    return (<small>Last update: {lastUpdate}</small>);
  }

render() { return (Showing {this.props.$value('total')} records); }


<br>
Flux by example
===

<br>
Stores
=====

<br>
According to the designers of *Flux*, a store *"contains the application state and logic"*. This same approach is implemented through the `Prism.Store` class, which extends `Backbone.Collection`.

<br>
```javascript
import {Model} from 'backbone';
import {Store} from 'backbone.prism';

let Task = Model.extend({
    urlRoot: '/tasks'
});

let TaskStore = Store.extend({
    model: Task,
    url: '/tasks'
});

let Profile = State.extend({ url: '/profile' });


<br>
Dispatcher
=====

<br>
The `Prism.Dispatcher` class doesn't add much to the original *Flux* dispatcher except for a few methods like `handleViewAction` and `handleServerAction`.

<br>
```javascript
// file: dispatcher.js
import {Dispatcher} from 'backbone.prism';
export default new Dispatcher();

Stores need to register their list of actions through the dispatcher. This example shows a simple approach for registering actions for a task store.

let Task = Model.extend({ urlRoot: '/tasks' });

let TaskStore = Store.extend({ model: Task, url: '/tasks' });

let store = new TaskStore([ new Task({ title: 'Do some coding', priority: 3 }), new Task({ title: '(Actually) make some tests', priority: 2 }), new Task({ title: 'Check out that cool new framework', priority: 1 }), new Task({ title: 'Make some documentation', priority: 1 }), new Task({ title: 'Call Saoul', priority: 3 }) ]);

store.dispatchToken = dispatcher.register(payload => { let action = payload.action;

switch (action.type) {
	case 'add-task':
		store.add(new Task(action.data));
	break;
			
	default:
}

});

export default store;


<br>
Finally, we define a simple interface for these actions.

<br>
```javascript
// File: actions.js
import dispatcher from './dispatcher';

let TaskActions = {
    addTask(task) {
        dispatcher.handleViewAction({
            type: 'add-task',
            data: task
        });
    }
};

export default TaskActions;