Created
July 29, 2015 04:54
-
-
Save jgautsch/bef8f7a95dfeff59c21f to your computer and use it in GitHub Desktop.
Defining client routes for react-routes in ruby, so they can be checked both on the client and server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import $ from 'jquery'; | |
import React from 'react'; | |
import Router, { Route } from 'react-router'; | |
import App from './components/App'; | |
// Recursively builds the nested react components that represent the routes | |
var buildRoutes = (routesObj) => { | |
return routesObj.map((route) => { | |
if (!route.name || !route.path || !route.handler) { | |
console.error('route.name (:as), route.path, and route.handler must all be defined.'); | |
} | |
var props = { | |
name: route.name, | |
path: route.path, | |
handler: require('./components/' + route.handler) | |
}; | |
if (route.children === undefined) { | |
return React.createElement.apply(this, [Route, props]); | |
} else { | |
return React.createElement.apply(this, [Route, props, buildRoutes(route.children)]); | |
} | |
}); | |
}; | |
var routes = React.createElement(Route, {handler: App}, buildRoutes(window.clientRoutes)); | |
$(function onLoad() { | |
function render() { | |
if ($('#content').length > 0) { | |
Router.run(routes, Router.HistoryLocation, (Root) => { | |
React.render(<Root/>, document.getElementById("content")); | |
}); | |
if (window.returnPath !== null) { | |
Router.HistoryLocation.push(window.returnPath); | |
} | |
} | |
} | |
render(); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<!-- ... --> | |
</head> | |
<body> | |
<%= render 'layouts/common/messages' %> | |
<%= yield %> | |
<div id="content"></div> | |
<script type="text/javascript"> | |
/* jshint ignore:start */ | |
window.clientRoutes = <%= raw json_escape(ClientRoutes.routes.to_json) %>; | |
<% if session[:client_return_path] %> | |
window.returnPath = "<%= session.delete(:client_return_path) %>"; | |
<% end %> | |
/* jshint ignore:end */ | |
</script> | |
<%= javascript_include_tag :main_webpack_bundle %> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ApplicationController < ActionController::Base | |
# ... | |
def rails_route_not_found | |
if ClientRoutes.has_route?(request.fullpath) && request.method == "GET" | |
# Client-side frieldly forwarding: | |
# We know the route exists as a client route, so | |
# render the right layout, and let the client navigate | |
# to the right route to render the right react components | |
session[:client_return_path] = request.fullpath | |
redirect_to root_path # or a blank page of some sort | |
else | |
raise ActionController::RoutingError.new('Not Found') | |
end | |
end | |
# ... | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# ClientRoutes class: | |
# Used to define client-specific routes, for use with react-routes. | |
# | |
class ClientRoutes | |
# Generate a set of client routes (a recursive array of hashes) from | |
# the block written using the DSL. Then freeze it so it's immutable | |
# in the rest of the application. | |
# | |
# @yield the block written in the client-route-defining DSL | |
def self.draw(&block) | |
@@client_routes ||= RouteBuilder.draw(&block) | |
@@client_routes = RouteBuilder.draw(&block) | |
@@client_routes.freeze | |
end | |
# The recursive array of hashes that is the set of routes defined | |
# for the client. | |
# | |
# @return [Array<Hash>] the array containing hashes representing client routes. | |
def self.routes | |
@@client_routes | |
end | |
# This expands the collection of routes into an array of strings. | |
# This can be useful for debugging. | |
# | |
# @return [Array<String>] a collection of strings representing the | |
# routes | |
# Ex. ["/inbox", "/inbox/messages", "/inbox/messages/:id"] | |
def self.expanded_routes | |
RoutesExpander.new(@@client_routes).expanded_routes | |
end | |
# Checks whether a route is present for a given path | |
# | |
# @param path [String] the path that is to be checked against the routes | |
# @return [Boolean] whether the path matches a route or not | |
def self.has_route?(path) | |
RoutesExpander.new(@@client_routes).recognize?(path) | |
end | |
# Prints out the routes in a readable form. | |
def self.print_routes | |
puts "\nClient Routes:\n============================================\n\n" | |
RoutePrinter.new(routes).print | |
end | |
# This class implements the DSL used to build up the collection of | |
# client routes. It calls itself recursively. | |
class RouteBuilder | |
attr_reader :routes | |
# The main DSL wrapper | |
# | |
# @yield evaluate the provided block within the context of an | |
# instance of this class | |
# @return [Array<Hash>] the built up collection of client routes | |
def self.draw(&block) | |
builder = RouteBuilder.new | |
builder.instance_eval(&block) | |
builder.routes | |
end | |
def initialize | |
@routes = [] | |
end | |
# The main method/command of the DSL used for defining the client | |
# routes. It is used to define a route, which will be added to the | |
# routes collection. | |
# Ex. `get 'inbox', as: 'inbox', handler: 'Inbox' # do ...` | |
# | |
# @param path [String] the path of the route being defined | |
# @param options [Hash] the options hash; the two important keys are: | |
# - :as => the name that will be given to the client route | |
# - :handler => the name of the component that is to be the entrypoint | |
# for handling/rendering requests to this route/path | |
# @yield recusively call the block to define sub/nested routes | |
# @return [Array<Hash>] an array of the built up collection of client routes | |
def get(path, options = {}, &block) | |
route = { | |
name: options[:as] || path, | |
path: path, | |
handler: options[:handler] || path.camelcase | |
} | |
route[:children] = RouteBuilder.draw(&block) if block | |
@routes.push(route) | |
end | |
# WIP: | |
# def namespace | |
# end | |
end | |
# This class's job is to print out information on the client routes, | |
# such as expanded path, name, and handler component. | |
class RoutePrinter | |
# Initialize an object of this class | |
# | |
# @param routes [Array<Hash>] the collection of client routes | |
# @param parent_path [String] the prefix to prepend to the string | |
# that is generated for a route. This is important because this | |
# class is called recursively for sub/nested routes | |
def initialize(routes, parent_path = '') | |
@routes = Array.wrap(routes) | |
@parent_path = parent_path | |
end | |
# Prints out all the routes, and their information | |
def print | |
@routes.each do |route| | |
path = "#{@parent_path}/#{route[:path]}" | |
puts "#{path} - As: #{route[:name]} - Component Handler: #{route[:handler]}" | |
RoutePrinter.new(route[:children], path).print unless route[:children].blank? | |
end | |
end | |
end | |
class RoutesExpander | |
def initialize(routes, parent_path = '') | |
@routes = Array.wrap(routes) | |
@parent_path = parent_path | |
end | |
# Expands the array[hashes] of routes, constructed from the DSL, | |
# into an array array of strings representing valid route paths. | |
# | |
# @return [Array<String>] collection of fully-expanded route templates | |
# Ex. ["/inbox", "/inbox/messages", "/inbox/messages/:id"] | |
def expanded_routes | |
@routes.map do |route| | |
path = "#{@parent_path}/#{route[:path]}" | |
[path, RoutesExpander.new(route[:children], path).expanded_routes].compact | |
end.flatten | |
end | |
# Compares a path ("/inbox/messages") to the available routes | |
# | |
# @param path [String] the path to be checked against the available routes | |
# @return [Boolean] whether the path param matches any of the available routes | |
def recognize?(path) | |
re = RouteRegexifier.regexify(expanded_routes) | |
path.match(re) | |
end | |
class RouteRegexifier | |
# Takes a route string or array of route strings and turns them into | |
# regex's thatare unioned, resulting in a single regular expression. | |
# | |
# @param routes [String, Array<String>] the route string or collection | |
# of route strings that describe the available routes. | |
# Ex. ["/inbox", "/inbox/messages", "/inbox/messages/:id"] | |
# @return [Regexp] a regular expression | |
def self.regexify(routes) | |
routes = Array.wrap(routes) | |
route_regexes = routes.map do |route| | |
Regexp.new('^' + route.gsub('/', '\/').gsub(/:[a-z]+/, '[-!#$&;=?0-9:_a-zA-Z~]+') + '$') | |
end | |
Regexp.union(route_regexes) | |
end | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Rails.application.routes.draw do | |
# ... | |
match '*path', via: :all, to: 'application#rails_route_not_found' | |
end | |
ClientRoutes.draw do | |
get 'dashboard', as: 'dashboard', handler: 'Dashboard' | |
get 'about', as: 'about', handler: 'About' | |
get 'inbox', as: 'inbox', handler: 'Inbox' do | |
get 'messages', as: 'messages', handler: 'Messages' do | |
get ':id', as: 'message', handler: 'Message' | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment