/Pokedex

pokedex made with Flux/ReactJS

Primary LanguageRuby

Pokedex: An Introduction to the React Router

Gotta Fetch 'em All

In this project, we'll write an app to manage your Pokemon and their Toys. We've already setup migrations/models/controllers/views for you to start with in a skeleton that we will email to you at the beginning of the day. Set things up with a bundle install, then rake db:setup (this is equivalent to rake db:create db:migrate db:seed).

Take a look at the schema, the routes file, and the jbuilder views to get yourself oriented. Navigate to the api routes to see the json that's sent up.

Note the defaults: {format: :json}. This means that HTTP requests that Rails handles for the pokemon resource should be assumed to be asking for a JSON response instead of HTML. When we render a template, instead of looking for template.html.erb, Rails will look for template.json.jbuilder.

Also: the root url localhost:3000 will be the home of our JS application. We have provided this controller and view for you.

Phase 1: Flux Structure

Update your Gemfile as you did yesterday:

gem 'react-rails', '1.3.0'
gem 'flux-rails-assets'

Structure your app's assets/javascripts directory. You should have actions, components, constants, dispatcher, stores, and util folders as before. Also create a pokedex.js.jsx file.

Add to your application.js:

//= require flux
//= require eventemitter
//= require react

Finally, create the dispatcher file.

Phase 2: Pokemon Index

ApiUtil

We'd like to render a list of pokemon. Let's start by setting up a way to fetch them from the back end. Make an api_util.js file inside your util folder. Inside this file, we'll make ajax requests that fetch information served by our rails controllers, and on success call a front end action creator.

Create a window.ApiUtil object. Give it a fetchAllPokemons attribute that is a function. The function should make an ajax request with url api/pokemon and a success callback. The success callback will be passed the fetched pokemons. For now, print the pokemons to the console and test that everything is working.

Once you can print the pokemons, change the success callback to instead pass them to ApiActions.receiveAllPokemons, which we have yet to write. receiveAllPokemons will dispatch actions to our stores.

ApiActions and PokemonConstants

Now let's write that action dispatcher. Create a file api_actions.js in the actions folder. In a window.ApiActions.receiveAllPokemons function, call AppDispatcher.dispatch and pass it an object with a property actionType whose value is PokemonConstants.POKEMONS_RECEIVED, and a property pokemons that passes along the argument to the function.

In the constants folder, create a pokemon_constants.js file that defines window.PokemonConstants.POKEMONS_RECEIVED to be the string "POKEMONS_RECEIVED".

PokemonStore

We need a way to keep track of the pokemons on the front end. In stores/pokemon.js, create window.PokemonStore. Remember to use EventEmitter.prototype. The file should have a local variable _pokemons that's initially set to an empty array. The all function of the pokemon store should return a copy of _pokemons. In the file, we also want a resetPokemons function that sets _pokemons equal to its argument. Good. Now we're able to keep track of the pokemons that we've fetched.

We want to call resetPokemons when PokemonConstants.POKEMONS_RECEIVED is dispatched. Make it so.

Check that calling ApiUtil.fetchAllPokemons and PokemonStore.all in the browser works as expected.

React Components

PokemonsIndex

Make a react component window.PokemonsIndex in components/pokemons/index.js.jsx to display the pokemons we've fetched. The state of PokemonsIndex should keep track of all the pokemons in the store. getInitialState will start us out right, but we also need to set the state whenever the store changes.

To do the latter we need to add a change listener to our store. On the store, write a function addPokemonsIndexChangeListener that takes a callback. It should call this.on and pass it POKEMONS_INDEX_CHANGE_EVENT and the callback. Add the variable POKEMONS_INDEX_SHANGE_EVENT to the file containing the store. You can give it whatever value you'd like.

PokemonStore should emit a POKEMONS_INDEX_CHANGE_EVENT event when it registers a PokemonConstants.POKEMON_RECEIVED dispatcher action.

Next, register an event listener with the pokemon index component. Write an _onChange function on PokemonsIndex that sets the state, and in the componentDidMount function add _onChange to the callbacks for the store's listener. Make sure to remove it in componentWillUnmount. You'll need another store function for this, too.

We're almost done. The only thing left is to fetch the pokemons when the component mounts. On success, that fetch will call ApiActions.receiveAllPokemons, which will dispatch an action. That action will cause the store to reset its pokemon and emit an event. The event will trigger the store's listener, which will reset the state of our pokemon index.

For now, to test that the PokemonsIndex component is working, just have render return a div containing this.state.pokemons. In pokedex.js.jsx on document ready, render a PokemonsIndex component into the DOM element with id pokedex that we've provided. It will overlap the background for now, but you should be able to see the info.

Now that that's working, let's change PokemonsIndex.render to render an unordered list of PokemonIndexItem components. Each index item should be passed a pokemon prop, and a unique key.

PokemonIndexItem

Create this class in a different file. It just need a render method for now. Give the pokemon list items a class name of "poke-list-item" so the css file we've provided can do its magic. Each list item should show its name and poke type.

Make sure this works. The list is still overlapping the background. We're about to fix that.

Phase 3: Router

