(Full Stack React)-BuildingYelp.pdf

August 10, 2017 | Author: Gustavo Luis Condoy Pogo | Category: Cascading Style Sheets, Json, Java Script, Google Chrome, Variable (Computer Science)
Share Embed Donate


Short Description

Download (Full Stack React)-BuildingYelp.pdf...

Description

As we’re writing the Fullstack React book, we get a lot of questions about how to build large applications with React and how to integrate external APIs. tl;dr - This post will guide you through building a full React app, even with little to no experience in the framework. We’re going build a Yelp clone in React You can get the completed code here Try the demo here

Let’s build a lightweight clone of Yelp using React. In this tutorial we’ll talk about: How to setup a new React project from scratch How to create a basic React component How to write modular CSS using postcss How to setup testing How route to different pages with react-router

How to integrate with Google Maps How to write a Google Maps React component How to write a five-star rating component We’ll be tying in a lot of different pieces of React together to build a full-sized React app. This post will guide you through building a full React app, even with little to no experience in the framework. Let’s buckle up and get building.

Table of Contents 1. Setup 01. A Word on Dependencies 02. Babel 03. webpack 04. React 05. Creating Our app.js 06. Demo: Basic app.js 07. postcss 08. CSS modules 09. Demo: CSS Modules 10. Configuring Multiple Environments 11. Font Awesome 12. Demo: Environment 13. Webpack Tip: Relative requires 2. Configuring Testing 01. Building Our Test Skeleton 02. Our Testing Strategy 3. Routing 01. Building real routes 02. Main page and nested routes 03. Demo: Routing to Container Content 04. Routing to Maps 05. Demo: Routing to a Map 4. Getting a List of Places 01. Demo: A List of Places 5. Creating a Sidebar 01. Inlining Styles 02. CSS Variables 6. Splitting Up Components 01. Business Listing Component 02. Item Component 03. Rating Component 04. Demo: Rating Stars 05. Demo: Sidebar with Listings

7. Building the Main Pane 01. Creating Markers On Our Map 02. Clicking on Markers 03. Adding More Details to Markers 04. Demo: Explore Google Places JSON API 05. Going Responsive α. display: flex β. flex-direction γ. order 8. Making the Map Page the Index 9. Conclusion

Setup One of the most painful parts of building a React app is building the boilerplate. We have so many choices we can make to start building our application, it can be overwhelming how to even get started. We’re going to be building our application using a few tools that we find useful as well and help us build our production apps here at Fullstack.io. Check out the final version of the package.json and the webpack.config.js on github at github.com/fullstackreact/react-yelp-clone While there are a ton of great boilerplates you can use, often times using a boilerplate can be more confusing than setting things up yourself. In this post we’re going to install everything directly and so this should give you a good idea about how to start a new project from scratch on your own. Throughout this process, we’ll use some JavaScript features of ES6, inline css modules, async module loading, tests, and more. We’ll use webpack for it’s ease of babel implementation as well as a few other convenient features it provides. In order to follow along with this process, ensure you have node.js installed and have npm available in your $PATH. If you’re not sure if you have npm available in $PATH, This is the folder structure we’ll be building towards:

Let’s create a new node project. Open a terminal and create the beginning of our folder structure: $ mkdir yelp && cd $_ $ mkdir -p src/{components,containers,styles,utils,views}\ && touch webpack.config.js

In the same directory, let’s create our node project by using the npm init command and answering a few questions about the project. After this command finishes, we’ll have a package.json in the same directory, which will allow us to define a repeatable process for building our app. It doesn’t quite matter how we answer the questions at this point, we can always update the package.json to reflect changes. Additionally, instead of the command npm init, we can use npm init -y to accept all the defaults and not answer any questions. $ npm init

We’ll need a few dependencies to get started.

A Word on Dependencies TLDR; Install the dependencies in each of the code sample sections. Before we can start building our app, we’ll need to set up our build chain. We’ll use a combination of npm and some configuration files.

Babel Babel is a JavaScript compiler that allows us to use the next generation JavaScript today. Since these features are not only convenient, but they make the process of writing JavaScript more fun. Let’s grab babel along with a few babel presets. In the same directory as the package.json, let’s install our babel requirements: $ npm install --save-dev babel-core babel-preset-es2015 babel-preset-react babelpreset-react-hmre babel-preset-stage-0

We’ll need to configure babel so our application will compile. Configuring babel is easy and can be set up using a file called .babelrc at the root of the project (same place as our package.json) file. $ touch .babelrc

Let’s include a few presets so we can use react as well as the react hot reloading features: { "presets": ["es2015", "stage-0", "react"] }

Babel allows us to configure different options for different operating environments using the env key in the babel configuration object. We’ll include the babel-hmre preset only in our development environment (so our production bundle doesn’t include the hot reloading JavaScript). { "presets": ["es2015", "stage-0", "react"], "env": { "development": { "presets": ["react-hmre"] } } }

webpack Setting up webpack can be a bit painful, especially without having a previous template to follow. Not to worry, however! We’ll be building our webpack configuration with the help of a well-built webpack starter tool called hjs-webpack. The hjs-webpack build tool sets up common loaders for both development and production environments, including hot reloading, minification, ES6 templates, etc. Let’s grab a few webpack dependencies, including the hjs-webpack package: $ npm install --save-dev hjs-webpack webpack

Webpack is a tad useless without any loaders or any configuration set. Let’s go ahead and install a few loaders we’ll need as we build our app, including the babel-loader, css/styles, as well as the the url and file loaders (for font-loading, built-in to hjs-webpack): $ npm install --save-dev babel-loader css-loader style-loader postcss-loader urlloader file-loader

In our webpack.config.js at the root directory, let’s get our webpack module started. First, let’s get some require statements out of the way: const webpack = require('webpack');

const fs const path join resolve

= = = =

require('fs'); require('path'), path.join, path.resolve;

const getConfig = require('hjs-webpack');

The hjs-webpack package exports a single function that accepts a single argument, an object that defines some simple configuration to define a required webpack configuration. There are only two required keys in this object: in - A single entry file out - the path to a directory to generate files var config = getConfig({ in: join(__dirname, 'src/app.js'), out: join(__dirname, 'dist') })

The hjs-webpack includes an option called clearBeforeBuild to blow away any previously built files before it starts building new ones. We like to turn this on to clear away any strangling files from previous builds. var config = getConfig({ in: join(__dirname, 'src/app.js'), out: join(__dirname, 'dist'), clearBeforeBuild: true })

Personally, we’ll usually create a few path variables to help us optimize our configuration when we start modifying it from it’s default setup. const const const const

root src modules dest

= = = =

resolve(__dirname); join(root, 'src'); join(root, 'node_modules'); join(root, 'dist');

var config = getConfig({ in: join(src, 'app.js'), out: dest, clearBeforeBuild: true })

Now, the hjs-webpack package sets up the environment specific (dev vs prod) configuration using the first argument value process.argv[1], but can also accept an option to define if we’re working in the development environment called isDev.

A development environment sets up a server without minification and accepts hot-reloading whereas a production one does not. Since we’ll use the value of isDev later in our configuration, we’ll recreate the default value in the same method. Alternatively, we can check to see if the NODE_ENV is ‘development’: const NODE_ENV = process.env.NODE_ENV; const isDev = NODE_ENV === 'development'; // alternatively, we can use process.argv[1] // const isDev = (process.argv[1] || '') // .indexOf('hjs-dev-server') !== -1; // ... var config = getConfig({ isDev: isDev, in: join(src, 'app.js'), out: dest, clearBeforeBuild: true })

Webpack expects us to export a configuration object from the webpack.config.js file, otherwise it won’t have access to the config variable. We can export the config object by adding the module.exports at the end of the file: // ... var config = getConfig({ // ... }) module.exports = config;

We’ll come back to modifying the configuration file shortly as we get a bit further along and need some more configuration. For the time being, let’s get our build up and running. Check out the final version of the webpack.config.js.

React In order to actually build a react app we’ll need to include the react dependency. Unlike the previous dependencies, we’ll include react (and it’s fellow react-dom) as an app dependency, rather than a development dependency. $ npm install --save react react-dom

We’ll also install react router to handle some routing for us as we’ll have multiple routes in our app, including a map place as well as details page for finding more details about each place we’ll list.

$ npm install --save react-router

A handy shortcut for installing and saving dependencies with the npm command: $ npm i -S [dependencies]

previous command could be rewritten as: $ npm i -S react react-dom

To install and save development dependencies, change the -S to -D, i.e.: $ npm i -D [dependencies]

Creating Our app.js We can’t start building our application without an entry file (as we added in the webpack configuration above). We’ll come back to build our React app with a real app container, but let’s make sure our server and build process are working up through this point. Let’s first start by setting up our app.js with a dummy react app. Create a file called src/app.js. $ touch src/app.js

In this file, let’s create a simple React container to house a single component with some random text. First, including the dependencies that webpack will bundle in our completed application bundle: import React from 'react' import ReactDOM from 'react-dom' const App = React.createClass({ render: function() { return (Text text text); } });

Demo: Basic app.js We’ll need to mount the component on the page before we can see it working. In order to mount the application on the page, we’ll need a DOM node reference to actually set it up, but where? The hjs-webpack package includes a minimal index.html it will generate if we don’t describe a custom one (using the html key in the configuration object). We won’t need to generate a custom html file here, so instead we’ll use the basic generated page. The default template includes a component with an id of root just for the purpose of setting up a Single Page App (SPA, for short). Let’s grab a hold of the DOM node with the id of root and render our basic React component inside of it. Our complete src/app.js file should look like: import React from 'react' import ReactDOM from 'react-dom' const App = React.createClass({ render: function() { return (Text text text) } }); const mountNode = document.querySelector('#root'); ReactDOM.render(, mountNode);

With the src/app.js in place, let’s boot up the server. The hjs-webpack package installs one for us by default in the ./node_modules directory. We can refer directly to it to start the server: $ NODE_ENV=development ./node_modules/.bin/hjs-dev-server

It’s usually a good idea to explicitly set the NODE_ENV, which we do here.

The server will print out a message about the url we can visit the app at. The default address is at http://localhost:3000. We’ll head to our browser (we’ll use Google Chrome) and go to the address http://localhost:3000.

Although it’s not very impressive, we have our app booted and running along with our build process. To stop the devServer, use Ctrl+C. It can be a pain to remember how to start our development server. Let’s make it a tad easier by adding it as a script to our package.json. The package.json has an entry that allows us to add scripts, not surprisingly called the scripts key. Let’s go ahead and add a start script in our package.json. { "name": "yelp", "version": "1.0.0", "description": "", "scripts": { "start": "NODE_ENV=development ./node_modules/.bin/hjs-dev-server", "test": "echo \"Error: no test specified\" && exit 1" }, /* ... */ }

With the start script configured in the scripts key, instead of using the binary directly we can call npm run start to start the server. The start and test scripts in a package.json file are special scripts and with either one of these defined, we can leave out the run in the npm run start command. I.e. $ npm start

All other scripts require the run command to be executed.

postcss Let’s finish off our configuration of our app process by setting up some styles configuration with postcss and CSS modules. PostCSS is a pre/post CSS processor. Similar to lesscss and sass, postcss presents a modular interface for programmatically building CSS stylesheets. The community of plugins and preprocessors is constantly growing and gives us a powerful interface for building styles. Setting postcss in our webpack configuration already works and hjs-webpack will already include one loader if we have it installed, the autoprefixer. Let’s go ahead and install the autoprefixer package:

$ npm install --save-dev autoprefixer

We’ll use a few other postcss preprocessors in our postcss build chain to modify our CSS. The two we’ll use is the precss package, which does a fantastic job at gluing a bunch of common postcss plugins together and cssnano, which does the same for minification and production environments. $ npm i -D precss cssnano

The hjs-webpack only automatically configures the autoprefixer package, not either one of ours, so in order to use these two packages, we’ll need to modify our webpack configuration so webpack knows we want to use them. At it’s core, the hjs-webpack tool creates a webpack configuration for us. If we want to extend it or modify the config it generates, we can treate the return value as a webpack config object. We’ll modify the config object returned with any updates to the config object. The postcss-loader expects to find a postcss key in the webpack config object, so we can just prepend and append our postcss modifiers to the config.postcss array. // ... var config = getConfig({ // ... }) config.postcss = [].concat([

require('precss')({}), require('autoprefixer')({}), require('cssnano')({}) ])

Each of the postcss plugins is exported as a function that returns a postcss processor, so we can have a chance to configure it. We’re not including any modification to the setup here, but it’s possible. For documentation on each one, check the documentation for each plugin: precss autoprefixer cssnano

CSS modules CSS modules are a way for us to interact with CSS definitions inside of JavaScript to avoid one of the cascading/global styles… errr… biggest pains in CSS. In CSS styles, our build script will take care of creating specific, unique names for each style and modifying the actual name in the style itself. Let’s look at it in code. For instance, let’s say we have a css file that includes a single class definition of .container: .container { border: 1px solid red; }

The class of .container is a very generic name and without CSS modules, it would apply to every DOM object with a class of container. This can lead to a lot of conflicts and unintended styling sideeffects. CSS modules allow us to load the style alongside our JavaScript where the style applies and won’t cause a conflict. To use the .container class above in our container, we could import it and apply the style using it’s name. import React from 'react' import ReactDOM from 'react-dom' import styles from './styles.module.css' const App = React.createClass({ // ...

});

The styles object above exports an object with the name of the css class as the key and a unique name for the CSS class as the value. "container" = "src-App-module__container__2vYsV"

We can apply the CSS class by adding it as a className in our React component as we would any other prop. // ... import styles from './styles.module.css' const App = React.createClass({ render: function() { return ( Text text text ); } }); // ...

Demo: CSS Modules In order to use CSS modules, we’ll need to configure webpack to be aware of the fact we’re using css modules. This part gets a little hairy, so let’s tread a little slower here. The css modules documentation page is a fantastic resource to use to get familiar with how they work and best practices in building CSS modules. The postcss-loader gives us a few options we can use to configure css modules. We’ll need to tell webpack how we want our modules named. In development, we’ll want to set up our modules in a slighly nicer way than in production (where we won’t do much debugging of classes). In our webpack.config.js, let’s create a dynamic naming scheme we’ll set as the module names: // ... const cssModulesNames = `${isDev ? '[path][name]__[local]__' : ''}[hash:base64:5]`; // ...

The hjs-webpack package makes it convenient to build our webpack configuration, but since we’re modifying our css loading, we’ll need to not only add a loader (to load our modules), but we’ll need to modify the existing one. Let’s load the initial loader by finding it in the array of config.module.loaders using a simple regex: const matchCssLoaders = /(^|!)(css-loader)($|!)/; const findLoader = (loaders, match) => { const found = loaders.filter(l => l && l.loader && l.loader.match(match)); return found ? found[0] : null; } // existing css loader const cssloader = findLoader(config.module.loaders, matchCssLoaders);

With our loader found in the existing module.loaders list, we can create a clone of the loader and add a new one that targets modules. It can be convenient to use a global stylesheet. By adding and modifying the existing css loader in webpack, we can retain the ability to import global styles as well as include css modules. Back in our webpack.config.js, let’s create a new loader as well as modify the existing loader to support loading css modules: // ... const newloader = Object.assign({}, cssloader, { test: /\.module\.css$/, include: [src], loader: cssloader.loader .replace(matchCssLoaders, `$1$2?modules&localIdentName=${cssModulesNames}$3`) }) config.module.loaders.push(newloader); cssloader.test = new RegExp(`[^module]${cssloader.test.source}`) cssloader.loader = newloader.loader // ...

In our new loader, we’ve modified the loading to only include css files in the src directory. For loading any other css files, such as font awesome, we’ll include another css loader for webpack to load without modules support:

config.module.loaders.push({ test: /\.css$/, include: [modules], loader: 'style!css' })

Credit for this (slightly modified) technique of loading css modules with webpack and hjswebpack goes to lukekarrys With our css loading devised in webpack, let’s create a single global style in our app at src/app.css. $ echo "body{ border: 1px solid red;}" > src/app.css

In our src/app.js, we can include these styles: // ... import './app.css' // ...

Starting up our server with npm start and refreshing our browser will reveal that global css loading works as expected.

To confirm our css module loading works as expected, let’s create a styles.module.css in our src/ directory with a single .wrapper css class (for now): // In src/styles.module.css .wrapper { background: blue; }

Loading our css module file in our src/app.js and applying the .wrapper class to our component is straightforward with the className prop: // ... import './app.css' import styles from './styles.module.css' const App = React.createClass({ render: function() { return ( Text text text ) } }); // ...

Loading our server using npm start and refreshing our Chrome window, we see that our css module style is set from the styles.wrapper class created by the css module.

Configuring Multiple Environments In our app, we’re going to interface with the Google API. As it’s never a good idea to hardcode our keys in a deployed application, we’ll need a way to configure our app to include dynamic API keys based upon the environment. One effective method for key handling is by using the environment variables of the system we’re building against and bundling our key. Using a combination of the webpack.DefinePlugin() and dotenv, we can create a multi-environment build process using our environment variables. First, let’s install the dotenv package: $ npm i -D dotenv && touch .env

In our .env file we created at the root of the project, we can set environment variables that we can build into the project. The dotenv project allows us to load configuration scripts and gives us access to these variables. Loading the variables is a pretty simple process using dotenv. In our webpack.config.js file, let’s load up the .env file in our environment: // ... const dotenv = require('dotenv');

const dotEnvVars = dotenv.config();

Our .env file is generally a good spot to place global environment variables. To separate our environments, we’ll create a mechanism to load those environment variables as well. Generally, we’ll keep these in a config/ directory as [env].config.js. To load these files in our server, we can use the same function, except adding a few options to change the source of the file. In our webpack.config.js file, let’s add loading the second environment variables: // ... const NODE_ENV = process.env.NODE_ENV; const dotenv = require('dotenv'); // ... const dotEnvVars = dotenv.config(); const environmentEnv = dotenv.config({ path: join(root, 'config', `${NODE_ENV}.config.js`), silent: true, });

We can merge these two objects together to allow the environment-based [env].config.js file to overwrite the global one using Object.assign(): // ... const dotEnvVars = dotenv.config(); const environmentEnv = dotenv.config({ path: join(root, 'config', `${NODE_ENV}.config.js`), silent: true, }); const envVariables = Object.assign({}, dotEnvVars, environmentEnv);

Our envVariables variable now contains all the environment variables and globally defined environment variables. In order to reference them in our app, we’ll need to grant access to this envVariables variable. Webpack ships with a few common plugins including the DefinePlugin(). The DefinePlugin() implements a regex that searches through our source and replaces variables defined in a key-value object, where the keys are the names of variables and their value is replaced in the source before shipping to the browser.

It’s conventional to surround the replaced variable by two underscores (__) on either side of the variable. For instance, access to the NODE_ENV variable in our source would be referenced by __NODE_ENV__. We can programmatically walk through our envVariables and replace each key in the conventional manner and stringifying their values. We’ll want to stringify the values we’ll replace using the DefinePlugin() as they might contain characters that a browser’s JavaScript parser won’t recognize. Stringifying these values helps avoid this problem entirely. In our webpack.config.js file, let’s use the reduce() method to create an object that contains conventional values in our source with their stringified values: // ... const envVariables = Object.assign({}, dotEnvVars, environmentEnv); const defines = Object.keys(envVariables) .reduce((memo, key) => { const val = JSON.stringify(envVariables[key]); memo[`__${key.toUpperCase()}__`] = val; return memo; }, { __NODE_ENV__: JSON.stringify(NODE_ENV) });

The defines object can now become the configuration object that the DefinePlugin() plugin expects to use to replace variables. We’ll prepend the existing webpack plugin list with our DefinePlugin(): // ... const defines = // ... config.plugins = [ new webpack.DefinePlugin(defines) ].concat(config.plugins); // ...

Checking to see if the replacement is working as we expect, we can set our component to display these variables (as JavaScript strings). For instance, to see the environment using the __NODE_ENV__ value, we can modify the render() function:

//... const App = React.createClass({ render: function() { return ( Environment: {__NODE_ENV__} ) } }); // ...

Kicking up our server using npm start and refreshing our browser, we’ll see that the value has been replaced by the string development (as set by NODE_ENV=development in our start script).

Font Awesome In our app, we’ll use Font Awesome to display rating stars. We’ve already handled most of the work required to get font awesome working. We’ll just need to install the font-awesome dependency and require the css in our source. Installing the dependency is straightforward using npm: $ npm i -S font-awesome

To use the fonts in font-awesome, we just need to apply the proper classes as described in the font awesome docs after we require the css in our source. Requiring the font-awesome css in our source is pretty easy. Since we’ll use this across components, we can require it in our main src/app.js: import React from 'react' import ReactDOM from 'react-dom' import 'font-awesome/css/font-awesome.css' // ...

Using font-awesome in our react components is like using font-awesome outside of react, placing the right css classes. To add a star to our component from font-awesome, we can modify our render() function: // ... import 'font-awesome/css/font-awesome.css' // ... const App = React.createClass({ render: function() { return ( Environment: {__NODE_ENV__} ) } });

Reloading our browser, we can see that the font-awesome css has loaded correctly and is displaying the star icon from the font-awesome icon library:

