Skip to content

Instantly share code, notes, and snippets.

@1gor
Forked from jgaskins/app.rb
Created February 16, 2019 13:36
Show Gist options
  • Save 1gor/50bb437b235b307a9df706a0936ccce4 to your computer and use it in GitHub Desktop.
Save 1gor/50bb437b235b307a9df706a0936ccce4 to your computer and use it in GitHub Desktop.
React Router v4-style routing in Clearwater
require 'opal'
require 'clearwater'
require 'routing'
class Layout
include Clearwater::Component
include Routing
def render
div([
nav([
Link.new({ href: '/' }, 'Home'),
' ',
Link.new({ href: '/articles' }, 'Articles'),
' ',
Link.new({ href: '/omg' }, 'OMG'),
]),
route do
match('articles') { ArticlesList.new }
# We can match it multiple times, both will be rendered.
match('omg') { h1('OMG') }
match('omg') { h2 'omg lol' }
miss { h1 'Home Page!' }
end,
])
end
end
class ArticlesList
include Clearwater::Component
include Routing
def render
MasterDetail.new(
div([
ul(articles.map { |article| ArticlesListItem.new(article) }),
]),
div([
route do
# Notice we call id.to_i - the yielded value is extracted from a
# string so that's what we get, but we are checking against a numeric
# id.
match(':id') { |id| ArticleDetail.new(articles[id.to_i]) }
miss { h3 'Select an article from the list at the left' }
end,
route do
match(':id') { |id| h2 "id: #{id}" }
end,
])
)
end
def articles
@articles ||= Collection.new(Array.new(20) { |id|
Article.new(
id: id + 1,
title: "Article ##{id + 1}",
body: %w(foo bar baz quux omg lol wtf bbq zomg wow so amaze).shuffle.join(' '),
)
})
end
end
ArticlesListItem = Struct.new(:article) do
include Clearwater::Component
def render
li([
Link.new({ href: "/articles/#{article.id}" }, article.title),
])
end
end
ArticleDetail = Struct.new(:article) do
include Clearwater::Component
def render
if article.nil?
return p('Article not found :-(')
end
main([
h1(article.title),
section(article.body),
])
end
end
# Indexed collection, subscriptable by model id
Collection = Struct.new(:models) do
include Enumerable
def each
models.each { |item| yield item }
end
def [] id
models_by_id[id]
end
def models_by_id
@models_by_id ||= models.each_with_object({}) do |model, hash|
hash[model.id] = model
end
end
end
class Article
attr_reader :id, :title, :body
def initialize attributes={}
attributes.each do |attr, value|
`self[#{attr}] = #{value}`
end
end
end
MasterDetail = Struct.new(:master, :detail) do
include Clearwater::Component
def render
div([
div({ style: Style.master }, master),
div({ style: Style.detail }, detail),
])
end
module Style
module_function
def master
side_by_side '25%'
end
def detail
side_by_side '74%'
end
def side_by_side width
{
display: 'inline-block',
vertical_align: :top,
width: width,
}
end
end
end
app = Clearwater::Application.new(component: Layout.new)
app.call
module Routing
attr_accessor :matched_path
def route
@matched = []
@path_matcher = PathMatcher.new(matched_path, Bowser.window.location.path)
yield
@matched
end
def match path_segment
match = @path_matcher.match? path_segment
if match
@matched << ChildRoute.new(yield(match), "#{@path_matcher.current_match}/#{path_segment}")
end
end
def miss
if @matched.none?
@matched << ChildRoute.new(yield, matched_path)
end
end
ChildRoute = Struct.new(:content, :current_path_match) do
include Clearwater::Component
def render
# Allow for plain JS objects but also check to see if we can use the accessor
if `!!(#{content} && #{content}.$$class) && #{content.respond_to? :matched_path=}`
content.matched_path = current_path_match
end
content
end
end
class PathMatcher
attr_reader :current_match, :current_path
def initialize current_match, current_path
@current_match = current_match.to_s
@current_path = current_path.to_s
end
def match? segment
segment = segment.sub(%r(^/), '')
match_check = "#{current_match}/#{segment}"
if current_path.start_with? match_check
true
elsif segment.start_with? ':'
current_path.sub(%r(^#{Regexp.escape(current_match)}/?), '')[/^\w+/]
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment