rosewood v0.1.3
Rosewood
A super-simple Model/View/Collection library for the browser.
- Install
- Usage
- Rosewood.Model
- Rosewood.Collection
- Rosewood.View
Rosewood's goal is to supply just enough code so you don't need to re-write boilerplate MVC code. Rosewood requires EcmaScript 5+, because it heavily relies on getters/setters, but has no other dependencies. (A non-ES5 alternative is http://backbonejs.org)
Install
Browserify / other NPM-based:
npm install --save rosewood
Browser standalone:
Download rosewood.js and include it:
<script type="application/javascript" src="rosewood.js"></script>Usage
Rosewood.Model
// Using ES6 class syntax
Rosewood = require('rosewood') // if using Browserify or other CommonJS system
var Model = Rosewood.Model
class Person extends Model {
constructor(attributes){
super(attributes)
this.attribute_names = ['first_name, last_name']
}
get url(){
return "/people/" + this.id.toString()
}
full_name(){
return this.first_name + " " + this.last_name
}
}
var bob = new Person({first_name: "Bob", last_name: "Robson"})model.attribute_names
Set attribute_names to the list of properties of the model that make up its data. These are the properties that will be synched with the model's API, and change events will be emitted when any of them change.
model.on('change')
model.on('change', function({attribute1: {new: ..., old: ...}, attribute2: {new: ..., old: ...}){ ... })The change event fires on a model whenever any of the attributes is modified.
The callback gets an object that lists new and old values for each changed attribute.
bob.on('change', function(changes){
console.log(changes.first_name.old, changes.first_name.new)
})
bob.first_name = "Robert" // logs "Bob", "Robert"model.sync()
model.sync(function callback(error){})
// or
model.sync() => PromiseSynchronizes the model with its backing API.
- If the model has never been synched with its API since creation, will call
model.create() - If the model has been synched but has been changed since, it will call
model.update() - If the model has been synched and has not been changed since, it will call
model.refresh()
Calls model.create(), model.update(), based on whether the model has been previously synced with (or retrieved from) its backing API, or model.refresh() if the model hasn't been changed since the last sync.
model.sync(), model.refresh(), model.create(), model.update(), and model.delete() are all asynchronous, and can be either called with a callback function or will return a Promise (specifically, https://github.com/petkaantonov/bluebird).
model.refresh()
model.refresh(function callback(error){})
// or
model.refresh() => PromiseGets the latest model attributes from the model's API.
var bob = new Person({id: 1})
bob.refresh()This will send an HTTP request like:
Method: GET
URL: /people/1It expects a response like:
Status: 200 OK
Headers: {Content-Type: application/json}
Body: {"id": 1, "first_name": "Bob", "last_name": "Robson"}Refresh will overwrite any local changes made to the model that haven't been synchronized with the API yet. If you'd rather do an intelligent sync, try model.sync()
model.create()
model.create(function callback(error){})
// or
model.create() => PromiseSends a create request to a REST API.
var bob = new Person({first_name: "Bob", last_name: "Robson"})
bob.create()This will send an HTTP request like:
Method: POST
URL: /people
Headers: {Content-Type: application/json}
Body: {"first_name": "Bob", "last_name": "Robson"}It expects a response like:
Status: 200 OK
Headers: {Content-Type: application/json}
Body: {"id": 1, "first_name": "Bob", "last_name": "Robson"}model.update()
model.update(function callback(error){})
// or
model.create() => PromiseSends a update request to the model's REST API.
This will make an HTTP request like:
Method: PATCH
URL: /people
Headers: {Content-Type: application/json}
Body: {"id": 1, "first_name": "Bob", "last_name": "Robson"}It expects a response like:
Status: 200 OK
Headers: {Content-Type: application/json}
Body: {"id": 1, "first_name": "Bob", "last_name": "Robson"}model.delete()
model.delete(function callback(error){})
// or
model.delete() => PromiseWill send a remove request to the model's REST API.
It sends a request like:
Method: DELETE
URL: /people/1It expects a response like:
Status: 200 OKRosewood.Collection
The Collection class represents a group of models. It could be every model of a type (all Person models), or a subset(all Person models with age >= 16)
// Continuing the above example code
var people = new Rosewood.Collection({url: '/people', model: Person})collection.refresh()
collection.refresh(function callback(error){})
// or
collection.refresh() => PromiseGets new collection models from a REST API.
people.refresh()This will send an HTTP request like:
Method: GET
URL: /people
Headers: {Content-Type: application/json}It expects a response like:
Status: 200 OK
Headers: {Content-Type: application/json}
Body: [{"id": 1, "first_name": "Bob", "last_name": "Robson"}, {"id": 2, "first_name": "John", "last_name": "Jackson"}]After, collection.models will contain instances of collection.model that match the received data. It will merge any existing models (based on id) and
collection.get(id)
collection.get(id) => ModelReturns the model with id id, or null if none is found.
people.get(1) // returns the Person instance with id 1collection.store()
collection.store(function(error){})
// or
collection.store() => PromiseWill make one HTTP request per model that has been modified. (It essentially calls model.sync() on each model.)
people.add(new Person({first_name: "Mike", las_name: "Mitchell"}))
people.get(1).last_name = "Roberts"
people.remove(people.get(2))
people.store()Will send requests like:
POST /people {"first_name": "Mike", "last_name": "Mitchell"}
PATCH /people/1 {"last_name": "Roberts"}
DELETE /people/1collection.sync()
collection.sync(function(error){}) or collection.sync() => PromiseEffectively a combination of collection.store() and collection.fetch(). It will sync all existing models and fetch any new ones.
collection.on('change')
collection.on('change', function({changed: [...], added: [...], removed: [...]}){ ... })An event that's emitted any time a model in the collection changes, a model is added, or a model is removed. The callback function is passed an object containing three keys: {added: [...], changed: [...], removed: [...]}, which contain the models that have changed in those ways.
collection.add(model)
Add a model to the collection. The collection will emit an add event.
collection.on('add')
collection.on('add', function(added_models){ ... })An event emitted whenever one or more models are added to the collection. The callback function is passed an array of added models.
collection.remove(model)
Remove a model from the collection. The collection will emit a remove event. Calling collection.sync() will send a DELETE HTTP request to collection.url (by calling model.delete()).
collection.on('remove')
collection.on('remove', function(removed_models){ ... })An event emitted whenever one or more models are removed the collection. The callback function is passed an array of added models.
Rosewood.View
A Rosewood.View wraps an HTMLElement and ties it to a Rosewood.Model for presentation.
A Rosewood.View is a Rosewood.StateMachine, so that views that change significantly based on certain events or data (such as an application's main view) can be easily re-configured.
Rosewood = require('rosewood')
class PersonForm extends Rosewood.View {
constructor(options){
super(options)
this.element.innerHTML = `
<label name="full_name"></label>
<input type="text" name="first_name"/>
<input type="text" name="last_name" />
`
this.first_name_field = this.element.querySelector('[name=first_name]')
this.last_name_field = this.element.querySelector('[name=last_name]' )
this.full_name_label = this.element.querySelector('[name=full_name]' )
var view = this // event listeners change the value of `this`; this gets around that.
// Alternatively, use the ES6 "=>" syntax.
// Whenever this view gets a new model, update the display
this.on('set_model', function() {
view.full_name_label.textContent = model.full_name()
view.first_name_field.value = view.model.first_name
view.last_name_field.value = view.model.last_name
})
this.on('model:change', function(changes) {
// Whenever the first or last name are changed, update the full_name_label
if(changes.first_name || changes.last_name){ view.full_name_label.textContent = view.model.full_name() }
// If the model's first_name attribute is changed, update the corresponding field
if(changes.first_name){ view.first_name_field.value = view.model.first_name }
// Advanced logic: only update the field if the model has changed and the user hasn't entered anything.
if(changes.last_name){
if(changes.last_name.old === view.last_name_field.value){ view.first_name_field.value = view.model.last_name }
}
})
// If the first or last_name fields have their contents changed,
// update the corresponding model attribute
this.first_name_field.addEventListener('change', function() {
view.model.first_name = view.first_name_field.value
})
this.last_name_field.addEventListener('change', function() {
view.model.last_name = view.last_name_field.value
})
}
}
var abbreviated_person_display = new PersonForm({
element: document.getElementById('person_form')
})view.element
The HTMLElement the View uses to represent itself on the page.
view.model
The Rosewood.Model instance the view represents (optional).
view.on('set_model')
view.on('set_model', function(){ ... })An event emitted whenever a view gets assigned a new model.
view.on('model:change')
view.on('model:change', function(changes){ ... })A proxy event emitted whenever view.model emits a change event. The callback parameter is the same as model.on('change').
view.hide()
Hides view.element using element.style.display = 'none'.
view.show()
Un-hides view.element using element.style.display = ''.
Rosewood.StateMachine
A super-simple state machine that keeps a state_machine.state and emits enter_state and exit_state events whenever it is changed.
state_machine.state
The current state the state machine is in. There are no restrictions on state changes; to change the current state, simply re-assign state_machine.state = "new_state_name".
state_machine.on('enter_state')
state_machine.on('enter_state', function(new_state, old_state){ ... })An event emitted after the event machine enters a new state. The callback function is passed the name of the new and old states.
state_machine.on('exit_state')
state_machine.on('exit_state', function(old_state, new_state){ ... })An event emitted before the state machine enters a new state. The callback function is passed the name of the previous and new states.
state_machine.on('enter_state:{state}')
state_machine.on('enter_state:${state_name}', function(old_state){ ... })A convenience event emitted after the machine enters a specific state.
state_machine.on('exit_state:{state}')
state_machine.on('exit_state:${state_name}', function(new_state){ ... })A convenience event emitted before the machine exits a specific state.