I have created a Backbone Movie app as part of a learning exercise, the app is hosted on codepen although I actually have it built on my local but thought this approach would be handier for code review. I have some dummy JSON containing some movies that are stored in a Movie Collection and then another collection to store movies added to the watchlist, I created a watchlist collection instead of filtering the Movies collection so that I could save these models to local storage and then show the watchlist on deeplink. I'm pretty happy with what I have but unsure about areas such as the WatchlistItemView and how I dispose this view when I click to delete a movie from the watchlist. I'm also still quite unsure about what level of management each view should have, if I have a collection view with sub views does that mean the collection view is primarily in charge of the subviews or can the subviews manage themselves? With Backbone being so unopinionated I often find it so hard to know what views should be taking care of what management.Here is the link to the codepen http://codepen.io/styler/pen/wKVZxrWill be great to get your feedback!JSconsole.clear();// Namespace our app.var App = { Models: {}, Views: {}, Collections: {}, utils: {} }// Model ClassesApp.Models.MovieModel = Backbone.Model.extend({    localStorage: new Store('watchlist'),    defaults: {        watchlist: false    },    toggleWatchlist: function() {        this.set( 'watchlist', !this.get('watchlist') )    },    parse: function(response) {        if(response.backdrop_path) {            response.backdrop_small = 'http://image.tmdb.org/t/p/w500/'+response.backdrop_path;            response.backdrop_large = 'http://image.tmdb.org/t/p/w1280/'+response.backdrop_path;        } else {            response.backdrop_small = 'https://placeimg.com/500/281/any/grayscale/1';            response.backdrop_large = 'https://placeimg.com/1280/720/grayscale/1';        }        return response;    }});// Collection ClassesApp.Collections.MovieCollection = Backbone.Collection.extend({    model: App.Models.MovieModel,    url: 'http://codepen.io/styler/pen/yYmWWY.js',    // Original http://codepen.io/styler/pen/yYmWWY.js    // Missing attributes http://codepen.io/styler/pen/PZYjOr.js    // Updated attribute names http://codepen.io/styler/pen/Rrbgxv.js    initialize: function(options) {        console.log('MovieCollection', options);    },    parse: function(response) {        return response[0].results;    }});App.Collections.WatchlistCollection = Backbone.Collection.extend({    model: App.Models.MovieModel,    localStorage: new Store('watchlist')});// View ClassesApp.Views.AppView = Backbone.View.extend({    currentView: null,    _firstLoad: true,    initialize: function() {        // Set up route listeners        this.listenTo(Backbone.Events, 'show:movies', this.onShowMovies);        this.listenTo(Backbone.Events, 'show:movie', this.onShowMovie, this);        this.listenTo(Backbone.Events, 'show:watchlist', this.onShowWatchlist);        this.listenTo(Backbone.Events, 'add:watchlist', this.onWatchlistAdd, this);        this.listenTo(Backbone.Events, 'remove:watchlist', this.onWatchlistRemove, this);    },    onShowMovies: function() {        var movieCollectionView = new App.Views.MovieCollectionView({            collection: dataStore.movies        });        this._changeView(movieCollectionView);        if(dataStore.movies.length <= 0) {            dataStore.movies.fetch();        }    },    onShowMovie: function(id) {        var deferred = $.Deferred(),            self = this;        if(dataStore.movies.length > 0) {            deferred.resolve();        } else {            dataStore.movies.fetch().done(function() {                deferred.resolve();            });        }        deferred.done(function() {            var movieModel = dataStore.movies.get(id);            var detailedMovieView = new App.Views.DetailedMovieView({                model: movieModel            });            this._changeView(detailedMovieView);        }.bind(this));    },    onShowWatchlist: function() {        console.log('onShowWatchlist');        if(this._firstLoad && dataStore.watchlist.length <= 0) {            dataStore.watchlist.fetch().done(function(response) {                console.log(response);                var watchlistCollectionView = new App.Views.WatchlistCollectionView({                    collection: dataStore.watchlist                });                this._changeView(watchlistCollectionView);                this._firstLoad = false;            }.bind(this));        } else {            var watchlistCollectionView = new App.Views.WatchlistCollectionView({                collection: dataStore.watchlist            });            this._changeView(watchlistCollectionView);        }    },    _changeView: function(view) {        if (this.currentView) {            this.currentView.dispose();            this.currentView = null;        }        this.currentView = view;        this.$el.html(this.currentView.render().el);    },    onWatchlistAdd: function(model) {        console.log('AppView::onWatchlistAdd', model);        model.set('watchlist', true);        var clonedFilm = model.clone();        dataStore.watchlist.add(clonedFilm);        clonedFilm.save(null, {            success: function(model, response, options) {                console.log('Success::', model, response, options);            },            error: function(model, response, options) {                console.log('Error::', model, response, options);            }        });        console.info(dataStore.watchlist);    },    onWatchlistRemove: function(model) {        console.log('AppView::onWatchlistRemove', model);        model.set('watchlist', false);        //@todo need to add a url to model        var clonedFilm = model.clone();        dataStore.watchlist.remove(model);        clonedFilm.destroy(null, {            success: function(model, response, options) {                console.log('Success::', model, response, options);            },            error: function(model, response, options) {                console.log('Error::', model, response, options);            }        });        console.info(dataStore.watchlist);    }});App.Views.MainHeaderView = Backbone.View.extend({    events: {        'click .js-nav-link': 'onNavItemClicked'    },    initialize: function() {        console.log('MainHeaderView::initialize', this.$el);        Backbone.Events.on('route:change', this.onDeeplink, this);    },    onNavItemClicked: function(event) {        event.preventDefault();        var $clickedEl = $(event.currentTarget),            clickedHref = $clickedEl.attr('href');        this.toggleActiveNavClass($clickedEl);        App.utils.router.navigate(clickedHref, {            trigger: true        });    },    onDeeplink: function(route) {        console.log('onDeeplink', route);        var $element = $('.js-nav-link[href=/'+route+']');        this.toggleActiveNavClass($element);    },    /**     * [toggleActiveNavClass description]     * @param  {obj} element jQuery selector     */    toggleActiveNavClass: function(element) {        $('.js-nav-link').removeClass('is-active');        element.addClass('is-active');    }});App.Views.MovieCollectionView = Backbone.View.extend({    className: 'movie-grid',    events: {        'click .js-movie--show': 'onMovieClick'    },    template: _.template( $('.tmpl-movies').html() ),    initialize: function() {        console.log('MovieCollectionView::initialize');        this.collection.on('sync', this.render, this);    },    render: function() {        console.log('MovieCollectionView::render');        this.$el.html(this.template({            movies: this.collection.toJSON()        }));        setTimeout(function() {            this.afterRender();        }.bind(this), 0);        return this;    },    afterRender: function() {        console.log('MovieCollectionView::afterRender');        var $images = imagesLoaded( this.$el.find('.js-trans-img') );        $images.on('progress', function(instance, image) {            $(image.img).removeClass('is-hidden');        });    },    onMovieClick: function(event) {        event.preventDefault();        var $clickedEl = $(event.currentTarget),            clickedHref = $clickedEl.attr('href'),            movieId = $clickedEl.data('id');        Backbone.Events.trigger('show:movie', movieId);        App.utils.router.navigate(clickedHref, {trigger: false});    },    dispose: function() {        this.remove();        console.info('MovieCollectionView::dispose', this);    }});App.Views.DetailedMovieView = Backbone.View.extend({    className: 'movie-detailed',    events: {        'click .js-action--back': 'onBackBtnClick',        'click .js-watchlist--add': 'onAddClick',        'click .js-watchlist--remove': 'onRemoveClick'    },    template: _.template( $('.tmpl-movie-detailed').html() ),    initialize: function() {        console.log('DetailedMovieView::initialize');    },    render: function() {        console.log('DetailedMovieView::render', this.model.toJSON());        this.$el.html( this.template( this.model.toJSON() ) );        setTimeout(function() {            this.afterRender();        }.bind(this), 0);        return this;    },    afterRender: function() {        console.log('DetailedMovieView::afterRender');        var loader = new App.Views.LoaderView();        this.$('.js-img-placeholder').append(loader.render().el);        loader.show();        imagesLoaded(this.$('.js-trans-img'), function() {            this.images.forEach(function(i) {                var $image = $(i.img);                $image.removeClass('is-hidden');                loader.hide();            });        });        this.createWatchlistBtn();    },    createWatchlistBtn: function() {        var watchlistBtn = new App.Views.WatchlistBtnSubview({            model: this.model        });        this.$('.js-watchlist--add').replaceWith( watchlistBtn.render().el );    },    onBackBtnClick: function(event) {        event.preventDefault();        window.history.back();    },    onAddClick: function(event) {        event.preventDefault();        Backbone.Events.trigger('add:watchlist', this.model);    },    onRemoveClick: function(event) {        event.preventDefault();        Backbone.Events.trigger('remove:watchlist', this.model);    },    dispose: function() {        this.remove();        console.info('DetailedMovieView::dispose', this);    }});App.Views.WatchlistCollectionView = Backbone.View.extend({    className: 'movie-grid',    events: {        'click .js-movie--show': 'onMovieClick'    },    template: _.template( $('.tmpl-watchlist').html() ),    initialize: function() {        console.log('MovieListItemView::initialize', this.collection.toJSON());        this.listenTo(this.collection, 'add', this.onAddMovie, this);        this.listenTo(this.collection, 'remove', this.onMovieRemoved, this);    },    render: function() {        console.log('MovieListItemView::render', this.collection.toJSON());        this.$el.html(this.template());        if(this.collection.length > 0) {            this.collection.forEach(this.onAddMovie, this);        }        setTimeout(function() {            this.afterRender();        }.bind(this), 0);        return this;    },    afterRender: function() {        console.log('WatchlistCollectionView::afterRender');        var $images = imagesLoaded( this.$el.find('.js-trans-img') );        $images.on('progress', function(instance, image) {            $(image.img).removeClass('is-hidden');        });    },    onAddMovie: function(movie) {        console.log('ADD MOVIE');        var watchlistMovie = new App.Views.WatchlistItemView({            model: movie        });        this.$el.prepend(watchlistMovie.render().el);    },    onMovieClick: function(event) {        event.preventDefault();        var $clickedEl = $(event.currentTarget),            clickedHref = $clickedEl.attr('href'),            movieId = $clickedEl.data('id');        Backbone.Events.trigger('show:movie', movieId);        App.utils.router.navigate(clickedHref, {trigger: false});    },    onMovieRemoved: function(model) {        console.log('REMOVE MOVIE', model.toJSON());        // this.collection.remove(model);        console.log(this.collection.toJSON());        if(this.collection.length === 0) {            this.$el.html(this.template());        }    },    dispose: function() {        this.remove();        console.info('WatchlistCollectionView::dispose', this);    }});App.Views.WatchlistItemView = Backbone.View.extend({    className: 'movie',    template: _.template( $('.tmpl-watchlist-movie').html() ),    events: {        'click .js-watchlist--remove': 'onWatchlistRemove'    },    render: function() {        this.$el.html( this.template( this.model.toJSON() ) );        return this;    },    onWatchlistRemove: function(event) {        event.preventDefault();        event.stopPropagation();        this.dispose();        var model = dataStore.movies.get(this.model.get('id'));        Backbone.Events.trigger('remove:watchlist', model);    },    dispose: function() {        this.remove();    }});App.Views.WatchlistBtnSubview = Backbone.View.extend({    template: _.template( $('.tmpl-watchlist-movie-btn').html() ),    initialize: function() {        console.log('WatchlistBtnSubview::initialize', this.model.toJSON());        this.listenTo(this.model, 'change:watchlist', this.render, this);    },    render: function() {        console.log('WatchlistBtnSubview::render', this.model.toJSON());        this.$el.html( this.template( this.model.toJSON() ) );        return this;    }});App.Views.LoaderView = Backbone.View.extend({    className: 'loading tt is-hidden',    stateClass: 'is-hidden',    template: _.template( $('.tmpl-loader').html() ),    initialize: function() {        this.listenTo(Backbone.Events, 'loader:show', this.show, this);        this.listenTo(Backbone.Events, 'loader:hide', this.hide, this);    },    render: function() {        this.$el.html( this.template() );        return this;    },    show: function() {        this.$el.removeClass(this.stateClass);    },    hide: function() {        this.$el.addClass(this.stateClass);        this.$el.one('transitionend', function() {            this.dispose();        }.bind(this));    },    dispose: function() {        this.remove();    }});// UtilitiesApp.utils.router = new (Backbone.Router.extend({    routes: {        '': 'onShowMovies',        'movie/:id': 'onShowMovie',        // 'movie': 'onShowMovie',        'watchlist': 'onShowWatchlist'    },    initialize: function() {        console.log('Router Started');        this.on('all', this._signalRouteChange);    },    onShowMovies: function() {        console.log('router::onShowMovies', this);        Backbone.Events.trigger('show:movies');    },    /**     * [onShowMovie description]     * @param {[number]} id movie id     */    onShowMovie: function(id) {        console.log('router::onShowMovie', this);        Backbone.Events.trigger('show:movie', id);    },    onShowWatchlist: function() {        console.log('router::onShowWatchlist');        Backbone.Events.trigger('show:watchlist');    },    _signalRouteChange: function(route) {        if(route === 'route') return;        console.log('Firing', route);        var fragment = Backbone.history.getFragment();        if(fragment.split('/')[0] === 'movie') {            fragment = '';        }        console.log('Fragment::', fragment);        Backbone.Events.trigger('route:change', fragment);    }}));// Create a data storevar dataStore = {};dataStore.movies = new App.Collections.MovieCollection();dataStore.watchlist = new App.Collections.WatchlistCollection();(function() {    new App.Views.MainHeaderView({        el: '.js-header'    });    // Start App.    new App.Views.AppView({        el: '.js-app'    });    // Start History API.    Backbone.history.start();})();Templates<header class=global-header js-header>    <nav class=main-nav>        <a href=/ class=main-nav__link is-active js-nav-link>Popular</a>        <a href=/watchlist class=main-nav__link js-nav-link>Watchlist</a>    </nav></header><div class=main-content js-app></div><!-- Templates --><script type=text/template class=tmpl-movies>    <% _.each(movies, function(movie) { %>        <a href=/movie/<%= movie.id %> class=movie js-movie--show data-id=<%= movie.id %>>            <img src=<%= movie.backdrop_small %> alt= class=trans-img is-hidden js-trans-img>            <span class=movie__info>                <h2><%= movie.title %></h2>            </span>        </a>    <% }); %></script><script type=text/template class=tmpl-movie-detailed>    <div class=poster-holder js-img-placeholder>        <img src=<%= backdrop_large %> alt= class=movie-detailed__poster trans-img is-hidden js-trans-img>    </div>          <div class=movie-details>        <a href=/ class=action action--back js-action--back>            <svg height=80px width=80px class=action__icon action__icon--back viewBox=0 0 80 80><rect height=38 width=5 x=19 y=20.547/><polygon points=46.371,59 49.906,55.465 36.488,42.047 61,42.047 61,37.047 36.652,37.047 49.906,23.793 46.371,20.258 27,39.629/></svg>        </a>        <h1 class=movie-details__title><%= title %></h1>        <div class=movie-details__content>            <p><%= overview %></p>        </div>        <button class=action action-watchlist--added js-watchlist--add>            <svg height=80px width=80px class=action__icon action__icon--add viewBox=0 0 80 80><polygon points=61,37 43,37 43,19 37,19 37,37 19,37 19,43 37,43 37,61 43,61 43,43 61,43 /></svg>            Add to watchlist        </button>    </div></script><script type=text/template class=tmpl-watchlist>    <% if(this.collection.length <= 0) { %>        <div class=notification>            <img src=http://cdn.meme.am/instances2/500x/3145144.jpg alt= class=sadface />        </div>    <% } %></script><script type=text/template class=tmpl-watchlist-movie>    <a href=/movie/<%= id %> class=js-movie--show data-id=<%= id %>>        <img src=<%= backdrop_small %> alt= class=trans-img is-hidden js-trans-img>        <span class=movie__info>            <h2><%= title %></h2>            <button class=action action-watchlist--remove js-watchlist--remove>                <svg height=80px width=80px class=action__icon action__icon--remove viewBox=0 0 80 80><polygon points=56.971,52.728 44.243,40 56.971,27.272 52.728,23.029 40,35.757 27.272,23.029 23.029,27.272 35.757,40 23.029,52.728 27.272,56.971 40,44.243 52.728,56.971/></svg>            </button>        </span>    </a></script><script type=text/template class=tmpl-watchlist-movie-btn>     <% if(!watchlist) { %>        <button class=action action-watchlist--added js-watchlist--add>            <svg height=80px width=80px class=action__icon action__icon--add viewBox=0 0 80 80><polygon points=61,37 43,37 43,19 37,19 37,37 19,37 19,43 37,43 37,61 43,61 43,43 61,43 /></svg>            Add to watchlist        </button>    <% } else { %>        <button class=action action-watchlist--removed js-watchlist--remove>            <svg height=80px width=80px class=action__icon action__icon--remove viewBox=0 0 80 80><polygon points=56.971,52.728 44.243,40 56.971,27.272 52.728,23.029 40,35.757 27.272,23.029 23.029,27.272 35.757,40 23.029,52.728 27.272,56.971 40,44.243 52.728,56.971/></svg>            Remove from watchlist        </button>    <% } %></script><script type=text/template class=tmpl-loader>    <div class=loader>Loading...</div></script>
Backbone learning piece, Movie Application using Views, Collection, Models, Routes and Localstorage
javascript;jquery;backbone.js;underscore.js