Skip to content

Instantly share code, notes, and snippets.

@jgautsch
Created July 29, 2015 04:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jgautsch/bef8f7a95dfeff59c21f to your computer and use it in GitHub Desktop.
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
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();
});
<!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>
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
#
# 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
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