1.3.2 • Published 8 years ago

backbone.prism v1.3.2

Weekly downloads
2
License
MIT
Repository
github
Last release
8 years ago

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;

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 (

  <h3>Landmarks of the World</h3>
  <ListOrderSelector view={this.defaultView} />
  <LandmarkList view={this.paginatedView} />
  <ListPaginationBar view={this.defaultView} paginateOn={this.paginatedView} />
</div>);

} }

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 Task = Model.extend({ urlRoot: '/tasks' });

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

<br>
Alternatively, you could use the `Prism.State` class, a `viewable model` based on `Backbone.Model`.

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

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

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;
1.3.2

8 years ago

1.3.1

8 years ago

1.3.0

8 years ago

1.2.1

9 years ago

1.2.0

9 years ago

1.1.2

9 years ago

1.1.1

9 years ago

1.1.0

9 years ago

1.0.0

9 years ago