Trello tribute with Phoenix and React (pt.2)

Phoenix Framework project setup for creating a Trello clone.
Jan 11, 2016 · 9 min read
elixir
phoenix
react
webpack
redux

Project setup

So now that we have selected our current stack let's start by creating the new Phoenix project. Before doing so we need to have both Elixir and Phoenix already installed in our system so check out both official sites for installation instructions.

Static assets through Webpack

Compared to Ruby on Rails, Phoenix doesn't have its own asset pipeline, instead it uses Brunch as the assets build tool which to me feels more modern and flexible. The cool thing is that you don't even need to use Brunch if you don't want to, you can also use Webpack. As I haven't tried Brunch before so we're going to use Webpack instead.

Phoenix has node.js as an optional dependency, as it's required by Brunch, however Webpack also requires node.js, so make sure you have node.js installed as well.

Let's create the new Phoenix project without Brunch:

$ mix phoenix.new --no-brunch phoenix_trello
  ...
  ...
  ...
$ cd phoenix_trello

Alright, now we have our new project created with no assets building tool. Let's create a new package.json file and install Webpack as a dev dependency:

$ npm init
  ... (You can just hit enter when prompted for setting default values.)
  ...
  ...
$ npm i webpack --save-dev

Now our package.json should look something similar to this:

{
  "name": "phoenix_trello",
  "devDependencies": {
    "webpack": "^1.12.9"
  },
  "dependencies": {
    ...
    "phoenix": "file:deps/phoenix",
    "phoenix_html": "file:deps/phoenix_html",
    ...
  },
}

We are going to need a bunch of dependencies in the project so instead of listing them all please take a look to the source file in the project's repository to copy and paste them into the package.json file. Now we have to run the following command to install all the packages:

$ npm install

We also need to add a webpack.config.js configuration file to tell Webpack how to build the assets:

'use strict';

var path = require('path');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var webpack = require('webpack');

function join(dest) { return path.resolve(__dirname, dest); }

function web(dest) { return join('web/static/' + dest); }

var config = module.exports = {
  entry: {
    application: [
      web('css/application.sass'),
      web('js/application.js'),
    ],
  },

  output: {
    path: join('priv/static'),
    filename: 'js/application.js',
  },

  resolve: {
    extensions: ['', '.js', '.sass'],
    modulesDirectories: ['node_modules'],
  },

  module: {
    noParse: /vendor\/phoenix/,
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
        query: {
          cacheDirectory: true,
          plugins: ['transform-decorators-legacy'],
          presets: ['react', 'es2015', 'stage-2', 'stage-0'],
        },
      },
      {
        test: /\.sass$/,
        loader: ExtractTextPlugin.extract('style', 'css!sass?indentedSyntax&includePaths[]=' + __dirname +  '/node_modules'),
      },
    ],
  },

  plugins: [
    new ExtractTextPlugin('css/application.css'),
  ],
};

if (process.env.NODE_ENV === 'production') {
  config.plugins.push(
    new webpack.optimize.DedupePlugin(),
    new webpack.optimize.UglifyJsPlugin({ minimize: true })
  );
}

Here we specify we want two different webpack entries, one for the JavaScript and the other one for stylesheets, both placed inside the web/static folder. Our output files are going to be created in the private/static folder. As we are going to use some ES6/7 and JSX features we will use Babel with some presets designed for this.

The final step is to tell Phoenix to start Webpack every time we start our development server, so it watches our changes while we are developing and generates the resulting asset files that the main view layout is going to reference. To do so we have to add a watcher in the config/dev.exs file:

# config/dev.exs

config :phoenix_trello, PhoenixTrello.Endpoint,
  http: [port: 4000],
  debug_errors: true,
  code_reloader: true,
  cache_static_lookup: false,
  check_origin: false,
  watchers: [
    node: ["node_modules/webpack/bin/webpack.js", "--watch", "--color", cd: Path.expand("../", __DIR__)]
  ]

...

If we now start our development server we can see that Webpack is also running and watching:

$ mix phoenix.server
[info] Running PhoenixTrello.Endpoint with Cowboy using http on port 4000
Hash: 93bc1d4743159d9afc35
Version: webpack 1.12.10
Time: 6488ms
              Asset     Size  Chunks             Chunk Names
  js/application.js  1.28 MB       0  [emitted]  application
css/application.css  49.3 kB       0  [emitted]  application
   [0] multi application 40 bytes {0} [built]
    + 397 hidden modules
Child extract-text-webpack-plugin:
        + 2 hidden modules

Front-end basic structure

Now that we have everything ready to start coding, let's begin by creating our front-end app structure which is going the need the following npm packages among others:

  • bourbon and bourbon-neat, my all time favorite Sass mixin library.
  • history to manage history with JavaScript.
  • react and react-dom.
  • redux and react-redux for handling the state.
  • react-router as routing library.
  • redux-simple-router to keep route changes in the state.

