README.md in hyper-router-2.4.1 vs README.md in hyper-router-4.0.0
- old
+ new
@@ -1,385 +1,447 @@
-# ![](https://github.com/Serzhenka/hyper-loop-logos/blob/master/hyper-router_150.png)Hyper-router
+## HyperRouter
-The Hyperloop Router allows you write and use the React Router in Ruby through Opal.
+HyperRouter allows you write and use the React Router in Ruby through Opal.
## Installation
Add this line to your application's Gemfile:
-
```ruby
gem 'hyper-router'
```
+Or execute:
+```bash
+gem install hyper-router
+```
-And then execute:
+Then add this to your components.rb:
+```ruby
+require 'hyper-router'
+```
- $ bundle
-
-## Usage
-
-This is simply a DSL wrapper on [react-router](....)
-
-### DSL
-
-The following DSL:
-
+### Using the included source
+Add this to your component.rb:
```ruby
-route("/", mounts: App, index: Home) do
- route("about")
- route("inbox") do
- redirect('messages/:id').to { | params | "/messages/#{params[:id]}" }
- end
- route(mounts: Inbox) do
- route("messages/:id")
- end
-end
+require 'hyper-router/react-router-source'
+require 'hyper-router'
```
-Is equivalent to this route configuration:
+### Using with NPM/Webpack
+react-router has now been split into multiple packages, so make sure they are all installed
+```bash
+npm install react-router react-router-dom history --save
+```
+Add these to your webpack js file:
```javascript
-const routes = {
- path: '/',
- component: App,
- indexRoute: { component: Dashboard },
- childRoutes: [
- { path: 'about', component: About },
- {
- path: 'inbox',
- component: Inbox,
- childRoutes: [{
- path: 'messages/:id',
- onEnter: ({ params }, replace) => replace(`/messages/${params.id}`)
- }]
- },
- {
- component: Inbox,
- childRoutes: [{
- path: 'messages/:id', component: Message
- }]
- }
- ]
-}
+ReactRouter = require('react-router')
+ReactRouterDOM = require('react-router-dom')
+History = require('history')
```
-The basic dsl syntax is designed with the following in mind:
+## Usage
-1. Most routes have a path so that is the assumed first argument.
-2. Use `mounts` rather than component (reads better?)
-3. Convention over configuration, given a path, the component name can be derived.
-4. Redirect takes the path, and a block (similar to the JSX DSL)
-5. The first param to route can be skipped per the documentation
-6. Use standard ruby lower case method names instead of caps (reserve those for components)
+This is simply a DSL wrapper on [react-router](https://github.com/ReactTraining/react-router)
-The above example does not cover all the possible syntax, here are the other methods and options:
+## Warning!!
+The folks over at react-router have gained a reputation for all their API rewrites, so with V4 we have made some changes to follow.
+This version is **incompatible** with previous versions' DSL.
-#### enter / leave / change transition hooks
+### DSL
-for adding an onEnter or onLeave hook you would say:
+Here is the basic example that is used on the [react-router site](https://reacttraining.com/react-router/)
-```ruby
-route("foo").on(:leave) { | t | ... }.on(:enter) { | t |.... }
-```
-which follows the react.rb event handler convention.
+```javascript
+import React from 'react'
+import {
+ BrowserRouter as Router,
+ Route,
+ Link
+} from 'react-router-dom'
-A `TransitionContext` object will be passed to the handler, which has the following methods:
+const BasicExample = () => (
+ <Router>
+ <div>
+ <ul>
+ <li><Link to="/">Home</Link></li>
+ <li><Link to="/about">About</Link></li>
+ <li><Link to="/topics">Topics</Link></li>
+ </ul>
-| method | available on | description |
-|-----------|------------------|-----------------|
-| `next_state` | `:change`, `:enter` | returns the next state object |
-| `prev_state` | `:change` | returns the previous state object |
-| `replace` | `:change`, `:enter` | pass `replace` a new path |
-| `promise` | `:change`, `:enter` | returns a new promise. multiple calls returns the same promise |
+ <hr/>
-If you return a promise from the `:change` or `:enter` hooks, the transition will wait till the promise is resolved before proceeding. For simplicity you can call the promise method, but you can also use some other method to define the promise.
+ <Route exact path="/" component={Home}/>
+ <Route path="/about" component={About}/>
+ <Route path="/topics" component={Topics}/>
+ </div>
+ </Router>
+)
-The hooks can also be specified as proc values to the `:on_leave`, `:on_enter`, `:on_change` options.
+const Home = () => (
+ <div>
+ <h2>Home</h2>
+ </div>
+)
-#### multiple component mounting
+const About = () => (
+ <div>
+ <h2>About</h2>
+ </div>
+)
-The `mounts` option can accept a single component, or a hash which will generate a `components` (plural) react-router prop, as in:
+const Topics = ({ match }) => (
+ <div>
+ <h2>Topics</h2>
+ <ul>
+ <li><Link to={`${match.url}/rendering`}>Rendering with React</Link></li>
+ <li><Link to={`${match.url}/components`}>Components</Link></li>
+ <li><Link to={`${match.url}/props-v-state`}>Props v. State</Link></li>
+ </ul>
-`route("groups", mounts: {main: Groups, sidebar: GroupsSidebar})` which is equivalent to:
+ <Route path={`${match.url}/:topicId`} component={Topic}/>
+ <Route exact path={match.url} render={() => (
+ <h3>Please select a topic.</h3>
+ )}/>
+ </div>
+)
-`{path: "groups", components: {main: Groups, sidebar: GroupsSidebar}}` (json) or
+const Topic = ({ match }) => (
+ <div>
+ <h3>{match.params.topicId}</h3>
+ </div>
+)
-`<Route path="groups" components={{main: Groups, sidebar: GroupsSidebar}} />` JSX
+export default BasicExample
+```
-#### The `mounts` option can also take a `Proc` or be specified as a block
+Here is what it looks like for us:
+```ruby
+class BasicExample < Hyperloop::Router
+ history :browser
-The proc is passed a TransitionContext (see **Hooks** above) and may either return a react component to be mounted, or return a promise. If a promise is returned the transition will wait till the promise is either resolved with a component, or rejected.
+ route do
+ DIV do
+ UL do
+ LI { Link('/') { 'Home' } }
+ LI { Link('/about') { 'About' } }
+ LI { Link('/topics') { 'Topics' } }
+ end
-`route("courses/:courseId", mounts: -> () { Course }`
+ Route('/', exact: true, mounts: Home)
+ Route('/about', mounts: About)
+ Route('/topics', mounts: Topics)
+ end
+ end
+end
-is the same as:
+class Home < Hyperloop::Router::Component
+ render(:div) do
+ H2 { 'Home' }
+ end
+end
-```jsx
-<Route path="courses/:courseId" getComponent={(nextState, cb) => {cb(null, Course)}} />
-```
+class About < Hyperloop::Router::Component
+ render(:div) do
+ H2 { 'About' }
+ end
+end
-Also instead of a proc, a block can be specified with the `mounts` method:
+class Topics < Hyperloop::Router::Component
+ render(:div) do
+ H2 { 'Topics' }
+ UL() do
+ LI { Link("#{match.url}/rendering") { 'Rendering with React' } }
+ LI { Link("#{match.url}/components") { 'Components' } }
+ LI { Link("#{match.url}/props-v-state") { 'Props v. State' } }
+ end
+ Route("#{match.url}/:topic_id", mounts: Topic)
+ Route(match.url, exact: true) do
+ H3 { 'Please select a topic.' }
+ end
+ end
+end
-`route("courses/:courseId").mounts { Course }`
+class Topic < Hyperloop::Router::Component
+ render(:div) do
+ H3 { match.params[:topic_id] }
+ end
+end
+```
-Which generates the same route as the above.
+Since react-router migrated back to everything being a component,
+this makes the DSL very easy to follow if you have already used react-router v4.
-More interesting would be something like this:
+### Router
+This is the base Router class, it can either be inherited or included:
```ruby
-route("courses/:id").mounts do | ct |
- HTTP.get("validate-user-access/courses/#{ct.next_state[:id]}").then { Course }
+class MyRouter < Hyperloop::Router
end
+
+class MyRouter < React::Component::Base
+ include Hyperloop::Router::Base
+end
```
-*Note that the above works because of promise chaining.*
+With the base Router class, you must specify the history you want to use.
-You can use the `mount` method multiple times with different arguments as an alternative to passing the the `mount` option a hash:
+This can be done either using a macro:
+```ruby
+class MyRouter < Hyperloop::Router
+ history :browser
+end
+```
+The macro accepts three options: `:browser`, `:hash`, or `:memory`.
-`route("foo").mount(:baz) { Comp1 }.mount(:bar) { Comp2 }.mount(:bomb)`
-
-Note that if no block is given (as in `:bomb` above) the component name will be inferred from the argument (`Bomb` in this case.)
-
-#### The index component can be specified as a proc
-
-Same deal as mount...
-
-`route("foo", index: -> { MyIndex })`
-
-#### The index method
-
-Instead of specifying the index component as a param to the parent route, it can be specified as a child using the
-index method:
-
+Or defining the `history` method:
```ruby
-route("/", mounts: About, index: Home) do
- index(mounts: MyIndex)
- route("about")
- route("privacy-policy")
+class MyRouter < Hyperloop::Router
+ def history
+ self.class.browser_history
+ end
end
```
-This is useful because the index method has all the features of a route except that it does not take a path or children.
+### BrowserRouter, HashRouter, MemoryRouter, StaticRouter
-#### The `redirect` options
+Using one of these classes automatically takes care of the history for you,
+so you don't need to specify one.
+They also can be used by inheritance or inclusion:
-with static arguments:
+```ruby
+class MyRouter < Hyperloop::HashRouter
+end
-`redirect("/from/path/spec", to: "/to/path/spec", query: {q1: 123, q2: :abc})`
+class MyRouter < React::Component::Base
+ include Hyperloop::Router::Hash
+end
+```
-the `:to` and `:query` options can be Procs which will receive the current state.
+### Rendering a Router
-Or you can specify the `:to` an `:query` options with blocks:
+To render children/routes use the `route` macro, it is the equivalent to `render` of a component.
+```ruby
+class MyRouter < Hyperloop::Router
+ ...
-`redirect("/from/path/spec/:id").to { |curr_state| "/to/path/spec/#{current_state[:id]}"}.query { {q1: 12} }`
+ route do
+ DIV do
+ H1 { 'Hello world!' }
+ end
+ end
+end
+```
-#### The `index_redirect` method
-just like `redirect` without the first arg: `index_redirect(to: ... query: ...)`
+### Routes
-### The Router Component
+Routes are no longer defined separately, but are just components you call inside the router/components.
-A router is defined as a subclass of `Hyperloop::Router` which is itself a `Hyperloop::Component`.
-
```ruby
-class Components::Router < Hyperloop::Router
- def routes # define your routes (there is no render method)
- route("/", mounts: About, index: Home) do
- route("about")
- route("inbox") do
- redirect('messages/:id').to { | params | "/messages/#{params[:id]}" }
- end
- route(mounts: Inbox) do
- route("messages/:id")
- end
+class MyRouter < Hyperloop::Router
+ ...
+
+ route do
+ DIV do
+ Route('/', mounts: HelloWorld)
end
- end
+ end
end
-```
-#### Mounting your Router
-You will mount this component the usual way (i.e. via `render_component`, `Element#render`, `react_render`, etc) or even by mounting it within a higher level application component.
-```ruby
-class Components::App < Hyperloop::Component
- render(DIV) do
- Application::Nav()
- MAIN do
- Router()
- end
+class HelloWorld < React::Component::Base
+ render do
+ H1 { 'Hello world!' }
end
end
```
-#### navigating
+The `Route` method takes a url path, and these options:
+- `mounts: Component` The component you want to mount when routed to
+- `exact: Boolean` When true, the path must match the location exactly
+- `strict: Boolean` When true, the path will only match if the location and path **both** have/don't have a trailing slash
+It can also take a block instead of the `mounts` option.
-Create links to your routes with `Router::Link`
```ruby
-#Application::Nav
- LI.nav_link { TestRouter::Link("/") { "Home" } }
- LI.nav_link { TestRouter::Link("/about") { "About" } }
- params.messsages.each do |msg|
- LI.nav_link { TestRouter::Link("/inbox/messages/#{msg.id}") { msg.title } }
+class MyRouter < Hyperloop::Router
+ ...
+
+ route do
+ DIV do
+ Route('/', exact: true) do
+ H1 { 'Hello world!' }
+ end
+ end
end
+end
```
-
-Additionally, you can manipulate the history with by passing JS as so
+The block will give you the match, location, and history data:
```ruby
-# app/views/components/app_links.rb
-class Components::AppLinks
- class << self
- if RUBY_ENGINE == 'opal'
- def inbox
- `window.ReactRouter.browserHistory.push('/inbox');`
+class MyRouter < Hyperloop::Router
+ ...
+
+ route do
+ DIV do
+ Route('/:name') do |match, location, history|
+ H1 { "Hello #{match.params[:foo]} from #{location.pathname}, click me to go back!" }
+ .on(:click) { history.go_back }
end
- def message(id)
- `window.ReactRouter.browserHistory.push('/messages/#{id}');`
- end
end
end
end
```
+It is recommended to inherit from `Hyperloop::Router::Component` for components mounted by routes.
+This automatically sets the `match`, `location`, and `history` params,
+and also gives you instance methods with those names.
+You can use either `params.match` or just `match`.
+and gives you access to the Route method and more.
+This allows you to create inner routes as you need them.
+```ruby
+class MyRouter < Hyperloop::Router
+ ...
-#### Other router hooks:
+ route do
+ DIV do
+ Route('/:name', mounts: Greet)
+ end
+ end
+end
-There are several other methods that can be redefined to modify the routers behavior
+class Greet < Hyperloop::Router::Component
+ render(DIV) do
+ H1 { "Hello #{match.params[:foo]}!" }
+ Route(match.url, exact: true) do
+ H2 { 'What would you like to do?' }
+ end
+ Route("#{match.url}/:activity", mounts: Activity)
+ end
+end
-#### history
-
-```ruby
-class Router < Hyperloop::Router
- def history
- ... return a history object
+class Activity < Hyperloop::Router::Component
+ render(DIV) do
+ H2 { params.match.params[:activity] }
end
end
```
-The two standard history objects are predefined as `browser_history` and `hash_history` so you can say:
+Routes will **always** render alongside sibling routes that match as well.
```ruby
-...
- def history
- browser_history
+class MyRouter < Hyperloop::Router
+ ...
+
+ route do
+ DIV do
+ Route('/goodbye', mounts: Goodbye)
+ Route('/:name', mounts: Greet)
+ end
end
+end
```
-or just
+### Switch
+Going to `/goodbye` would match `/:name` as well and render `Greet` with the `name` param with the value 'goodbye'.
+To avoid this behavior and only render one matching route at a time, use a `Switch` component.
+
```ruby
-...
- alias_method :history :browser_history
+class MyRouter < Hyperloop::Router
+ ...
+
+ route do
+ DIV do
+ Switch do
+ Route('/goodbye', mounts: Goodbye)
+ Route('/:name', mounts: Greet)
+ end
+ end
+ end
+end
```
-#### create_element
+Now, going to `/goodbye` would match the `Goodbye` route first and only render that component.
-`create_element` (if defined) is passed the component that the router will render, and its params. Use it to intercept, inspect and/or modify the component behavior.
+### Links
-`create_element` can return any of these values:
+Links are available to Routers, classes that inherit from `HyperLoop::Router::Component`,
+or by including `Hyperloop::Router::ComponentMethods`.
-+ Any falsy value: indicating that rendering should continue with no modification to behavior.
-+ A `React::Element`, or a native `React.Element` which will be used for rendering.
-+ Any truthy value: indicating that a new Element should be created using the (probably modified) params
-
+The `Link` method takes a url path, and these options:
+- `search: String` adds the specified string to the search query
+- `hash: String` adds the specified string to the hash location
+It can also take a block of children to render inside it.
```ruby
-class Router < Hyperloop::Router
- def create_element(component, component_params)
- # add the param :time_stamp to each element as its rendered
- React.create_element(component, component_params.merge(time_stamp: Time.now))
+class MyRouter < Hyperloop::Router
+ ...
+
+ route do
+ DIV do
+ Link('/Gregor Clegane')
+
+ Route('/', exact: true) { H1() }
+ Route('/:name') do |match|
+ H1 { "Will #{match.params[:name]} eat all the chickens?" }
+ end
+ end
end
end
```
-The above could be simplified to:
+### NavLinks
+NavLinks are the same as Links, but will add styling attributes when it matches the current url
+- `active_class: String` adds the class to the link when the url matches
+- `active_style: String` adds the style to the link when the url matches
+- `active: Proc` A proc that will add extra logic to determine if the link is active
```ruby
-...
- def create_element(component, component_params)
- component_params[:time_stamp] = Time.now
- end
-```
+class MyRouter < Hyperloop::Router
+ ...
-Just make sure that you return a truthy value otherwise it will ignore any changes to component or params.
+ route do
+ DIV do
+ NavLink('/Gregor Clegane', active_class: 'active-link')
+ NavLink('/Rodrik Cassel', active_style: { color: 'grey' })
+ NavLink('/Oberyn Martell',
+ active: ->(match, location) {
+ match && match.params[:name] && match.params[:name] =~ /Martell/
+ })
-Or if you just wanted some kind of logging:
-
-```ruby
-...
- def create_element(component, component_params)
- puts "[#{Time.now}] Rendering: #{component.name}" # puts returns nil, so we are jake mate
+ Route('/', exact: true) { H1() }
+ Route('/:name') do |match|
+ H1 { "Will #{match.params[:name]} eat all the chickens?" }
+ end
+ end
end
+end
```
-The component_params will always contain the following keys as native js objects, and they must stay native js objects:
+### Pre-rendering
-+ `children`
-+ `history`
-+ `location`
-+ `params`
-+ `route`
-+ `route_params`
-+ `routes`
+Pre-rendering has been made extremely simple in this new version.
+Under the hood a StaticRouter is used whenever a Router component is prerendering.
+To prerender correctly though you will need to give it the current path.
+Since there is no DOM, you must pass in the current path from your controller/view as a param.
+There is a special param macro called `prerender_path`,
+which still acts as a normal param but will use that param as the current path in prerendering.
-We will try to get more fancy with a later version of reactrb-router ;-)
-
-#### `stringify_query(params_hash)` <- needs work
-
-The method used to convert an object from <Link>s or calls to transitionTo to a URL query string.
-
```ruby
-class Router < Hyperloop::Router
- def stringify_query(params_hash)
- # who knows doc is a little unclear on this one...is it being passed the full params_hash or just
- # the query portion.... we shall see...
+class MyController < ApplicationController
+ def show
+ render component: 'MyRouter', props: { current_path: request.path }
end
end
-```
-#### `parse_query_string(string)` <- needs work
+class MyRouter < Hyperloop::Router
+ prerender_path :current_path
-The method used to convert a query string into the route components's param hash
-
-#### `on_error(data)`
-
-While the router is matching, errors may bubble up, here is your opportunity to catch and deal with them. Typically these will come when promises are rejected (see the DSL above for returning promises to handle async behaviors.)
-
-#### `on_update`
-
-Called whenever the router updates its state in response to URL changes.
-
-#### `render`
-
-A `Router` default `render` looks like this:
-
-```ruby
- def render
- # Router.router renders the native router component
- Router.router(build_params)
+ route do
+ DIV do
+ Route('/:name', mounts: Greet)
+ end
end
+end
```
-
-This is primarily for integrating with other libraries that need to participate in rendering before the route components are rendered. It defaults to render={(props) => <RouterContext {...props} />}.
-
-Ensure that you render a <RouterContext> at the end of the line, passing all the props passed to render.
-
-### Hyperloop::Router::Component
-
-The class Hyperloop::Router::Component is a subclass of Hyperloop::Component that predefines the params that the router will be passing in to your component. This includes
-
-`params.location`
-
-The current location.
-
-`params.params`
-
-The dynamic segments of the URL.
-
-`params.route`
-
-The route that rendered this component.
-
-`params.route_params`
-
-The subset of `params.params` that were directly specified in this component's route. For example, if the route's path is `users/:user_id` and the URL is /users/123/portfolios/345 then `params.route_params` will be `{user_id: '123'}`, and `params.params` will be `{user_id: '123', portfolio_id: 345}`.
## Development
`bundle exec rake` runs test suite