Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

383 lines (374 sloc) 13.296 kB

the change to backbone.

Welcome, this section of the blog post is hosted here because tumblr doesn't do code well enough. it makes this app

The first thing to notice is the template, and how there isn't one, there is an object with 3 smaller templates in it

templates=
    topNav:"""
        <ul class="nav">
            {{#topList}}
                <li id='li{{_id}}' {{#active}}class='active'{{/active}}>
                    <a class='nogo{{#brand}} brand{{/brand}}'href='{{_id}}' id='{{_id}}'>{{{title}}}</a>
                </li>
            {{/topList}}
        </ul>
        <ul class="nav pull-right" >
            <li>
                <a href="start" id="toggleStart"><i class="icon-refresh"></i></a>
            </li>
        </ul>
    """
    sideList:"""
        <li class='nav-header'>Notes</li>
        {{#sideList}}
            <li id='li{{_id}}' {{#active}}class='active'{{/active}}>
                <a class="nogo" href='{{_id}}' id='{{_id}}'>{{{title}}}</a>
            </li>
        {{/sideList}}
    """
    mainContent:"""
        {{^edit}}
            <h1>{{title}}{{#editable}}<a id='edit{{_id}}' class="nogo" href='edit{{_id}}'><i class ='icon-edit' id='edit{{_id}}'></i></a>{{/editable}}</h1>
            <p>{{#md}}{{{body}}}{{/md}}</p>
        {{/edit}}
        {{#edit}}
            <form class="form" id="updateForm">
                <div class="tabbable tabs-left">
                    <ul class="nav nav-tabs">
                        <li  class="active"><a href="#tab1" data-toggle="tab">edit</a></li>
                        <li><a href="#tab2" data-toggle="tab">view</a></li>
                    </ul>
                    <div class="tab-content">
                        <div class="tab-pane active" id="tab1"> 
                            <div class="control-group">
                                <label class="control-label" for="noteTitle">Title</label>
                                <div class="controls">
                                    <input type="text" id="noteTitle" name="title" value='{{title}}' class="forMD">
                                </div>
                            </div>
                            <div class="control-group">
                                <label class="control-label" for="nodeBody">Your Note</label>
                                <div class="controls">
                                    <textarea rows="10" id="noteBody" class="span8 forMD" name='body'>{{{body}}}</textarea></div>
                                </div>
                            </div>
                            <div class="tab-pane" id="tab2"></div>
                        </div>
                    </div>
                    <div class="control-group">
                        <div class="controls" id="btg">
                            <button type="submit" class="btn btn-success" id="updateNote">Update</button>
                            <button type="reset" class="btn btn-info" id="cancelUpdate">Cancel Edit</buton>
                            <button type="button" class="btn btn-danger" id="deleteNote">Delete</button>
                        </div>
                    </div>
            </form>
        {{/edit}}
    """

now isn't that much nicer to read? we took the three sections that have changes and gave them their own template that way, if the title of a note in the side bar changes, we don't need to update the entire page.

class Item extends Backbone.Model
    idAttribute: "_id"
    defaults : ()->
        active : false
    validate:(attr)->
        if 'title' not of attr or attr.title is ''
            return 'needs a title'
        else
            return

we create a model for our data, which sets the id to the same name as what couch/pouch uses, then have it add in a new field we're using and default it to false, and lastly we make sure it has a title (the server will do this too btw), next.

class Items extends Backbone.Collection
    model: Item

items = new Items

now make a collection of our model and get an instance, note the use of CoffeeScript style classes.