I'm not going to waste any time on talking about stylesheets as I'm still modifying them at this moment but what I'd like to mention is that for creating a suitable file structure to organize my Sass files I usually follow css-burrito, which in my personal opinion is very useful.

We need to configure our Redux store so let's create the following file:

//web/static/js/store/index.js

import { createStore, applyMiddleware } from 'redux';
import { routerMiddleware }             from 'react-router-redux';
import createLogger                     from 'redux-logger';
import thunkMiddleware                  from 'redux-thunk';
import reducers                         from '../reducers';

const loggerMiddleware = createLogger({
  level: 'info',
  collapsed: true,
});

export default function configureStore(browserHistory) {
  const reduxRouterMiddleware = routerMiddleware(browserHistory)
  const createStoreWithMiddleware = applyMiddleware(reduxRouterMiddleware, thunkMiddleware, loggerMiddleware)(createStore);

  return createStoreWithMiddleware(reducers);
}

Basically we are configuring the Store with three middlewares:

  • routerMiddleware to dispatch router actions to the store.
  • redux-thunk to dispatch async actions.
  • redux-logger to log any action and state changes through the browser's console.

We also need to pass all the combined state reducers, so let's create a basic version of that file:

//web/static/js/reducers/index.js

import { combineReducers }  from 'redux';
import { routerReducer }    from 'react-router-redux';
import session              from './session';

export default combineReducers({
  routing: routeReducer,
  session: session,
});

As starting point we are only going to need two reducers, the routeReducer which will automatically set routing changes into the state and a session reducer which looks like this:

//web/static/js/reducers/session.js

const initialState = {
  currentUser: null,
  socket: null,
  error: null,
};

export default function reducer(state = initialState, action = {}) {
  return state;
}

Its initial state will consists of the currentUser object which we will set after authenticating visitors, the sockect that we will use for connecting to channels and an error to keep track of any issue while authenticating the user.

Having all this prepared now we can go to our main application.js file and render de Root component:

//web/static/js/application.js

import React                    from 'react';
import ReactDOM                 from 'react-dom';
import { browserHistory }       from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
import configureStore           from './store';
import Root                     from './containers/root';

const store = configureStore(browserHistory);
const history = syncHistoryWithStore(browserHistory, store);

const target = document.getElementById('main_container');
const node = <Root routerHistory={history} store={store} />;

ReactDOM.render(node, target);

We create the history, configure the store we finally render the Root component in the main application layout which will be a Redux Provider wrapper for the routes:

//web/static/js/containers/root.js

import React, { PropTypes }         from 'react';
import { Provider }                 from 'react-redux';
import { Router, RoutingContext }   from 'react-router';
import invariant                    from 'invariant';
import configRoutes                 from '../routes';

const propTypes = {
  routerHistory: PropTypes.object.isRequired,
  store: PropTypes.object.isRequired
};

const Root = ({ routerHistory, store }) => {
  invariant(
    routerHistory,
    '<Root /> needs either a routingContext or routerHistory to render.'
  );

  return (
    <Provider store={store}>
      <Router history={routerHistory}>
        {configRoutes(store)}
      </Router>
    </Provider>
  );
};

Root.propTypes = propTypes;
export default Root;

Now let's define our, very basic, routes file:

//web/static/js/routes/index.js

import { IndexRoute, Route }  from 'react-router';
import React                  from 'react';
import MainLayout             from '../layouts/main';
import RegistrationsNew       from '../views/registrations/new';

export default (
  <Route component={MainLayout}>
    <Route path="/" component={RegistrationsNew} />
  </Route>
);

Our application is going to be wrapped inside the MainLayout component and the root path will render the registrations view. The final version of this file is a bit more complex due to the authentication mechanism we'll be implementing, but we'll talk about it on the next post.

Finally we need to add the html container where we'll render the Root component in the main Phoenix application layout:

<!-- web/templates/layout/app.html.eex -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="ricardo@codeloveandboards.com">

    <title>Phoenix Trello</title>
    <link rel="stylesheet" href="<%= static_path(@conn, "/css/application.css") %>">
  </head>

  <body>
    <main id="main_container" role="main"></main>
    <script src="<%= static_path(@conn, "/js/application.js") %>"></script>
  </body>
</html>

Note both the link and the script tags referencing the static assets generated by Webpack.

As we are going manage our routing on the front-end, we need to tell Phoenix to handle any http request through the index action of the PageController which will just render the main layout and our Root component:

# master/web/router.ex

defmodule PhoenixTrello.Router do
  use PhoenixTrello.Web, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  scope "/", PhoenixTrello do
    pipe_through :browser # Use the default browser stack

    get "/*path", PageController, :index
  end
end

That's it for now. On the next post we'll be covering how to create our first database migration, the User model and all the functionality for creating new user accounts. In the meanwhile you can check out the live demo and the final result source code:

Happy coding!