We would like to be able to render different elements depending on our url. Eventually we want to be able to click on an item in our pokemon index and see a detailed view of that pokemon. We'll use the react router to render a root component that will in turn render our index and detail components. Then, by navigating to different urls, we'll be able to change which pokemon detail is displayed.

Getting the React Router

We'll start by refactoring the logic we already have. The react router doesn't come with react; we'll need to get it. Include the react router in your vendor/assets/javascripts folder and require it in application.js Now we have access to the global ReactRouter.

In pokedex.js.jsx, create a router and a route:

var Router = ReactRouter.Router;
var Route = ReactRouter.Route;

Instead of rendering a PokemonIndex, render the router. It should have a single route with path "/" the renders a component Root into the div with id pokedex.

Now we have to write the Root component. This should render a single div, that for now just contains one div containing a PokemonsIndex component. We want access to the route's params inside of PokemonsIndex, so pass it a prop params={this.props.params}. The div immediately containing the index should have a class "pokemon-index", for styling purposes. With the styling, you should now be able to see the index clearly.

Phase 4: Pokemon Detail

We will soon write a PokemonDetail component to display more info about individual pokemons. First add a route to the react router. It should be nested under the existing route, and have path "pokemon/:pokemonId". It should also render Root. Change Root to render a ".pokemon-detail" div after ".pokemon-index", containing a PokemonDetail component.

PokemonDetail needs to have pokemon info. Right now it only has this.props.params.pokemonId. Write a getStateFromStore function on the component that returns an object with a pokemon property. You'll need to write a find function on the pokemon store to return a pokemon given an id. find should take an integer argument. In PokemonDetail, this.props.params.pokemonId is stored as a string. Deal with this discrepency. Set the initial state of the component to this.getStateFromStore(). In render, return a div containing a "div.detail" that shows the properties of the pokemon.

There will be no pokemon when there is no pokemonId - that is, before the fetch of pokemons comes back - so first check if this.state.pokemon is defined and return an empty div if it isn't.

Phase 5: OnClick

We want to be able to click on a pokemon index item and navigate to that pokemon's url. PokemonIndexItem will need an onClick property of its rendered li to call a showDetail function. In order to navigate to a different url in this function, we'll add the mixin ReactRouter.History. Then we can use this.history.pushState to navigate to the appropriate url.

You should now be able to click on different pokemon and see the url change. The pokemon detail, however, is still blank. That's because the component doesn't update when its this.props.params change... unless we tell it to. Add a componentWillReceiveProps function to the detail. This is passed the new props. In it, call an ApiUtil function (that we haven't written yet) to fetch the appropriate pokemon. Using the flux pattern, we're going to set it up so that the fetch will cause the component's state to change. Fetching a pokemon individually from the back end when we navigate to its url will also allow us to get its toys, which we don't have access to when we fetch all the pokemons together.

Write the fetch for a single pokemon, and also modify actions, constants, and the store appropriately. You'll need an ajax request to fetch a single pokemon. This should call a receiveSinglePokemon action, which should dispatch an action that triggers the store to reset the information of a single pokemon. You might want to write a function in the file with the store to do this. The store should also emit a POKEMON_DETAIL_CHANGE_EVENT, and allow listeners for such an event to be registered with itself. In PokemonDetail, register a listener that resets state.

Now, if we fetch a single pokemon when the detail mounts and when its props change, the pokemon in state should be updated appropriately. Make sure you can explain to your partner how this works.

You should now be able to refresh the page and still see a pokemon detail view.

Phase 6: Toys

The pokemon detail should render a ToysIndex component. A toys index will have toys passed in as a property, and should render a ToyIndexItem for each toy. The index's toys will be undefined before an individual pokemon is fetched, so account for that in render.

Index items should have a toy as a property, and render a "li.toy-list-item" with its name, happiness, and price.

When we click on a toy index item, we'd like to see its detail. Give the index item class an onClick that navigates to a "/pokemon/:pokemonId/toys/:toyId" url. The router should register this nested route and render Root. Add a "div.toy-detail" containing a ToyDetail component to the div rendered by Root.

I wrote the following functions for the toy detail:

  • getStateFromStore
    • When might you not have access to a pokemon, and its toys? What simple checks can you do to not cause errors in those situations?
  • _onChange
  • getInitialState
  • componentDidMount
    • We already have a way to register a listener for a fetch of a pokemon
    • Since we're always rendering a pokemon detail whenever we render a toy detail, we don't need to fetch a pokemon here
  • componentWillUnmount
  • componentWillReceiveProps
  • render
    • Return a "div.detail"

Phase 8: PokemonForm

We'd like to be able to create new pokemon. Let's make a PokemonForm component. This will be rendered above the pokemon index in the same div. PokemonForm should render a form with a class name "new-pokemon".

We want the form to have controlled inputs. The easiest way to do this is with the mixin React.addons.LinkedStateMixin. In each of the files in config/environments, add the line config.react.addons = true # defaults to false so that we have access to the mixin. Now we can add a valueLink attribute to the inputs we want to control: valueLink={this.linkState("name")}, for example, where "name" is part of the component's state. This replaces the need to reset state in an onChange handler.

Write an onSubmit that calls a function ApiUtil.createPokemon.

Bonus: Reassigning Toys

Add a select to the toy detail that has an option for each pokemon. Choosing a different pokemon should change the ownership of the toy.