class genericView extends Backbone.View
    initialize: () =>
        @collection.on 'add', @handleUpdate, @
        @collection.on 'remove', @handleUpdate, @
        @collection.on 'change:active', @updateActive, @
        _.each @options.fields, (v)=>
            @collection.on 'change:'+v,@handleUpdate,@
        @render = @options.render
        @template = (Mustache.compile @options.template)
        @converter = new Showdown.converter()
        @md = ()=>
            (text, render)=>
                @converter.makeHtml(render(text))
    setActive : (e)=>
        e.preventDefault()
        routes.navigate e.target.id,{trigger:true}
    updateActive : (e)=>
        @render() if @belongs(e)
        if e.changed.active
            oldActive = @collection.where(_.extend({},@options.which,{active:true})).filter((v)->e.id isnt v.id)
            if oldActive.length > 0
                _.each oldActive,(v)=>
                    @collection.get(v.id).set "active", false
        true
    handleUpdate : (e)=>
        @render() if @belongs(e)
    belongs : (v)=>
        if "which" not of @options
            return v
        else 
            for key of @options.which
                if v.get(key) != @options.which[key]
                    return false
            return v
    renderMD:()=>
        innterTemplate = Mustache.compile "<h3>{{title}}</h3>{{#md}}{{body}}{{/md}}"
        renderedBody = @$("#tab2")
        note =
            body : @$("#noteBody").val()
            title : @$("#noteTitle").val()
            md : @md
        renderedBody.html(innterTemplate(note))
    makeNote : (e)=>
        note =
            body : @$("#noteBody").val()
            title : @$("#noteTitle").val()
            _id : Math.uuid()
            type : "note"
        @collection.add note
        e.preventDefault()
        routes.navigate note._id,{trigger:true}
    resetNote : (e)=>
        e.preventDefault()
        id = @collection.where({'active':true})[0].id
        if id == '#newNote'
            routes.navigate 'home', {trigger:true}
        else
            routes.navigate id,{trigger:true}
    updateNote : (e)=>
        e.preventDefault()
        id = @collection.where({'active':true})[0].id
        @collection.get(id).set
            body : @$("#noteBody").val()
            title : @$("#noteTitle").val()
            edit : false
        routes.navigate id,{trigger:true}
    deleteNote : (e)=>
        e.preventDefault()
        @collection.remove @collection.get(@collection.where({'active':true})[0].id)
        routes.navigate 'home',{trigger:true}
    restartPouch : (e)=>
        e.preventDefault()
        pouch.stop().start()

ah our generic view, lets break this down a bit

    initialize: () =>
        @collection.on 'add', @handleUpdate, @
        @collection.on 'remove', @handleUpdate, @
        @collection.on 'change:active', @updateActive, @
        _.each @options.fields, (v)=>
            @collection.on 'change:'+v,@handleUpdate,@
        @render = @options.render
        @template = (Mustache.compile @options.template)
        @converter = new Showdown.converter()
        @md = ()=>
            (text, render)=>
                @converter.makeHtml(render(text))

the initilizer, this this sets a function which fires if something is added or removed from the collection, and one that fires if there is a change to a field called active, then we go through an array in the options object called fields and set something to fire if those fields are changed.

properties that are defined on initilization are in an option object so we take the render function that is defined by an instance and let it be accesed at view.render() instead of view.option.render(), then same thing for the template but we render it first, make a markdown converter that can be used latter, and also we have a reusable markdown partial.

    setActive : (e)=>
        e.preventDefault()
        routes.navigate e.target.id,{trigger:true}
    updateActive : (e)=>
        @render() if @belongs(e)
        if e.changed.active
            oldActive = @collection.where(_.extend({},@options.which,{active:true})).filter((v)->e.id isnt v.id)
            if oldActive.length > 0
                _.each oldActive,(v)=>
                    @collection.get(v.id).set "active", false
        true

two functions work to change which page is the active one, set active is called to...do what it says and causes the app to navigate to the new page, update active makes sure the previously active item is deactivated.

    handleUpdate : (e)=>
        @render() if @belongs(e)
    belongs : (v)=>
        if "which" not of @options
            return v
        else 
            for key of @options.which
                if v.get(key) != @options.which[key]
                    return false
            return v

these two work to let a view ignore any changes that don't effect it.

    renderMD:()=>
        innterTemplate = Mustache.compile "<h3>{{title}}</h3>{{#md}}{{body}}{{/md}}"
        renderedBody = @$("#tab2")
        note =
            body : @$("#noteBody").val()
            title : @$("#noteTitle").val()
            md : @md
        renderedBody.html(innterTemplate(note))
    makeNote : (e)=>
        note =
            body : @$("#noteBody").val()
            title : @$("#noteTitle").val()
            _id : Math.uuid()
            type : "note"
        @collection.add note
        e.preventDefault()
        routes.navigate note._id,{trigger:true}
    resetNote : (e)=>
        e.preventDefault()
        id = @collection.where({'active':true})[0].id
        if id == '#newNote'
            routes.navigate 'home', {trigger:true}
        else
            routes.navigate id,{trigger:true}
    updateNote : (e)=>
        e.preventDefault()
        id = @collection.where({'active':true})[0].id
        @collection.get(id).set
            body : @$("#noteBody").val()
            title : @$("#noteTitle").val()
            edit : false
        routes.navigate id,{trigger:true}
    deleteNote : (e)=>
        e.preventDefault()
        @collection.remove @collection.get(@collection.where({'active':true})[0].id)
        routes.navigate 'home',{trigger:true}
    restartPouch : (e)=>
        e.preventDefault()
        pouch.stop().start()

these are the various functions that will latter be called by events in the various views.

sideView = new genericView 
    fields : ['title']
    template : templates.sideList
    el : $ "#sideList"
    collection : items
    which : {"type": "note"}
    render : ()->
        obj =
            sideList : @collection.where(@options.which).map((v)->v.toJSON())
        @$el.html(@template(obj))
        @
    events : 
        "click .nogo":"setActive"

our first view controls the sidebar, we'll go through the properties,

  • it only cares about the field called 'title'
  • it's template is called sideList
  • it's element is some element that already exists and has an id of sideList
  • it's collection is items
  • it only cares about the titles of documents with type 'note'
  • it's render function which transforms the documents we care about into an object for the mustache tempalte
  • an event it has, which is just if you click on soemthing with class='nogo' it will be set active
topView = new genericView
    fields : ['title']
    template : templates.topNav
    el : $ "#topNav"
    collection : items
    which : {"type":"page"}
    render : ()->
        obj =
            topList : @collection.where(@options.which).map((v)->
                if v.id=='home'
                    v.set "brand", true
                v.toJSON())
        obj.topList.sort (a, b) ->
            if a.brand
                0
            else
                1
        @$el.html(@template(obj))
        @
    events : 
        "click .nogo":"setActive"
        "click #toggleStart":"restartPouch"

the toplist, it's pretty much the same, except in the render function it sets the brand attribute of the home document so it shows up nicer and sorts it so that brand is always first, also we have an event for the refreshing of pouch

mainView = new genericView
    fields : ['title', 'body','edit']
    template : templates.mainContent
    el : $ "#mainContent"
    collection : items
    which : {active:true}
    render : ()->
        obj = @collection.where(@options.which)
        if obj.length == 1
            obj=obj[0]
        else
            return
        if obj.get("type") == "note"
            obj.set "editable", true
        obj.set "md", @md
        @$el.html @template(obj.toJSON())
        @renderMD()
        @
    events : 
        "change .forMD" : "renderMD"
        "submit #noteForm" : "makeNote"
        "reset #noteForm" : "resetNote"
        "reset #updateForm" : "resetNote"
        "submit #updateForm" : "updateNote"
        "click #deleteNote" : "deleteNote"
        "click .nogo":"setActive"

last of the view is the main part of the page, where we care about more fields, note the updated render function which deals with the more complicated template and the various events for editing notes.

class Routes extends Backbone.Router
    routes : 
        'edit:page' : "editPage"
        ':page' : "goto"
    editPage:(page)->
        items.get(page).set({"edit":true,"active":true})
    goto : (page)->
        curent = items.get(page)
        firstLoad = (e)->
                if e.id == page
                    items.off "change",firstLoad
                    e.set {"edit":false,"active":true}
        if curent
            curent.set({"edit":false,"active":true})
        else
            items.on "add",firstLoad,@
routes = new Routes

remember in my previous example where i was throwing around location.hash like it was going out of style? this kind of stuff is built into backbone

pouch = new PouchCore location.protocol + "//"+ location.host + "/backbone", (change)=>
        if change.id[0] isnt "_" and (change.doc.type is 'note' or change.doc.type is 'page')
            unless change.doc._deleted
                items.add change.doc, {merge:true, validate:true}
            else if change.doc._deleted and items.get(change.id)
                items.remove items.get(change.id) 

same old pouchCore we did in the second blog post, we update an the object unless it's been deleted, in which case we delete it

$ ()->
    root = "/backbone/_design/pouch/_rewrite/"
    Backbone.history.start {root:root,pushState:true,hashChange: false}
    items.on "add change:body change:title", (item)->
        doc = 
            _id : item.get '_id'
            body : item.get 'body'
            title : item.get 'title'
            type : item.get 'type'
        doc._rev = item.get('_rev') if item.get('_rev')
        pouch.add doc
    items.on "remove", (item)->
        pouch.remove item.id

then we initilize the stuff, first we set the root, and start the router thing, then set it up so we add changes that are made here to pouchcore

and were done checkout live app also you'll notice this is a couchapp and we use some of those features, especially the rewrites here.

If you want to checkout some other pouch db demos checkout out the pouchdb wiki

Jump to Line
Something went wrong with that request. Please try again.