Demo: Environment Webpack Tip: Relative requires As we’re using webpack to package our app, we can use it to make packaging our relative requires simpler. Rather than requiring files relative to the directory that the current file is located in, we can require them using an alias. Let’s add the webpack root to be both the node_modules/ directory as well as the src/ directory. We can also set up a few aliases referencing the directories we previously created: var config = getConfig({ // ... }) // ... config.resolve.root = [src, modules] config.resolve.alias = { 'css': join(src, 'styles'), 'containers': join(src, 'containers'), 'components': join(src, 'components'), 'utils': join(src, 'utils') }

In our source, instead of referencing our containers by relative path, we can simply call require('containers/SOME/APP').

Configuring Testing React offers a wide range of methods of testing that our application is working as we expect it to work. We’ve been opening the browser and refreshing the page (although, hot-reloading is set up, so even refreshing the page isn’t a requirement). Although developing with such rapid feedback is great and offers convenience at development time, writing tests to programmatically test our application is the quickest, most reliable way to ensure our app works as we expect it to work. Most of the code we will write in this section will be test-driven, meaning we’ll implement the test first and then fill out the functionality of our components. Let’s make sure that we can test our code. Although the react team uses jest (and we cover it in-depth in fullstackreact), we’ll be using a combination of tools: karma is our test runner chai is our expectation library mocha as our test framework enzyme as a react testing helper sinon as a spy, stub, and moch framework Let’s start by installing our testing dependencies. We’ll install the usual suspects, plus a babel polyfill so we can write our tests using ES6. $ npm i -D mocha chai enzyme chai-enzyme expect sinon babel-register babel-polyfill react-addons-test-utils

We’ll be using a library called enzyme to make testing our react components a bit easier and for fun to write. In order to set it up properly, however, we will need to make a modification to our webpack setup. We’ll need to install the json-loader to load json files along with our javascript files (hjswebpack automatically configures the json loader for us, so we won’t need to handle updating the webpack configuration manually): $ npm i -D json-loader

We’ll be using karma to run our tests, so we’ll need to install our karma dependencies. We’ll use karma as it’s a good compliment to webpack, but it does require a bit of setup.

Karma has a fast testing iteration, it includes webpack compiling, runs our tests through babel, and mounts our testing environment in a browser just the same as though we are testing it in our own browser. Additionally, it is well supported and has a growing community working with karma. It makes it a good candidate for us to use together with our webpack build pipeline. Let’s install the dependencies for karma: $ npm i -D karma karma-chai karma-mocha karma-webpack karma-phantomjs-launcher phantomjs-prebuilt phantomjs-polyfill $ npm i -D karma-sourcemap-loader

We’ll be using PhantomJS to test our files so we don’t actually need to launch a browser with a window. PhantomJS is a headless, WebKit-driven, scriptable browser with a JS API and allows us to run our tests in the background. If you prefer to use Google Chrome to run the tests with a window, swap out karmaphantomjs-launcher with karma-chrome-launcher and don’t update the config below. Grab a cup of tea to let these install (phantom can take a little while to install). Once they are ready, we’ll need to create two config files to both configure karma as well as the tests we’ll have karma launch. Let’s set up our webpack testing environment through karma. The easiest way to get started with karma is by using the karma init command: $ karma init

After we answer a few questions, it will spit out a karma.conf.js file. Since we’re going to manipulate most of this file, it’s a good idea to just press enter on all of the questions to have it generate the file for us.

Alternatively, we can touch the file as we have done with other files and recreate the file: $ touch karma.conf.js

With our karma.conf.js file generated, we’ll need to give it a few configuration options, most of which are autogenerated or we have already set up. First, the basics. We’ll use some default options that karma has spit out for us automatically: module.exports = function(config) { config.set({ // ... basePath: '', preprocessors: {}, port: 9876, colors: true, logLevel: config.LOG_INFO, browsers: ['Chrome'], concurrency: Infinity, plugins: [] }); }

We’ll need to tell karma that we want to use mocha and chai as the testing framework, instead of the default jasmine framework, so let’s change the frameworks: [] option. We’ll also need to add these to the plugins karma will use. module.exports = function(config) { config.set({ frameworks: ['mocha', 'chai'], basePath: '', plugins: [ 'karma-mocha', 'karma-chai' ], // ... }); }

As we’re using webpack to compile our files together, we’ll also need to tell karma about our webpack configuration. Since we already have one, there is no need to recreate it, we’ll just require our original one. var webpackConfig = require('./webpack.config'); module.exports = function(config) { config.set({ frameworks: ['mocha', 'chai'], webpack: webpackConfig, webpackServer: { noInfo: true }, // ... }); }

We’ll also need to tell karma how to use webpack in it’s confgiuration. We can do this by setting the karma-webpack plugin in it’s plugin list. module.exports = function(config) { config.set({ frameworks: ['mocha', 'chai'], webpack: webpackConfig, webpackServer: { noInfo: true }, plugins: [ 'karma-mocha', 'karma-chai', 'karma-webpack'

], // ... }); }

Let’s change the reporter to something a tad nicer (the spec-reporter, instead of the default progress) reporter and change the browser from Chrome to PhantomJS (adding the proper plugins). First, let’s install the spec reporter: $ npm i -D karma-spec-reporter

Back in our karma.conf.js file, let’s add the plugins and change the browsers and plugins: module.exports = function(config) { config.set({ reporters: ['spec'], plugins: [ 'karma-mocha', 'karma-chai', 'karma-webpack', 'karma-phantomjs-launcher', 'karma-spec-reporter' ], browsers: ['PhantomJS'] // ... }); }

Finally, we need to test karma where to find the files it will run as our tests. Instead of pointing it to the actual tests, we’ll use a middleman, a webpack config to tell Karma where to find the tests and package them together. module.exports = function(config) { config.set({ files: [ 'tests.webpack.js' ], // ... }) }

Before we move on, we’ll need to let karma know that it needs to run our tests.webpack.js file through the webpack preprocessor. We’ll also ask it to run it through a sourcemap preprocessor to spit out usable sourcemaps (so we can debug our code effectively):

module.exports = function(config) { config.set({ files: [ 'tests.webpack.js' ], preprocessors: { 'tests.webpack.js': ['webpack', 'sourcemap'] }, plugins: [ 'karma-mocha', 'karma-chai', 'karma-webpack', 'karma-phantomjs-launcher', 'karma-spec-reporter', 'karma-sourcemap-loader' ], // ... }); }

Let’s create the tests.webpack.js file. This file will serve as middleware between karma and webpack. Karma will use this file to load all of the spec files, compiled through webpack. The file is fairly simple: require('babel-polyfill'); var context = require.context('./src', true, /\.spec\.js$/); context.keys().forEach(context);

When karma executes this file, it will look through our src/ directory for any files ending in .spec.js and execute them as tests. Here, we can set up any helpers or global configuration we’ll use in all of our tests. Since we’re going to be using a helper called chai enzyme, we can set our global configuration up here: require('babel-polyfill'); // some setup first var chai = require('chai'); var chaiEnzyme = require('chai-enzyme'); chai.use(chaiEnzyme()) var context = require.context('./src', true, /\.spec\.js$/); context.keys().forEach(context);

Up through this point, our complete karma conf file should look like this: var path = require('path'); var webpackConfig = require('./webpack.config'); module.exports = function(config) { config.set({ basePath: '', frameworks: ['mocha', 'chai'], files: [ 'tests.webpack.js' ], preprocessors: { // add webpack as preprocessor 'tests.webpack.js': ['webpack', 'sourcemap'], }, webpack: webpackConfig, webpackServer: { noInfo: true }, plugins: [ 'karma-mocha', 'karma-chai', 'karma-webpack', 'karma-phantomjs-launcher', 'karma-spec-reporter', 'karma-sourcemap-loader' ], reporters: ['spec'], port: 9876, colors: true, logLevel: config.LOG_INFO, browsers: ['PhantomJS'] }) };

We’ve covered almost the entire karma setup, but we’re missing two final pieces. Before we complete the karma setup, let’s create a sample test file so we can verify our test setup is complete. Instead of placing a sample file in the root (only to move it later), let’s place it in it’s final spot. We’re going to use the component we created earlier as a container for the rest of our app. We’ll create a spec file in the containers/App/App.spec.js file. $ mkdir src/containers/App && touch src/containers/App.spec.js

In here, let’s create a simple test that tests for the existence of an element with a custom wrapper style class (from our CSS modules). Without going in-depth to writing tests (yet), this simple test describes our intention using mocha and chai. import React from 'react' import { expect } from 'chai' import { shallow } from 'enzyme' import App from './App' import styles from './styles.module.css' describe('', function () { let wrapper; beforeEach(() => { wrapper = shallow() }) it('has a single wrapper element', () => { expect(wrapper.find(`.${styles.wrapper}`)) .to.have.length(1); }); });

We walk through testing our app later in this course. For the time being, feel free to copy and paste the code into your own file to get us through setting up our build/test workflow. To get this test running, we’ll need to create two more files from the previous test. The src/containers/App.js file along with the custom CSS module src/containers/styles.module.css. We don’t need to make our tests pass, initially, just get them running. Let’s create the App.js file and move our original src/styles.module.css into the container directory: $ touch src/containers/App/App.js $ mv src/styles.module.css \ src/containers/App/styles.module.css

Let’s go ahead and move our definition from src/app.js into this new file at src/containers/App/App.js: import React from 'react' import ReactDOM from 'react-dom'

import styles from './styles.module.css' const App = React.createClass({ render: function() { return ( Environment: {__NODE_ENV__} ) } }); module.exports = App;

Finally, we’ll need to import the component from the right file in our src/app.js: // ... import './app.css' import App from 'containers/App/App' const mountNode = document.querySelector('#root'); ReactDOM.render(, mountNode);

To execute our tests, we’ll use the karma command installed in our ./node_modules directory by our previous npm install: $ NODE_ENV=test \ ./node_modules/karma/bin/karma start karma.conf.js

Uh oh! We got an error. Do not worry, we expected this… don’t look behind the curtain… This error is telling us two things. The first is that webpack is trying to find our testing framework and bundle it in with our tests. Webpack’s approach to bundling is using a static file analyzer to find all the dependencies we’re using in our app and to try to bundle those along with our source. As enzyme imports some dynamic files, this approach doesn’t work. Obviously we don’t want to do this as we don’t need to bundle tests with our production framework. We can tell webpack to ignore our testing framework and assume that it’s available for us by setting it as an external dependency. In our webpack.config.js file, let’s set a few external dependencies that enzyme expects: // ./webpack.config.js // ... var config = getConfig({ isDev, in: join(src, 'app.js'), out: dest, clearBeforeBuild: true }); config.externals = { 'react/lib/ReactContext': true, 'react/lib/ExecutionEnvironment': true } // ...

The second error we’ve encountered is that our testing framework is that a few of our production webpack plugins are mucking with our tests. We’ll need to exclude a few plugins when we’re running webpack under a testing environment. Since we’re now handling two cases where testing with webpack differs from production or development, let’s create a conditional application for our webpack testing environment. First, we can tell if we are in a testing environment by checking to see if the command we are issuing is karma OR by checking the NODE_ENV is test. At the top of our webpack.config.js file, let’s set our variable isTest: require('babel-register'); const NODE_ENV = process.env.NODE_ENV; const isDev = NODE_ENV === 'development'; const isTest = NODE_ENV === 'test'; // ...

Later in our config file, we can manipulate our config under testing environments vs. dev/production environments. Moving our previous externals definition into this conditional statement and excluding our production plugins, our updated webpack.config.js file: // ./webpack.config.js // ... var config = getConfig({ // ... }); if (isTest) { config.externals = { 'react/lib/ReactContext': true, 'react/lib/ExecutionEnvironment': true } config.plugins = config.plugins.filter(p => { const name = p.constructor.toString(); const fnName = name.match(/^function (.*)\((.*\))/) const idx = [ 'DedupePlugin', 'UglifyJsPlugin' ].indexOf(fnName[1]); return idx < 0; }) } // ...

Now, if we run our tests again, using karma we’ll see that our tests are running, they are just not passing yet. $ NODE_ENV=test \ ./node_modules/karma/bin/karma start karma.conf.js

Let’s get our test passing! First, let’s wrap our long karma command into an npm script instead of running it at the command-line. In our package.json file, let’s update the test script with our karma command. { "name": "yelp", "version": "1.0.0", "description": "", "scripts": { "start": "./node_modules/.bin/hjs-dev-server", "test": "NODE_ENV=test ./node_modules/karma/bin/karma start karma.conf.js" }, // ... }

Instead of passing the previous command, we can run our tests with npm test: $ npm test

Our previous test is not passing because although we have defined a .wrapper{} class in our src/containers/App/styles.module.css file, it’s empty so webpack just discards the class and styles.wrapper ends up undefined. In order to get our test passing, we’ll need to add a description to it. .wrapper { display: flex; }

We’ll be using the flexbox layout in our app, so we can use display: flex; in our css description. Running our tests again, using npm test this time, we can see that our test goes all green (i.e. passes).

It can be a tad painful when flipping back and forth between our terminal and code windows. It would be nice to have our tests constantly running and reporting any failures instead. Luckily karma handles this easily and so can we. We’ll use a command-line parser to add an npm script to tell karma to watch for any file changes. In our package.json file, let’s add the test:watch command: { "name": "yelp", "version": "1.0.0", "description": "", "scripts": {

"start": "./node_modules/.bin/hjs-dev-server", "test": "NODE_ENV=test ./node_modules/karma/bin/karma start karma.conf.js", "test:watch": "npm run test -- --watch" }, // ... }

Instead of using npm test, we’ll launch our test watcher by using npm run test:watch. We’ll also need to tell karma (by using our karma.conf.js config file) that we want it to watch for any changes to our files. Karma handles this out of the box for us by using the singleRun key in it’s configuration object. We can set this using a command-line parser called yargs. Let’s install yargs as a development dependency: $ npm i -D yargs

In our karma.conf.js file, we can check for the --watch flag using yargs and set the singleRun option based upon the existence of the --watch flag. var argv = require('yargs').argv; // ... module.exports = function(config) { config.set({ basePath: '', frameworks: ['mocha', 'chai'], // ... singleRun: !argv.watch }); }

Now, when we execute the npm run test:watch script and modify and save a file, our tests will be executed, making for easy, fast test-driven development.

Building Our Test Skeleton Let’s build the “infrastructure” of our app first. Our container element will contain the structure of the page, which essentially boils down to handling routes. Being good software developers, let’s build our test first to verify that we are able to ensure our assumptions about the component. Let’s make sure that we have a component loaded in our app. This is also a good time for us to spend setting up our app testing structure.

In src/contains/App.spec.js, let’s build the beginning of our jasmine spec. First, we’ll need to include our libraries and the App component itself: import { expect } from 'chai' import { shallow } from 'enzyme' import App from './App' describe('', () => { // define our tests in here })

We’ll use expect() to set up our expectations and shallow() (from enzyme) to render our elements into the test browser.

Our Testing Strategy When testing any code in any language, the principles of testing are pretty much the same. We want to: 1. define the focused functionality 2. set an expectation of the output 3. compare the executed code with the test code In Jasmine, each of these steps are well-defined: 1. Using describe()/it() defines the functionality 2. We’ll use expect() to set the expectation 3. We’ll use beforeEach() and matchers to confirm the output To set up our our test, we’ll need to pretend we’re rendering our component in the browser. Enzyme makes this easy regardless of handling shallow or deep rendering (we’ll look at the difference later). In our test, we can shallow render our component into our browser and store the result (which will be our rendered DOM component). Since we’ll want a “clean” version of our component every time, we need to do this in the beforeEach() block of our test: // ... describe('', () => { // define our tests in here let wrapper; // "dom" node wrapper element beforeEach(() => { wrapper = shallow(); }) })

With our test set up, we can test that our component contains a Router component by using the find() method in our wrapper instance. To set the expectation in our component, we’ll use expect(): // ... describe('', () => { // ... it('has a Router component', () => { expect(wrapper.find('Router')) .to.have.length(1); }) })

Finally, we can run our tests by using our npm script we previously built: $ npm run test

Since we haven’t implemented the component, our test will fail. Let’s turn our test green.

Routing Before we implement our routes, let’s take a quick look at how we’ll set up our routing. When we mount our react app on the page, we can control where the routes appear by using the children to situate routes where we want them to appear. In our app, we’ll have a main header bar that we’ll want to exist on every page. Underneath this main header, we’ll switch out the content for each individual route. We’ll place a component in our app as a child of the component with rules which designate which children should be placed on the page at any given route. Thus, our component we’ve been working with will simply become a container for route handling, rather than an element to hold/display content. Although this approach sounds complex, it’s an efficient method for holding/generating routes on a per-route basis. It also allows us to create custom data handlers/component generators which come in handy for dealing with data layers, such as Redux. With that being said, let’s move on to setting up our main views. In our src/containers/App.js, let’s make sure we import the react-router library. import React, { PropTypes } from 'react'; import { Router } from 'react-router'; // ...

Next, in our usual style, let’s build our React component (either using the createClass({}) method we used previously or using the class-based style, as we’ll switch to here): import React, { PropTypes } from 'react'; import { Router } from 'react-router'; class App extends React.Component { render() { return (Content) } }

We like to include our content using the classical getter/setter method, but this is only a personal preference. import React, { PropTypes } from 'react' import { Router } from 'react-router'

class App extends React.Component { // class getter content() { return () } render() { return ( {this.content} ) } }

We’ll use our app container to return an instance of the component. The component require us to pass a history object which tells the browser how to listen for the location object on a document. The history tells our react component how to route. There are multiple different types of history objects we can use, but the two most popular types are the browserHistory and hashHistory types. The browserHistory object uses the native html5 react router to give us routes that appear to be server-based. On the other hand, the hashHistory uses the # sign to manage navigation. Hash-based history, an old trick for client-side routing is supported in all browsers. We’ll use the browserHistory method here. We need to tell the instance we want to use the browserHistory by passing it as a prop in our routing instance: import React, { PropTypes } from 'react'; import { Router } from 'react-router'; class App extends React.Component { static propTypes = { history: PropTypes.object.isRequired } // class getter get content() { return ( ) } render() { return (

{this.content} ) } }

We’re almost ready to place our routes on the page, we just have to pass in our custom routes (we’ll make them shortly). We’ll wrap our routes into this component: import React, { PropTypes } from 'react'; import { Router } from 'react-router'; class App extends React.Component { static propTypes = { routes: PropTypes.object.isRequired, history: PropTypes.object.isRequired } // class getter get content() { return () } render() { return ( {this.content} ) } }

In order to actually use our component, we’ll need to pass through the two props the component itself expects to receive when we render the component: history - we’ll import the browserHistory object from react router and pass this export directly. routes - we’ll send JSX that defines our routes Back in our src/app.js file, we’ll pass through the history directly as we import it. import React from 'react' import ReactDOM from 'react-dom' import 'font-awesome/css/font-awesome.css' import './app.css'

import {browserHistory} from 'react-router' import App from 'containers/App/App' const mountNode = document.querySelector('#root'); ReactDOM.render( , mountNode);

Lastly, we’ll need to build some routes. For the time being, let’s get some data in our browser. Let’s show a single route just to get a route showing up. We’ll revise this shortly. To build our routes, we need access to the: component component Our custom route components. The Router and Route component can be imported directly from react-router: import React from 'react' import ReactDOM from 'react-dom' import 'font-awesome/css/font-awesome.css' import './app.css' import {browserHistory, Router, Route} from 'react-router' import App from 'containers/App/App' const mountNode = document.querySelector('#root'); ReactDOM.render( , mountNode);

We can create our custom route by building a JSX instance of the routes using these two components: // ... import './app.css' import {browserHistory, Router, Route} from 'react-router' const routes = ( ) //...

Since we haven’t yet defined the Home component above, the previous example fails, so we can create a really simple to prove it is working: // ... import './app.css' import {browserHistory, Router, Route} from 'react-router' const Home = React.createClass({ render: function() { return (Hello world) } }) const routes = ( ) //...

Finally, we can pass the routes object into our instance of and refreshing our browser. Provided we haven’t made any major typos, we’ll see that our route has resolved to the root route and “Hello world” is rendered to the DOM.

We also see that our tests pass as the component now has a single component being rendered as a child.

Building real routes Up through this point, we’ve built our app using a demo routing scheme with a single route that doesn’t do very much. Let’s break out our routes to their own file both to keep our src/app.js clean and to separate concerns from the bootstrap script. Let’s create a src/routes.js file where we’ll export our routes and we can consume them from the src/app.js file. $ touch src/routes.js

In this src/routes.js file, let’s create and export a function to create and export the routes JSX instance rather. Let’s copy and remove the instances from the src/app.js file and into our new routes.js file. Moving the contents from src/routes.js should leave the routes file as: import React from 'react' import {browserHistory, Router, Route, Redirect} from 'react-router' const Home = React.createClass({ render: function() {

return (Hello world) } }) export const makeRoutes = () => ( ) export default makeRoutes

To use this in our src/app.js file, we can replace our routes definition and call the exported function makeRoutes: import React from 'react' import ReactDOM from 'react-dom' import 'font-awesome/css/font-awesome.css' import './app.css' import App from 'containers/App/App' import {browserHistory} from 'react-router' import makeRoutes from './routes' const routes = makeRoutes() const mountNode = document.querySelector('#root'); ReactDOM.render( , mountNode);

Let’s confirm our is still running as we expect by using npm run test. If we don’t make any typos, our app should still render in the browser.

Main page and nested routes With our routing set up, let’s move on to building our main view. This main view is designed to display our main map and the listing of restaurants. This is our main map page. Since we’ll be building a complex application, we like to separate our routes by themselves to be controlled by the component that will be using them. In other words, we’ll be building our main view with the idea that it will define it’s sub-routes as opposed to having one gigantic routing file, our nested components can define their own views.

Let’s make a new directory in our root src directory we’ll call views with a single directory in it with the name of the route we’ll be building. For lack of a better name: Main/: $ mkdir -p src/views/Main

In this Main/ directory, let’s create two files: routes.js - a file for the Main/ view to define it’s own routing Container.js - the file that defines the container of the route itself $ touch src/views/Main/{Container,routes}

To get things started, let’s add a single route for the Container in our src/views/Main/routes.js file. The routes.js file can simply contain a route definition object just as though it is a top level routes file. import React from 'react' import {Route} from 'react-router' import Container from './Container' export const makeMainRoutes = () => { return (

) } export default makeMainRoutes;

When we import this routes file into our main routes file, we’ll define some children elements, but for the time being, to confirm the set-up is working as we expect it, we’ll work with a simple route container element. The container element can be as simple as the following: // in src/views/Main/Container.js import React from 'react' export class Container extends React.Component { render() { return ( Hello from the container ) } } export default Container

With our containing element defined, let’s flip back to our src/routes.js file to include our new subroutes file. Since we exported a function, not an object, we’ll need to make sure we display the return value of the function rather than the function itself. Modifying our initial src/routes.js file, to both remove the Home container definition and import our new routes, our src/routes.js file can look more akin to the following: import React from 'react' import {browserHistory, Router, Route, Redirect} from 'react-router' import makeMainRoutes from './views/Main/routes' export const makeRoutes = () => { const main = makeMainRoutes(); return ( {main} ) } export default makeRoutes

Since we’re defining sub-routes in our application, we won’t need to touch the main routes.js file much for the rest of this application. We can follow the same steps to add a new top-level route.

Demo: Routing to Container Content Refreshing the browser, we’ll see our new content comes directly from the new container element.

Routing to Maps Before we jump too far ahead, let’s get our component on the page. In a previous article, we built a component from the ground-up, so we’ll be using this npm module to generate our map. Check out this in-depth article at {{ name }}. Let’s install this npm module called google-maps-react: $ npm install --save google-maps-react

In our component, we’ll place an invisible map on the page. The idea behind an invisible map component is that our google map will load the Google APIs, create a Google Map instance and will pass in on to our children components, but won’t be shown in the view. This is good

for cases where we want to use the Google API, but not necessarily need to show a map at the same time. Since we’ll be making a list of places using the Google Places API, we’ll place an invisible component on screen. Before we can use the component, we’ll need to grab a google API key. In our webpack set up, we’re using the WebpackDefinePlugin() to handle replacing variables in our source, so we can set our google key as a variable in our .env file and it will just work. For information on how to get a Google API key, check out the {{ name }} article. In our /.env file, let’s set the GAPI_KEY to our key: GAPI_KEY=abc123

To use our GAPI_KEY, we’ll reference it in our code surrounded by underscores (i.e.: __GAPI_KEY__). Before we can start using the component, we’ll need to wrap our component in the GoogleApiWrapper() higher-order component. This HOC gives us access to the lazily-loaded google api and pass through a google prop which references the object loaded by the google script. import React from 'react' import Map, {GoogleApiWrapper} from 'google-maps-react' export class Container extends React.Component { // ... } export default GoogleApiWrapper({ apiKey: __GAPI_KEY__ })(Container)

Now, when we load the component on the page, the wrapper takes care of loading the google api along with our apiKey.

With the google API loaded, we can place a component in our component and it will just work. Let’s make sure by placing a instance in our component: The only requirement the component needs is the google prop, so we can add the component directly in our render code: import React from 'react' import Map, {GoogleApiWrapper} from 'google-maps-react' export class Container extends React.Component { render() { return ( Hello from the container ) } }

Demo: Routing to a Map With the google map displaying on our page, we can load up and start using the google maps service.

Getting a List of Places With the hard work out of the way (displaying the map), let’s get to displaying a list of places using the google api. When the loads in the browser, it will call the prop function onReady() if it’s passed in. We’ll use the onReady() function to trigger a call to the google places API using the google script. Let’s modify our src/views/Main/Container.js file to define an onReady() function we can pass as a prop: export class Container extends React.Component { onReady(mapProps, map) { // When the map is ready and mounted } render() { return ( ) } }

From here, we can use the google API as though we aren’t using anything special. We’ll create a helper function to run the google api command. Let’s create a new file in our src/utils directory called googleApiHelpers.js. We can nest all our Google API functions in here to keep them in a common place. We can return a promise from our function so we can use it regardless of the location: export function searchNearby(google, map, request) { return new Promise((resolve, reject) => { const service = new google.maps.places.PlacesService(map); service.nearbySearch(request, (results, status, pagination) => { if (status == google.maps.places.PlacesServiceStatus.OK) { resolve(results, pagination); } else { reject(results, status); } }) }); }

Now, within our container we can call this helper along with the maps and store the return from the google request within the onReady() prop function for our component. import {searchNearby} from 'utils/googleApiHelpers' export class Container extends React.Component { onReady(mapProps, map) { const {google} = this.props; const opts = { location: map.center, radius: '500', types: ['cafe'] } searchNearby(google, map, opts) .then((results, pagination) => { // We got some results and a pagination object }).catch((status, result) => { // There was an error }) } render() {

return ( ) } }

Since we’ll be storing a new state of the so we can stave the new results in our , let’s set it to be stateful: export class Container extends React.Component { constructor(props) { super(props); this.state = { places: [], pagination: null } } // ...

Now, when we fetch successful results, we can instead set some state on the local to hold on to the results fetched from Google. Updating our onReady() function with setState: export class Container extends React.Component { onReady(mapProps, map) { const {google} = this.props; const opts = { location: map.center, radius: '500', types: ['cafe'] } searchNearby(google, map, opts) .then((results, pagination) => { this.setState({ places: results, pagination }) }).catch((status, result) => { // There was an error }) } // ... }

Now, we can update our render() method by listing the places fetch we now have in our state: export class Container extends React.Component { // ... render() { return ( Hello from the container {this.state.places.map(place => { return ({place.name}) })} ) } }

Demo: A List of Places

Creating a Sidebar With our listing of places in-hand, let’s move on to actually turning our app into a closer to yelp-like component. In this section, we’re going to use turn our app into something that looks a little stylish and add some polish. In order to build this part of the app, we’re going to add some inline styling and use the natural React props flow. First, let’s install an npm module called classnames. The README.md is a fantastic resource for understanding how it works and how to use it. We’re going to use it to combine classes together (this is an optional library), but useful, regardless. $ npm install --save classnames

Now, let’s get to breaking out our app into components. First, let’s build a component to wrap around our app. As we’re building a shared component (rather than one specific to one view), a natural place to build the component would be the the src/components/Header directory. Let’s create this directory and create the JS files that contain the component and tests: $ mkdir src/components/Header $ touch src/components/Header/{Header.js,Header.spec.js}

Our component can be pretty simple. All we’ll use it for is to wrap the name of our app and possibly contain a menu (although we won’t build this here). As we’re building our test-first app, let’s write the spec that reflects the purpose first: In the src/components/Header.spec.js file, let’s create the specs: import React from 'react' import { expect } from 'chai' import { shallow } from 'enzyme' import Header from './Header' describe('', () => { let wrapper; beforeEach(() => { wrapper = shallow() }); // These show up as pending tests it('contains a title component with yelp'); it('contains a section menu with the title'); })

The tests themselves are pretty simple. We’ll simply expect for the text we expect: import React from 'react' import { expect } from 'chai' import { shallow } from 'enzyme' import Header from './Header' describe('', () => { let wrapper; beforeEach(() => { wrapper = shallow() }); it('contains a title component with yelp', () => { expect(wrapper.find('h1').first().text()) .to.equal('Yelp') }); it('contains a section menu with the title', () => { expect(wrapper.find('section').first().text()) .to.equal('Fullstack.io') }); })

Running our tests at this point will obviously fail because we haven’t yet written the code to make them pass. Let’s get our tests going green. Since the full component itself is pretty straightforward, nearly the entire implementation (without styles) can be summed to: import React from 'react' import {Link} from 'react-router' export class Header extends React.Component { render() { return ( Yelp Fullstack.io ) } } export default Header

Running the tests will now pass:

But loading the browser, the styles don’t look quite right yet… Let’s see it in the browser by making sure we import our new component into our main view and using in the render() function of our main component. In src/views/Main/Container.js: // our webpack alias allows us to reference `components` // relatively to the src/ directory import Header from 'components/Header' // ... export class Container extends React.Component { // ... render() { return ( {/* ... */} ) } }

Inlining Styles

Adding styles to our component is pretty straightforward. Without CSS modules, we can create a specific CSS identifier, include a global CSS stylesheet, and applying it to the component. Since we’re using CSS modules, let’s create a CSS module to handle the header bar component. We can create a CSS module by naming a CSS stylesheet with the suffix: .module.css (based upon our webpack configuration). Let’s create a stylesheet in the same directory as our Header.js: $ touch src/components/Header/styles.module.css

Let’s create a single class we’ll call topbar. Let’s add a border around it so we can see the styles being applied in the browser: /* In `src/components/Header/styles.module.css `*/ .topbar { border: 1px solid red; }

We can use this style by importing it into our component and applying the specific classname from the styles: import React from 'react' import {Link} from 'react-router' import styles from './styles.module.css'; export class Header extends React.Component { render() { return ( {/* ... */} ) } }

By setting the styles.topbar class to the component, the styles will be applied to the component. With our border in the styles.topbar class, we’ll see we have a red border around our topbar:

Let’s go ahead and add a fixed style to make it look a little nicer: /* In `src/components/Header/styles.module.css `*/ .topbar { position: fixed; z-index: 10; top: 0; left: 0; background: #48b5e9; width: 100%; padding: 0 25px; height: 80px; line-height: 80px; color: #fff; a { text-transform: uppercase; text-decoration: none; letter-spacing: 1px; line-height: 40px; h1 { font-size: 28px; } } section { position: absolute; top: 0px;

right: 25px; } }

Let’s also add a few styles to the main app.css (some global styles to change the layout style and remove global padding). Back in src/app.css, let’s add a few styles to help make our top bar look decent: *, *:after, *:before { box-sizing: border-box; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-smoothing: antialiased; text-rendering: optimizeLegibility; font-size: 16px; } body { color: #333333; font-weight: lighter; font: 400 15px/22px 'Open Sans', 'Helvetica Neue', Sans-serif; font-smoothing: antialiased; padding: 0; margin: 0; }

At this point, we can say that our header looks decent. However, with the way that we have it styled now, the content runs through the topbar. We can fix this by adding some styles to our content. Let’s create a content css modules in our src/views/Main/styles.module.css directory that our component will use. $ touch src/views/Main/styles.module.css

In here, we can wrap our entire container with a class and add a content class, etc. In our new styles.module.css class, let’s add the content classes: .wrapper { overflow-y: scroll; display: flex; margin: 0; padding: 15px; height: 100vh; -webkit-box-orient: horizontal; -o-box-orient: horizontal; } .content { position: relative;

flex: 2; top: 80px; }

Setting flex: 2 sets our content box as the larger of the two elements (sidebar vs content). We’ll come back and look at this in more depth at the end of this article. In the same fashion as we did before, we can now import these container styles with the component. In our component, let’s add a few styles around the elements on the page: import styles from './styles.module.css' export class Container extends React.Component { // ... render() { return ( {/* contents */} ) } }

CSS Variables Notice above how we have some hardcoded values, such as the height in our topbar class. /* In `src/components/Header/styles.module.css `*/ .topbar { /* ... */ background: #48b5e9; height: 80px; line-height: 80px; /* ... */ }

One of the valuable parts of using postcss with the postcss pipeline we’ve set up is that we can use variables in our css. There are a few different ways to handle using variables in CSS, but we’ll use the method that is built into postcss syntax. To add a custom property (variable), we can prefix the variable name with two dashes (-) inside of a rule. For instance: .topbar { --height: 80px; }

Note, when creating a custom property (which in our case is a variable), we need to place it inside a CSS rule. We can then use the --height property/variable in our css by using the var() notation. In our case, we can then replace the height properties in our topbar class: /* In `src/components/Header/styles.module.css `*/ .topbar { --height: 80px; /* ... */ background: #48b5e9; height: var(--height); line-height: var(--height); /* ... */ }

Although this is all well and good, we’re limited to using this --height variable to the .topbar class at this point. Instead, if we want to use this height in say, the content class, we’ll need to put the variable in a higher DOM selector. We could place the variable in the wrapper class, for instance, but postcss has a more clever way of handling variable notation. We can place the variable declaration at the root node of the CSS using the :root selector: /* In `src/components/Header/styles.module.css `*/ :root { --height: 80px; } .topbar { /* ... */ height: var(--height); line-height: var(--height); /* ... */ }

Since we’ll likely want to use this within another css module, we can place this variable declaration in another common css file that both modules can import. Let’s create a root directory for our CSS variables to live inside, such as src/styles/base.css: $ touch src/styles/base.css

Let’s move our :root declaration into this base.css file: :root { --topbar-height: 80px;

}

To use this variable in our multiple css modules, we’ll import it at the top of our CSS module and use it as though it has been declared at the top of the local file. Back in our src/components/Header/styles.module.css, we can replace the :root definition with the import line: @import url("../../styles/base.css"); /* ... */ .topbar { /* ... */ height: var(--height); line-height: var(--height); /* ... */ }

Now, we have one place to change the height of the topbar and still have the variable cascade across our app.

Splitting Up Components With our topbar built, we can move to building the routes of our main application. Our app contains a sidebar and a changable content area that changes views upon changing context. That is, we’ll show a map to start out with and when our user clicks on a pin, we’ll change this to show details about the location the user has clicked on. Let’s get started with the sidebar component that shows a listing of the places. We’ve done the hardwork up through this point. The React Way dictates that we define a component that uses a components to display the view (components all the way down – weeee). What this literally entails is that we’ll build a component that lists individual listing items. This way we can focus on the detail for each item listing rather than needing to worry about it from a high-level. Let’s take our listing from a simple list to a component. Since we’ll create the component as a shared one, let’s add it in the src/components/Sidebar directory. We’ll also create a JavaScript file and the CSS module: $ mkdir src/components/Sidebar $ touch src/components/Sidebar/{Sidebar.js,styles.module.css}

The component will be a fairly simple component that we can use to contain the listing of places and a wrapper for a sticky sidebar. Let’s place it in our view initially, so we can get it listed in the view. In our src/views/Main/Container.js file, let’s import this new

component (yet to be built). Let’s import it and place it in our render() function: import Sidebar from 'components/Sidebar/Sidebar' export class Container extends React.Component { // ... render() { return ( {/* contents */} ) } }

We’ll also want to pass a few props into the component. Since it is responsible for listing our places, we’ll definitely need to pass in the list of places we fetch from the google api. As of now, it’s stored in the state of the . We’ll come back to passing through a click handler (that gets called when a list item is clicked). Modifying our implementation, let’s pass through these props: import Sidebar from 'components/Sidebar/Sidebar' export class Container extends React.Component { // ... render() { return ( {/* contents */} ) } }

With our component in the , let’s start building our . The component will mostly be used as a container to hold styles and dynamic data. import React, { PropTypes as T } from 'react' import styles from './styles.module.css' export class Sidebar extends React.Component { render() { return ( {this.props.title} ) } } export default Sidebar

Let’s also make sure we add the appropriate top positioning such that it isn’t rendered underneath the topbar. Let’s create the styles in src/components/Sidebar/styles.module.css and add some styles: @import url("../../styles/base.css"); .sidebar { height: 100%; top: var(--topbar-height); left: 0; overflow: hidden; position: relative; flex: 1; z-index: 0; .heading { flex: 1; background: #fff; border-bottom: 1px solid #eee; padding: 0 10px; h1 { font-size: 1.8em; } } }

Our component so far is straightforward and currently only renders the title prop we passed it above. Currently, we don’t have much interesting going on with our component. Let’s pass through our details to a new component. We could display the listing directly inside the component, but this would not be the React Way. Let’s create a component to list each place. We’ll use an additional component called to handle listing each individual item. That being said, let’s build the component. We’ll make the components, a css module, and the tests to go along with it: $ mkdir src/components/Listing $ touch src/components/Listing/{Listing.js,Item.js,styles.module.css} $ touch src/components/Listing/{Listing.spec.js,Item.spec.js}

Since we’re doing some test driven development, let’s get our Listing.spec.js updated. The assumptions we’ll make about our component is that it has the styles we expect and that it lists an component for each place in our places prop. The test can be stubbed as follows: import React from 'react' import { expect } from 'chai' import { shallow } from 'enzyme'

import Listing from './Listing' import styles from './styles.module.css' describe('', () => { let wrapper; const places = [{ name: 'Chicago' }, { name: "San Francisco" }]; beforeEach(() => { wrapper = shallow() }); it('wraps the component in a listing css class') it('has an item for each place in the places prop') })

Let’s go ahead and fill out these tests. It’s okay that they will ultimately fail when we run them at first (we haven’t implemented either the or the components yet). The final tests will look like: import React from 'react' import { expect } from 'chai' import { shallow } from 'enzyme' import Listing from './Listing' import styles from './styles.module.css' describe('', () => { let wrapper; const places = [{ name: 'Chicago' }, { name: "San Francisco" }]; beforeEach(() => { wrapper = shallow() }); it('wraps the component in a listing css class', () => { expect(wrapper.find(`.${styles.container}`)) .to.be.defined; }) it('has an item for each place in the places prop', () => { expect(wrapper.find('Item').length) .to.equal(places.length);

}) })

Business Listing Component With the component tests written, let’s turn our sights on implementing the component. Similar to the component, the component serves basically as a wrapper around the component with styles. At it’s core, we simply need a wrapper component around a listing of components. Our entire component will be fairly short and simple. import React, { PropTypes as T } from 'react' import classnames from 'classnames' import Item from './Item'; import styles from './styles.module.css' export class Listing extends React.Component { render() { return ( {this.props.places.map(place => { return ( ) })} ) } } export default Listing

Let’s create the container style inside the css module for the component. In our src/components/Listing/styles.module.css, let’s make sure we have a CSS container that removes some basic styling. .container { height: 100%; overflow: auto; padding-bottom: 60px; margin: 0; }

Before we can depend upon the view changing with our updated component, we’ll need to write up our component.

Item Component The expectations we’ll make with our component will be that it shows the name of the place, that it is styled with the appropriate class, and that it shows a rating (given by the Google API). In our src/components/Listing/Item.spec.js, let’s stub out these assumptions as a Jasmine test: import React from 'react' import { expect } from 'chai' import { shallow } from 'enzyme' import Item from './Item' import styles from './styles.module.css' describe('', () => { let wrapper; const place = { name: 'San Francisco' } beforeEach(() => { wrapper = shallow() }); it('contains a title component with yelp') it('wraps the component with an .item css class') it('contains a rating') })

Let’s fill up these tests. The tests themselves are straightforward. // ... it('contains a title component with yelp', () => { expect(wrapper.find('h1').first().text()) .to.equal(place.name) }); it('wraps the component with an .item css class', () => { expect(wrapper.find(`.${styles.item}`)) .to.have.length(1); }) it('contains a rating', () => { expect(wrapper.find('Rating')) .to.be.defined; });

It may be obvious at this point, but we’ll create another component we’ll call which will encapsulate the ratings on a place given back to us through the Google API. For the time being, let’s turn these tests green. In our src/components/Listing/Item.js. Our component won’t need to worry about fetching a place or dealing with the API, since we’ll be passing through the place object when we mount it. We’ll display the name of the place along with it’s rating. Before we get to building our component, let’s build up our component to display the name and it’s rating by number. import React, { PropTypes as T } from 'react' import classnames from 'classnames' import Rating from 'components/Rating/Rating'; import styles from './styles.module.css' export class Item extends React.Component { render() { const {place} = this.props; return ( {place.name} {place.rating/5} ) } } export default Item

In order to style the component, let’s create the .item class in the css module at src/components/Listing/styles.module.css. We’ll use flexbox to design our listing which contains the two elements: /* ... */ .item { display: flex; flex-direction: row; border-bottom: 1px solid #eeeeee; padding: 10px; text-decoration: none; h1 { flex: 2; &:hover { color: $highlight; } } .rating { text-align: right; flex: 1; } &:last-child { border-bottom: none;

} }

Now, back in our view, we’ll see that the ratings are starting to take shape:

Rating Component Let’s get the ratings looking a little nicer. We’ll use stars to handle the rating numbering (rather than the ugly decimal points – ew). Since we’re going to make a component, let’s create it in the src/components directory. We’ll create the module, the css module, and the tests: $ mkdir src/components/Rating $ touch src/components/Rating/{Rating.js,styles.module.css} $ touch src/components/Rating/Rating.spec.js

The assumptions we’ll make with the component is that we will have two layers of CSS elements and that the first one will fill up the stars at a percentage of the rating. With these expectations, the test skeleton will look akin to: import React from 'react' import {expect} from 'chai' import {shallow} from 'enzyme'

import Rating from './Rating' import styles from './styles.module.css'; describe('', function () { it('fills the percentage as style'); it('renders bottom and top star meters') });

Let’s fill up these tests. We’ll shallow-mount each of the components in each test and make sure that the CSS style that’s attached matches our expectations: // ... it('fills the percentage as style', () => { let wrapper = shallow() expect(wrapper.find(`.${styles.top}`)) .to.have.style('width', '10%'); wrapper = shallow() expect(wrapper.find(`.${styles.top}`)) .to.have.style('width', '99%') }); it('renders bottom and top star meters', () => { let wrapper = shallow() expect(wrapper.find(`.${styles.top}`)).to.be.present; expect(wrapper.find(`.${styles.bottom}`)).to.be.present; })

Our tests will fail while we haven’t actually implemented the component. Let’s go ahead and turn our tests green. The component is straightforward in that we’ll have two levels of Rating icons with some style splashed on to display the stars. In our src/components/Rating/Rating.js file, let’s create a stateless component along with our component: import React, { PropTypes as T } from 'react' import styles from './styles.module.css'; const RatingIcon = (props) => (★) export class Rating extends React.Component { render() { // ... } } export default Rating

The can be a stateless component as its output is not dependent upon the props. It simply shows a star (*). We can use this component inside our component. We’ll use inline style to set the width of the top colored rating icons. In our src/components/Rating/Rating.js, let’s update the component to use the component: export class Rating extends React.Component { render() { const {percentage} = this.props; const style = { width: `${(percentage || 0) * 100}%` } return ( ) } }

Without any style, the component doesn’t look quite like a rating.

Let’s fix the styling to separate the top and the bottom part of the components. .sprite { unicode-bidi: bidi-override; color: #404040; font-size: 25px; height: 25px; width: 100px; margin: 0 auto; position: relative; padding: 0; text-shadow: 0px 1px 0 $light-gray; } .top { color: #48b5e9; padding: 0; position: absolute; z-index: 1; display: block; top: 0; left: 0; overflow: hidden; } .bottom {

padding: 0; display: block; z-index: 0; color: #a2a2a2; }

Demo: Rating Stars Before we move on, let’s make sure we have our colors as a variable as well. This way, we can ensure our colors across our app are consistent. Let’s create a src/styles/colors.css file to keep our colors as variables. As we did previously with our base.css, let’s keep these styles in the :root component. In src/styles/colors.css, let’s set our colors: :root { --dark: #404040; --light-gray: $a2a2a2; --white: #ffffff; --highlight: #48b5e9; --heading-color: var(--highlight); }

With these colors set, let’s convert our component’s css module to use them (remembering to import the src/styles/colors.css file): @import "../../styles/colors.css"; .sprite { unicode-bidi: bidi-override; color: var(--dark); font-size: 25px; height: 25px; width: 100px; margin: 0 auto; position: relative; padding: 0; text-shadow: 0px 1px 0 var(--light-gray); } .top { color: var(--highlight); padding: 0; position: absolute; z-index: 1; display: block; top: 0; left: 0;

overflow: hidden; } .bottom { padding: 0; display: block; z-index: 0; color: var(--light-gray); }

Now our with our component is complete.

Demo: Sidebar with Listings

Building the Main Pane Now that we have our implemented, let’s build up our main elements. That is, let’s show our map and details. Since we’ll want these pages to be linkable, that is we want our users to be able to copy and paste the URL and see the same URL as they expect, we’ll set the main content to be defined by it’s URL.

In order to set these elements up to be handled by their routes, we’ll need to change our routes. Currently, our routes are set by a single route that shows the component. Let’s modify the route to show both a Map component and details. Our src/views/routes.js file currently only contains a single component. As a reminder, it currently looks like: import React from 'react' import {Route} from 'react-router' import Container from './Container' export const makeMainRoutes = () => { return ( ) } export default makeMainRoutes;

Let’s modify the makeMainRoutes() function to set the component as a container (surprise) for the main routes. export const makeMainRoutes = () => { return ( {/* child routes in here */} ) }

Now, let’s build the container we will surface in this area. We’ll build this component in the src/views/Main/Map directory, so let’s import the exported component in our makeMainRoutes() file and list it as a child: import Map from './Map/Map' export const makeMainRoutes = () => { return ( ) }

Loading our new routes in the browser will show nothing until we navigate to the /map route AND we build our component. For the time being, let’s create a simple component to show in the Map area to confirm the route is working. As we’ve done a few times already, let’s create a JS file and the css module file in the src/views/Main/Map directory: $ mkdir src/views/Main/Map $ touch src/views/Main/Map/{styles.module.css,Map.js}

A really simple default component with some dummy text is pretty simple to create import React, { PropTypes as T } from 'react' import classnames from 'classnames' import styles from './styles.module.css' export class MapComponent extends React.Component { render() { return ( MAP! ) } } export default MapComponent

Heading back to the browser, we’ll see that… wait, it’s blank? Why? We haven’t told the component how or where to render it’s child routes. Before we can go much further, we’ll need to share our expectations with the component. The React Way to handle this is by using the children prop of a component. The this.props.children prop is handed to the React component when it mounts for any nodes that are rendered as a child of a React component. We’ll use the children prop to pass forward our child routes to be rendered within the container. To use the children prop, let’s modify the component in src/views/Main/Container.js to pass them down inside the content block. export class Container extends React.Component { // ... render() { return ( {/* Setting children routes to be rendered*/} {this.props.children} ) } }

Now, when we load the view in the browser we’ll see that the route for /map inside the content block.

Since we are already loading the container in the outer component, we will already have a handle to the google reference as well as a reference to a created map instance. We can pass the reference down as a prop to the child elements by cloning children and creating a new instance to handle passing down data. React makes this process easy to handle by using the React.cloneElement() function. In our render() method of the component, let’s get a handle to the children props outside of the component:

export class Container extends React.Component { // ... render() { let children = null; if (this.props.children) { // We have children in the Container component children = this.props.children; } return ( {/* shortened for simplicity */} {/* Setting children routes to be rendered*/} {children} ) } }

With a handle to the children props, we can create a clone of them passing the new props down to the children, for instance: export class Container extends React.Component { // ... render() { let children = null; if (this.props.children) { // We have children in the Container component children = React.cloneElement( this.props.children, { google: this.props.google }); } return ( {/* shortened for simplicity */} {/* Setting children routes to be rendered*/} {children} ) } }

Now, the children of the component will receive the google prop. Of course, we also will have to pass the places from the state of the component. We can pass any other data we want to send off within the React.cloneElement() function. We’ll pass as much data down as we need to here, which we’ll take advantage of a bit later.

export class Container extends React.Component { // ... render() { let children = null; if (this.props.children) { // We have children in the Container component children = React.cloneElement( this.props.children, { google: this.props.google, places: this.state.places, loaded: this.props.loaded }); } return ( {/* shortened for simplicity */} {/* Setting children routes to be rendered*/} {children} ) } }

Now, we can update our component (in src/views/Main/Map/Map.js) to show an actual from our GoogleApiComponent() container using the google props: import React, { PropTypes as T } from 'react' import classnames from 'classnames' import Map from 'google-maps-react' import styles from './styles.module.css' export class MapComponent extends React.Component { render() { return ( ) } } export default MapComponent

The here will work as expected as we are already creating the google component (in fact, we can also pass the map prop down and the GoogleApiComponent npm module will handle it correctly without creating a new map instance).

Creating Markers On Our Map Let’s get some on our component. The GoogleApiComponent HOC decorates it’s children elements with the appropriate props passed by the component (provided it’s the parent component). That is, we can render components from inside the component as children. In our component, we can use a similar process to the element to render the child props. Since we’ll want to display a for each place, we will iterate through the this.props.places array and instantiate a new instance for each. Let’s create the instances using a method we’ll handle the children rendering using a helper function: export class MapComponent extends React.Component { renderMarkers() { } render() { return ( {this.renderMarkers()} ) } }

To create the Marker, let’s import the component from the google-maps-react npm module: import Map, { Marker } from 'google-maps-react'

Now, inside the renderMarkers() function we can iterate over the this.props.places prop and create a instance: export class MapComponent extends React.Component { renderMarkers() { return this.props.places.map(place =>{ return }) } // ... }

Loading this in the browser, we’ll see that our this.props.places is null and this method will throw an error (there are multiple ways to handle this, we’ll use a simple check). We can avoid this check by returning null if there are no places at the beginning of the function: export class MapComponent extends React.Component { renderMarkers() { if (!this.props.places) { return null; } return this.props.places.map(place =>{ return }) } // ... }

Now, we have our component showing markers for each of the places in the map. With the component set up, let’s move on to handling settling the screen that shows more details about each place after we click on the marker that corresponds to the place.

Clicking on Markers For each of the components, we can listen for onClick events and run a function when it’s clicked. We can use this functionality to route the user to a new path that is designed to show details specifically about a single place from the Google API. Since we’re handling most of the business logic in the and we are mostly using the as a non-stateful component, we’ll put this logic inside the containing component, as opposed to the . Containing the entire routing logic in the component is a simple way to keep the business logic of the application in a single spot and not clutter up other components. Plus, it makes testing the components way simpler. Just like we can pass through data in the props of a component, we can pass down function references as well. Let’s take the call to React.cloneElement() and pass through a reference to a function that will get called when one is clicked. Let’s call it onMarkerClick() (obvious name for the function, eh?). Let’s open up our element in src/views/Main/Container.js and add the function as well as the prop to the children: export class Container extends React.Component { // ... onMarkerClick(item) { } render() { let children = null; if (this.props.children) { // We have children in the Container component children = React.cloneElement( this.props.children, { google: this.props.google, places: this.state.places, loaded: this.props.loaded, onMarkerClick: this.onMarkerClick.bind(this) }); } // ... }

}

To use our new onMarkerClick prop, we can pass it through to the onClick() method in the component. The component accepts a click handler through the onClick prop. We can pass through the prop directly to the component. Back in the (at src/views/Main/Map/Map.js), let’s update the component with the onClick prop to call back to our onMarkerClick prop (that is a mouthful): export class MapComponent extends React.Component { renderMarkers() { return this.props.places.map(place =>{ return }) } // ... }

Now, when a is clicked, it will call our component’s onMarkerClick() function. The callback it executes is called with the instance of the component, along with the map and google instance. We’ll only need to work with the instance as we have all the information passed through in props. Since we pass the place object in the component, we can reference the entire place object in the callback. export class Container extends React.Component { // ... onMarkerClick(item) { const {place} = item; // place prop } // ... }

When our user clicks on the marker, we’ll want to send them to a different route, the details route. To define this, we’ll use the router in the component’s context to push the user’s browser to another route. In order to get access to the context of the component, we’ll need to define the contextTypes. We’ll be using a single context in our contextTypes. At the end of the src/views/Main/Container.js file, let’s set the router to the type of object:

export class Container extends React.Component { // ... } Container.contextTypes = { router: React.PropTypes.object }

Now, inside the onMarkerClick() function we can get access to the push() method from the router context and call it with the destination route: export class Container extends React.Component { // ... onMarkerClick(item) { const {place} = item; // place prop const {push} = this.context.router; push(`/map/detail/${place.place_id}`) } // ... }

Now, clicking on a marker will push us to a new route, the /map/detail/${place.place_id} route.

Adding More Details to Markers Let’s create the /detail/:placeId route. When our user navigates to a URL like /detail/abc123, instead of /map, we’ll want to show them the details for the place with the id of abc123. This scheme allows us to copy and paste the URL so the user can share it around and expect to see the same result every single time. To create the new route, let’s modify the routes.js file so that we have this route. In our src/views/Main/routes.js file, let’s add: export const makeMainRoutes = () => { return ( ) }

React router takes the final part of the URL and treats it as a variable route. That is to say if the user visits a url such as: /detail/abc123, it will match our new route and the :placeId will be passed through as props to our Detail component as params. We’ll come back to this in a moment. Let’s create the Detail component and it’s css module (just like we did previously for the component): $ mkdir src/views/Main/Detail $ touch src/views/Main/Detail/{styles.module.css,Detail.js}

The component is a single component that is responsible for showing the data associated with a place. In order to handle finding more details about the place, we can call another Google API that directly gives us details about one specific place. Let’s create this API handler in our src/utils/googleApiHelpers.js (as we have done with the nearbySearch()). Let’s add the following request at the end of the file: export function getDetails(google, map, placeId) { return new Promise((resolve, reject) => { const service = new google.maps.places.PlacesService(map); const request = {placeId} service.getDetails(request, (place, status) => { if (status !== google.maps.places.PlacesServiceStatus.OK) { return reject(status); } else { resolve(place); } }) }) }

Let’s create the component, making sure to import our new helper function (which we’ll use shortly). In the src/views/Main/Detail/Detail.js, let’s create the component: import React, { PropTypes as T } from 'react' import {getDetails} from 'utils/googleApiHelpers' import styles from './styles.module.css' export class Detail extends React.Component { constructor(props, context) { super(props, context) this.state = { loading: true, place: {}, location: {}

} } getDetails(map) {} // ... render() { return ( ) } } export default Detail

The component is a stateful component as we’ll need to hold on to the result of an API fetch to the getDetails() request. In the constructor, we’ve set the state to hold on to a few values, including the loading state of the request. We have to handle two cases for when the component mounts or updates in the view. 1. The first case is when the mounts initially, we’ll want to make a request to fetch more details about the place identified by the :placeId. 2. The second is when the map component updates or the placeId changes. In either case, we’ll need a common method for getting details. Let’s update our getDetails() method inside the component that we’ll call our helper method of the same name. First, let’s get the placeId from the URL (passed to our component through this.props.params): import {getDetails} from 'utils/googleApiHelpers' export class Detail extends React.Component { getDetails(map) { // the placeId comes from the URL, passed into // this component through params const {google, params} = this.props; const {placeId} = params; } // ... }

With the placeId, we can call our helper method and store the result from the returned promise: import {getDetails} from 'utils/googleApiHelpers' export class Detail extends React.Component { getDetails(map) { // the placeId comes from the URL, passed into

// this component through params const {google, params} = this.props; const {placeId} = params; // Set the loading state this.setState({loading: true}, () => { getDetails(google, map, placeId) .then(place => { const {location} = place.geometry; const loc = { lat: location.lat(), lng: location.lng() } this.setState({ place, location: loc, loading: false }); }) }); } // ... }

Although it looks like quite a bit, the method is straight-forward. We’re setting the state as loading (so we can show the loading state in the view) and then calling the method. When it comes back successfully, we’ll update the state with the place/location and update the loading state. We’re storing the location as a custom state object to standardize the location, rather than creating the object in the render() function. Back to the two states, the first one is easy to handle. When the componentDidMount(), we can check to see if we have a map available in the props (we won’t have a map prop if the has yet to load – it’s asynchronous, after all) and call our getDetails() method. In the src/views/Main/Detail/Detail.js, let’s add the componentDidMount() function: // ... export class Detail extends React.Component { componentDidMount() { if (this.props.map) { this.getDetails(this.props.map); } } getDetails(map) { // ... } }

The second case is relatively straight-forward as well. The component will update when the props update, which in our case might happen when the map is loading or the placeId changes in the URL. If either one of those are true, we’ll call through to our getDetails() method: // ... export class Detail extends React.Component { componentDidUpdate(prevProps) { if (this.props.map && // make sure we have a map (prevProps.map !== this.props.map || prevProps.params.placeId !== this.props.params.placeId)) { this.getDetails(this.props.map); } } getDetails(map) { // ... } }

With that, we’ll have the place details in the this.state of the component ready for rendering. If we’re in a loading state (i.e. this.state.loading is true), we’ll show the user that we’re loading the page. We’ll set this up simply in the render() function in our component: export class Detail extends React.Component { // ... render() { if (this.state.loading) { return ( Loading... ); } // We're no longer loading when we get here } }

Let’s show the place’s name, which we now have in the this.state.place: export class Detail extends React.Component { // ... render() { if (this.state.loading) { return ( Loading... ); } // We're no longer loading when we get here

const {place} = this.state; return ( {place.name} ) } }

Before we move on, let’s add a small bit of style to the component. Mostly for demonstration purposes as well as making our app responsive. Before we get there, let’s wrap our element in the header class so we can modify the style and it’s container: export class Detail extends React.Component { // ... render() { if (this.state.loading) { return ( Loading... ); } // We're no longer loading when we get here const {place} = this.state; return (

{place.name} ) } }

Back in our src/views/Main/Detail/styles.module.css, we can add the .header{} CSS class definition to give it some definition. Let’s increase the font-size and add some padding around the title to make it stand out more: .header { padding: 0 25px; h2 { font-size: 1.5em; } }

Remember, we can use the same padding across the entire app for consistency, so rather than hardcode the padding in the header, let’s use the globally defined variable --padding. First, we’ll need to import the base style and then use the var() syntax to apply the --padding: @import url("../../../styles/base.css"); .header { padding: 0 var(--padding); h2 { font-size: 1.5em; } }

Although this style addition isn’t incredibly impressive, we now have confirmed the css module is hooked up to our component. The Google Places API gives us back an interesting object with all sorts of fun goodies included, such as photos. Let’s get a photo panel showing the inside of the cafe (usually) that are handed back by API.

Demo: Explore Google Places JSON API Let’s add in the photos to the render() function of the component. The Google API doesn’t hand us back photo URLS, however. Instead, we’ll need to call out to Google to give us a URL to display these photos. Luckily (and well-designed), the Google JS SDK returns us a function we can call with the maxWidth and maxHeight defined inside of an object as an argument. We’ll use this to generate our URL. Rather than display the photos inline to handle the process, let’s nest the photo rendering in a function in the component (we’ll call it renderPhotos()): export class Detail extends React.Component { // ... renderPhotos(place) { } render() {

// ... const {place} = this.state; return ( {place.name} {this.renderPhotos(place)} ) } }

An alternate way of handling photo rendering is by creating a component to take care of loading the photos. However, we’ll nest them in the component to contain the display to a single component. Since some places don’t have photos, we’ll want to return back an empty virtual DOM node. Let’s check for this case: export class Detail extends React.Component { // ... renderPhotos(place) { if (!place.photos || place.photos.length == 0) return; // We have photos here } }

For each photo of the place’s photo array, we’ll pass back the generated URL (as we discussed above): export class Detail extends React.Component { // ... renderPhotos(place) { if (!place.photos || place.photos.length == 0) return; const cfg = {maxWidth: 100, maxHeight: 100} return ( {place.photos.map(p => { const url = `${p.getUrl(cfg)}.png` return () })} ) } }

Checking our browser, we now have a beautiful photo spread… well, almost beautiful.

Let’s add some style to our already classNamed photoStrip CSS class in our modules. First, let’s use flexbox to make our photoStrip be a single, horizontally-scrollable line of photos. In the same CSS file (src/views/Main/Detail/styles.module.css), let’s add the photoStrip CSS definition with flex: .details { /* a little bit of padding */ padding: 0 var(--padding); .photoStrip { flex: 1; display: flex; overflow: auto; } }

Annnnndddd let’s get rid of that awful scrollbar at the bottom of the photoStrip by adding the ::webkit-scrollbar CSS definition: .details { padding: 0 var(--padding); ::-webkit-scrollbar { width: 0px; background: transparent; } /* ... */ }

Finally, let’s add a small margin between the photos so we can see they are different photos instead of a single one: .details { /* a little bit of padding */ padding: 0 var(--padding); .photoStrip { flex: 1; display: flex; overflow: auto; img {

margin: 0 1px; } } }

Going Responsive Our app looks great at a medium-sized to larger screen, but what if our user is using a mobile device to view our yelp-competitor?

Let’s fix this right now. We will still want our list of other close-by restaurants showing, but perhaps the location makes sense below the details of the currently interesting one. It makes more sense for the list of restaurants to be below the details about a particular one or as a pull-out menu from the side.

To avoid adding extra JS work (and focus on the CSS responsiveness), let’s move the menu to below the detail of a specific location. In order to handle this, we’ll use some media queries. Definition of Media query from w3c schools: Media query is a CSS technique introduced in CSS3. It uses the @media rule to include a block of CSS properties only if a certain condition is true. We can use media queries to ask the browser to only display a CSS rule when a browser condition is true. For instance, we can use a media query to only set a font-size to 12px from 18px when we’re printing by using the sample CSS media query: body { font-size: 18px; } /* Media query for printing */ @media (print) { font-size: 12px; } /* ... */

In our postcss setup, we’re using the precss postcss (mouthful, right?) processor, which conveniently adds the postcss-custom-media plugin to our code. The postcss-custom-media plugin allows us to define media queries as variables and use these variables as the definitions for our media queries. In a mobile-first only world, it makes sense for us to write our CSS to look correct on mobile first and then add media queries to style larger screens. 1. Design/write mobile-friendly CSS (first) 2. Add styles for larger screen (second) With this in mind, let’s design our main screen to show the content block as the first visual component and the sidebar to come second. In order to set up our app to use the flexbox approach, we’ll need to look at 3 aspects of flexbox. To learn more about flexbox, what it is and how to use it, Chris Coyier has a fantastic article on using it. We’ll spend just enough time on it to setup our app.

display: flex To tell the browser we want a component to use the flexbox approach, we’ll need to add the display: flex rule to the parent component of an element. For us, this means we’ll set the display: flex; rule on the wrapper of the entire page (since we’re using flexbox on every element in our Main view). We set this in our wrapper class previously.

flex-direction The flex-direction rule tells the browser where to set the axis of the layout. We can either lay elements out horizontally (column) or vertically (row). By setting the flex-direction: column rule, the browser will lay elements one on top of the other. Since we want our page to lay out our elements horizontally for mobile, let’s set the flex-direction to column: /* In src/views/Main/styles.module.css */ .wrapper { overflow-y: scroll; display: flex; margin: 0; padding: 15px; height: 100vh; -webkit-box-orient: horizontal; -o-box-orient: horizontal; /* Added these rules */ flex-direction: column; flex: 1; } /* ... */

Refreshing our browser, we’ll see that our layout has completely switched from horizontal to vertical. Adding a flex: 1 to the content container balances out the sizing of the app in the mobile view. Now, if we expand the view back to desktop size, the vertical layout looks out of place and doesn’t quite work as well. Let’s add our first media query to fix this view on a larger screen. First, we like to define our media queries by themselves. We could add our custom media queries into the base.css file, but it can clutter the styles as it grows larger. Instead, let’s create a new file in src/styles/queries.css to contain all of our media query definitions. $ touch src/styles/queries.css

In this new file, we’ll use the @custom-media definition to define the screen for phones vs. those which are larger: @custom-media --screen-phone (width 35.5em);

Now, any screen that is relatively small can be targeted using the --screen-phone media query and any larger screen can be targeted using the --screen-phone-lg rule. Back in our main styles css module, let’s apply the media query to set the flex-direction back to column when we’re on a larger screen. /* In src/views/Main/styles.module.css */ .wrapper { overflow-y: scroll; display: flex; margin: 0; padding: 15px; height: 100vh; -webkit-box-orient: horizontal; -o-box-orient: horizontal; flex-direction: column; flex: 1; @media (--screen-phone-lg) { /* applied when the screen is larger */ flex-direction: column; } } /* ... */

Now, both the content block and the sidebar are set side-by-side, but are both the same size. We can fix the ordering using the last rule we’ll discuss in-depth here.

order The flexbox styling allows us to set the order of content blocks as well in CSS. Currently, our content block and the sidebar block are both set to be ordered first, which defaults to showing the content block that is defined first initially. To modify the content block so that it is defined first on mobile and last on desktop, we’ll apply the CSS rule of order. /* In src/views/Main/styles.module.css */ .wrapper { overflow-y: scroll; display: flex; margin: 0; padding: 15px; height: 100vh; -webkit-box-orient: horizontal; -o-box-orient: horizontal; flex-direction: column; flex: 1; @media (--screen-phone-lg) {

/* applied when the screen is larger */ flex-direction: column; } } .content { position: relative; top: var(--topbar-height); left: 0; flex: 1; order: 1; @media (--screen-phone-lg) { flex: 2; order: 2; } } /* ... */

In order to set the sidebar to play nicely with the rest of our content box, let’s apply the same principles of ordering and flex to the sidebar (except in reverse): @import url("../../styles/base.css"); @import url("../../styles/queries.css"); .sidebar { /* ... */ flex-direction: column; order: 2; flex: 1; @media (--screen-phone-lg) { display: block; flex-direction: row; order: 1; } .heading { /* ... */ } }

Back in the browser, refreshing the page, we’ll see that the layout looks even at either mobile or larger.

Making the Map Page the Index

One final note before we end our app, when our user visits the page for the first time, they’ll end up at a blank page. This is because we haven’t defined an index route. We can handle an index route in a few different ways. We’ll set our Map component as the index route using react router’s component. Back in src/views/Main/routes.js, let’s set the index route: import {Route, IndexRoute} from 'react-router' // ... export const makeMainRoutes = () => { return ( ) }

Refreshing the page and navigating to the root at http://localhost:3000, we’ll see we no longer have a blank page, but the main route of our app with the map and sidebar showing pages.

Conclusion We walked through a complete app with a large number of routes, complex interactions, concepts, and even added responsiveness to the app. We’ve placed the entire app on github at fullstackreact/yelp-clone. If you’re stuck, have further questions, feel free to reach out to us by: Commenting on this post at the end of the article Emailing us at [email protected] Filling an issue on the Github repo Tweet at us at @fullstackreact

View more...

Comments

Copyright ©2017 KUPDF Inc.
SUPPORT KUPDF