Skip to content

Instantly share code, notes, and snippets.

@kevincolten
Last active August 29, 2015 14:25
Show Gist options
  • Save kevincolten/9ed1245349d0c39c66fb to your computer and use it in GitHub Desktop.
Save kevincolten/9ed1245349d0c39c66fb to your computer and use it in GitHub Desktop.
@post Backbone + React: or, How I Came to Stop Looking for an Example and Built My Own

This tutorial came out of many frustrating google searches trying to find a decent end-to-end boilerplate example of incorporating React as the view component in Backbone. The three components I wanted in a tutorial were:

  1. Build something more than a single page TODO
  2. Involved routing and controllers
  3. Included a build process using a modern build tool
  4. Optimization and source-mapping
  5. Done in an organized manner with descriptive naming patterns

What we'll be building in this tutorial is a basic blog viewer. The source can be found here and the finished product can be found here. When complete, we'll be able to write the blog post in markdown in a gist with the title starting with a key word (ex. @post Here's My Post Title!) and display it in all its html glory.

So let's get started.

This tutorial assumes that you have Node.js with npm (preferably with nvm which can be installed via Homebrew on OSX).

If entering which npm gives you a path, you should be good to go.

Be sure you've installed Gulp globally with the command: sudo npm install -g gulp

And we'll needing a local server, so go ahead and run sudo npm install -g http-server. We'll use that later.

Let's set up our environment. In a new directory, create a package.json with these packages:

{
  "name": "gistblog",
  "version": "0.0.1",
  "dependencies": {
    "backbone": "^1.2.1",
    "backbone-react-component": "^0.8.1",
    "backbone.controller": "^0.3.1",
    "jquery": "^2.1.4",
    "marked": "^0.3.3",
    "react": "^0.13.3",
    "underscore": "^1.8.3"
  },
  "devDependencies": {
    "browserify": "^10.2.4",
    "gulp": "^3.9.0",
    "gulp-rename": "^1.2.2",
    "gulp-sourcemaps": "^1.5.2",
    "gulp-uglify": "^1.2.0",
    "gulp-util": "^3.0.6",
    "lodash.assign": "^3.2.0",
    "reactify": "^1.1.1",
    "vinyl-buffer": "^1.0.0",
    "vinyl-source-stream": "^1.1.0",
    "watchify": "^3.2.3"
  }
}

and run npm install.

Please google all of these packages, as they all do amazing things and are generally well documented.

Let's set up our build system. Create a gulpfile.js and let's add a few things.

// gulpfile.js

'use strict';

var watchify = require('watchify');
var browserify = require('browserify');
var gulp = require('gulp');
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
var gutil = require('gulp-util');
var sourcemaps = require('gulp-sourcemaps');
var assign = require('lodash.assign');
var reactify = require('reactify');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');

// add custom browserify options here
var customOpts = {
  entries: ['./app.js'],
  debug: true
};

// combine arguments passed in with
// customOpts
var opts = assign({}, watchify.args, customOpts);

// create watchified-browserify object
var b = watchify(browserify(opts));

// transform react syntax
b.transform('reactify');

// so you can run `gulp js` to build the file
gulp.task('js', bundle);

// on any dep update, runs the bundler
b.on('update', bundle);

// output build logs to terminal
b.on('log', gutil.log);

function bundle() {
  return b.bundle()

    // simply pipes in a filename
    .pipe(source('bundle.js'))

    // more info here https://goo.gl/htmChu
    .pipe(buffer())

    // build sourcemaps
    // loads map from browserify file
    .pipe(sourcemaps.init({loadMaps: true}))

     // writes .map file to relative location
     // of bundle.js
    .pipe(sourcemaps.write('./'))

    // destination of bundle.js
    .pipe(gulp.dest('./'));
}

// run 'gulp uglify' to produce optimized javascript
gulp.task('uglify', function() {
  return gulp.src('bundle.js')
    .pipe(uglify())
    .pipe(rename('bundle.min.js'))
    .pipe(gulp.dest('./'));
});

Let's create our index.html. We'll keep it extremely simple.

<!--index.html-->

<html>
  <head>
  </head>
  <body>
    <div id="content"></div>
    <script src="./bundle.js"></script>
  </body>
</html>

And now we can start on some Javascript. Let's build our entry point into the app. This is what will be tracked by Gulp and Watchify. If anything in this file changes, or any of its required dependencies, or any of the required depenedencies of the required dependencies, etc, gulp should rebuild bundle.js. This will be our app.js:

// app.js

'use strict';

var Backbone = require('backbone');
var PostsController = require('./controllers/PostsController');

var Application = Backbone.Router.extend({
  // our router can keep track of our
  // instantiated controllers
  controllers: {},

  initialize: function() {
    // instantiate a new PostsController and
    // pass in this router
    this.controllers.posts = new PostsController({router: this});

    // This is to start Backbone listening to
    // url hash changes. It will allow us to
    // navigate using the back and forward
    // buttons on the browser
    Backbone.history.start();
  }
});

// start the app!
window.app = new Application();

Let's define our models/PostModel.js

// models/PostModel.js

var Backbone = require('backbone');
var marked = require('marked');

module.exports = Backbone.Model.extend({
  // here is the api endpoint to receive a
  // single gist object
  urlRoot: 'https://api.github.com/gists/',
  // this attribute is added to the end by
  // backbone when making a fetch() call
  idAttribute: 'id',

  // parse will parse through the raw data
  // coming back and let us adjust it to fit
  // our needs
  parse: function(model)
  {
    // removes the "@post" from the title
    model.description = model.description.replace("@post ", "");

    // grabs content from the first file
    // included in the gist
    var content = model.files[Object.keys(model.files)[0]].content

    if (content) {
      // if the content exists, run through
      // markdown decoder and make it safe
      model.content = marked(content, { sanitize: true });
    }
    // return the adjusted json model for
    // backbone.
    return model;
  }
});

And now for our collections/PostsCollections.js

// collections/PostsCollections.js

var Backbone = require('backbone');
var _ = require('underscore');
var PostModel = require('../models/PostModel');

module.exports = Backbone.Collection.extend({
  // here we have our gists endpoint,
  // feel free to enter your own github
  // username!
  url: 'https://api.github.com/users/mistakevin/gists',
  model: PostModel,

  // let's parse through the returned json
  parse: function(collection)
  {
    // we want only the gists that start
    //with '@post '
    collection = _.filter(collection, function (model) {
      return model.description.indexOf('@post ') > -1;
    });
    return collection;
  }
});

Next, let's create our controllers/PostsController.js

// controllers/PostsController.js

var Backbone = require('backbone');
var React = require('react');
require('backbone.controller');
var PostComponent = require('../components/PostComponent');
var PostsListComponent = require('../components/PostsListComponent');
var PostsCollection = require('../collections/PostsCollection');
var PostModel = require('../models/PostModel');
var $ = require('jquery');

module.exports = Backbone.Controller.extend({
  routes: {
    '': 'index',
    '/': 'index',
    'posts/:id': 'show',
    'posts': 'index',
  },

  initialize: function() {
    this.collection = new PostsCollection();
  },

  // this will render the list of posts
  index: function() {

    // fetch our collection
    this.collection.fetch();

    // don't forget to pass in your
    // collection! At this point, the
    // collection probably isn't finished
    // fetching, but backebone.react will
    // update the the view/component for us
    // on a successful sync!
    React.render(<PostsListComponent collection={this.collection} />, $('#content')[0]);
  },

  // this will show the individual post and
  // content
  show: function(id) {
    // if the link is clicked, we'll grab the
    // id from the params then grab that
    // model out of the already fetched
    // collection
    var post = this.collection.get(id);

    if (!post) {
      // if the collection was never fetched,
      // as in a page refresh on the
      // individual post page, we'll create a
      // new model with only the id
      // attribute, and that will give us
      // enough information to fetch
      post = new PostModel({ id: id });
    }

    // let's fetch our post
    post.fetch();

    // don't forget to pass in your model!
    React.render(<PostComponent model={post} />, $('#content')[0]);
  }
});

And now our different compoenents, which will consist of a PostsListComponent, PostItemComponent, and a PostComponent. First in our components/PostsListComponent.js we'll create the list that hold all of the post items.

// components/PostsListComponent

var React = require('react');
var Backbone = require('backbone');
require('backbone-react-component');

module.exports = React.createClass({
  mixins: [Backbone.React.Component.mixin],

  // every component will have a render()
  // function
  render: function() {
    // the way you access the collection
    // passed in is with
    // this.state.collection here we are
    // mapping through our collection of
    // posts, creating a component
    // PostItemComponent and passing in
    // postItem model as model
    var postItems = this.state.collection.map(function(postItem) {
      return <PostItemComponent model={postItem} />
    });

    // we'll return the component, passing in
    // the postItems 'mustache style'
    return (
      <div>
        <h1>Posts</h1>
        {postItems}
      </div>
    );
  }
});

And for our components/PostItemComponent.js

// components/PostItemComponent.js

var Backbone = require('backbone')
var React = require('react');
require('backbone-react-component');

module.exports = React.createClass({
  mixins: [Backbone.React.Component.mixin],
  // here we can see that when we pass something
  // through a component as a prop, we can access
  // it from this.props
  render: function () {
    return (
      <div>
        <h3>
          <a href={ "#posts/" + this.props.model.id}>
            {this.props.model.description}
          </a>
        </h3>
      </div>
    );
  }
});

From here we can create the components/PostComponent.js, which will be where the item links to so we can read the content of our post!

// components/PostComponent.js

var React = require('react');
var Backbone = require('backbone');
require('backbone-react-component');

module.exports = React.createClass({
  mixins: [Backbone.React.Component.mixin],
  // the 'dangerouslySetInnerHTML is a way to
  // get around the automatic escaping. Since
  // we 'sanitized' in our model, we should
  // be ok
  render: function() {
    return (
      var content = this.state.model.content;
      <div>
        <h1>{this.state.model.description}</h1>
        <p dangerouslySetInnerHTML={{__html: content}}></p>
      </div>
    );
  }
});

Alright! We should be good to go. Let's run gulp js and boot up our http-server in the directory of our project and navigate to localhost:8080 in your browser. Hopefully you'll have your very own blog, ready to deploy to Github Pages.

If you haven't figured it out, this blog you are reading is a stylized verison of the same source on the styled branch!

Please feel free to leave comments and questions below the gist located here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment