Skip to content

Instantly share code, notes, and snippets.

@1gor
Forked from jgaskins/example.rb
Created February 16, 2019 13:32
Show Gist options
  • Save 1gor/b66e9f83cfbaef1f26f0172d042feaeb to your computer and use it in GitHub Desktop.
Save 1gor/b66e9f83cfbaef1f26f0172d042feaeb to your computer and use it in GitHub Desktop.
React Router v4-style routing
require 'opal'
require 'clearwater'
require 'routing'
class Layout
include Clearwater::Component
include Routing
def render
div([
route { |r|
# Root path, no child paths
r.root { HomePage.new }
r.match('articles') { ArticlesIndex.new }
r.miss { div('404 not found') }
},
])
end
end
class ArticlesIndex
include Clearwater::Component
include Routing
def render
div([
route { |r|
r.root { ArticlePage.new(articles.first) }
r.match(':article_id') { |route, id| ArticlePage.new(articles[id]) }
r.miss { h2('That article does not exist') }
},
])
end
def articles
# Load articles here
end
end
class ArticlePage
include Clearwater::Component
# No need to include Routing here despite it being a routing target because it has no child routes
def initialize article
@article = article
end
def render
return nil if @article.nil?
article([
header([
h1(@article.title),
div(@article.author_name),
time(@article.published_at.to_s),
]),
section(@article.body),
])
end
end
require 'clearwater/component'
module Routing
attr_writer :remaining_path, :current_router
def route(path: remaining_path, base_path: current_router.base_path)
router = Router.new(path: path, base_path: base_path)
result = yield router
router.matches
end
# Default to the URL path in the browser window
def remaining_path
@remaining_path || Bowser.window.location.path
end
def current_router
@current_router ||= Router.new(path: remaining_path, base_path: '')
end
class Router
attr_reader :base_path, :path, :matches
def initialize(path:, base_path: '')
@path = path.to_s
@base_path = base_path
@matches = []
end
def match candidate_path
match = check(candidate_path)
if match
route = Route.new(base_path, match)
# If the match length is 2, it means there is the full match and the
# first capture. We don't want to yield an array if there is only one
# capture because of how block argument destructuring works.
# If we get back a Ruby object, find out if we can set its remaining
# path. If it's a JS object convert it to a Ruby object before checking.
# If we don't respond to Native, we're probably not in a JS environment
# so it'll be a Ruby object anyway.
content = yield route, *match[1..-1]
matches << content
if (respond_to?(:Native) ? Native(content) : content).respond_to? :remaining_path=
content.remaining_path = match.post_match
end
if (respond_to?(:Native) ? Native(content) : content).respond_to? :current_router=
content.current_router = self
@base_path = route.path
end
end
match
end
def miss
if matches.empty?
result = yield
matches << result
result
end
end
def root
if path.match %r{^/?$}
result = yield
matches << result
result
end
end
def unique_match candidate_path, &block
return unless matches.empty?
match(candidate_path, &block)
end
private
# Check to see if the current path matches a candidate path segment
# @param candidate_path [String] the path segment to check
# @return [MatchData] Match data
def check candidate_path
# Convert dynamic segments into regexp captures
matchable_path = candidate_path.gsub(/:\w+/, '([^/]+)')
# Don't match a partial segment. For example,
# don't match /widget for /widgets.
path.match(Regexp.new("^/?#{matchable_path}(?:/|$)"))
end
end
class Redirect
require 'clearwater/black_box_node'
include Clearwater::BlackBoxNode
def self.to path, &block
new to: path, &block
end
def initialize(to:)
@target = to
end
def mount element
Bowser.window.history.push @target
# Delay rendering until the next frame so we don't interfere with the
# current render.
Bowser.window.animation_frame do
Clearwater::Component.call
end
end
end
class ConfirmableLink
include Clearwater::Component
def initialize props={}, content=nil, &check
@props = props.reject { |key, value| key == :message }
@content = content
@message = props[:message]
@check = check || proc { true }
end
def render
a(@props.merge(onclick: method(:onclick)), @content)
end
def onclick event
event.prevent
@props.fetch(:onclick) { proc {} }.call event
navigate = [email protected] || `confirm(#@message)`
navigate_to @props[:href] if navigate
end
def navigate_to path
Bowser.window.history.push path
call
end
end
class Route
attr_reader :path
def initialize base_path, match
@path = [base_path, match[0]]
.join('/')
.sub(%r{/$}, '') # Don't end URL paths in slashes
.sub('//', '/') # Duplicate slashes can happen if matches contain them
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment