Skip to content

Instantly share code, notes, and snippets.

@jgaskins
Created July 27, 2016 23:27
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jgaskins/da9fa65ac20809f17db58024197797f0 to your computer and use it in GitHub Desktop.
Save jgaskins/da9fa65ac20809f17db58024197797f0 to your computer and use it in GitHub Desktop.
Clearwater app to fetch and render data from the GitHub API
require 'opal'
require 'clearwater'
require 'grand_central/model'
require 'bowser/http'
class Layout
include Clearwater::Component
attr_reader :users
# Setup the initial users collection, which will get updated as we go along.
# Important to note that Collection is immutable. If you're updating it, it
# needs to be reassigned. This is great for cache-invalidation performance.
# However, if you need this collection inside a subordinate routing target,
# you shouldn't store it here. It should go into a global store, like a
# GrandCentral::Store.
def initialize
@users = Collection.new
end
def render
div([
h1(Link.new({ href: '/' }, 'People on GitHub')),
button({ onclick: method(:load_users) }, 'Load Users'),
# Master-detail view of the user list and user details (when implemented)
div([
div({ style: Style.side_by_side('20%') }, user_list),
div({ style: Style.side_by_side('75%') }, outlet),
]),
])
end
# Dynamically render the user list based on the status of
def user_list
if users.loading?
p 'Fetching users...'
elsif users.any?
ul({ style: { padding: 0, margin: 0 } }, users.map { |user|
li(Link.new({ href: "/#{user.login}" }, [
# Notice we call new on this component. That means it cannot hold
# state between renders. We are generating an entirely new object.
UserAvatar.new(user),
user.login,
]))
})
elsif !users.loaded?
p 'Users not loaded yet'
else
p 'No users :-('
end
end
# Load users from the GitHub API
def load_users
@users = @users.update(loading: true)
call # Rerender on the next animation frame
# Send off an AJAX request to the GitHub API for a list of users past the
# highest user id we currently have.
Bowser::HTTP.fetch("https://api.github.com/users?since=#{last_user_id}")
.then do |response| # On success, we get a Response object back.
@users = Collection.with(response.json.map { |hash| User.new(hash) })
call
end
.fail do |exception| # On failure
alert exception.message
end
end
def last_user_id
users.last && users.last.id
end
module Style
module_function
def side_by_side width
{
display: 'inline-block',
vertical_align: :top,
width: width,
}
end
end
end
# Presentational component to represent a user's GitHub avatar.
class UserAvatar
include Clearwater::Component
def initialize user
@user = user
end
def render
img(
src: @user.avatar_url,
style: {
width: size,
height: size,
},
)
end
def size
'32px'
end
end
# Simple model to represent users fetched from the GitHub API.
class User < GrandCentral::Model
attributes(:id, :login, :avatar_url)
end
# An immutable collection. It has a few properties to represent the status
# of loading from the server.
class Collection < GrandCentral::Model
include Enumerable
attributes(:models, :loading, :loaded)
alias loading? loading
alias loaded? loaded
def self.with(models)
new(models: models, loaded: true)
end
def each &block
models.each &block
end
def last
models.last
end
def models
@models || []
end
def << value
self + [value]
end
def + enum
self.class.with(models.to_a + enum.to_a)
end
def - enum
self.class.with(models.to_a - enum.to_a)
end
end
app = Clearwater::Application.new(
component: Layout.new, # Also a long-lived component, so it can hold state
)
app.call
source 'https://rubygems.org'
gem 'clearwater', '~> 1.0.0.rc1'
gem 'opal-rails'
gem 'grand_central'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.2.6'
# Use sqlite3 as the database for Active Record
gem 'sqlite3'
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment