Skip to content

Instantly share code, notes, and snippets.

@1gor
Forked from jgaskins/renderable.rb
Created February 16, 2019 13:30
Show Gist options
  • Save 1gor/d93dec3438504035c623cd2c3f34bb6b to your computer and use it in GitHub Desktop.
Save 1gor/d93dec3438504035c623cd2c3f34bb6b to your computer and use it in GitHub Desktop.
"Functional" components in Clearwater
require 'clearwater/component'
# Renderable is just a component factory that makes heavy
# use of Ruby metaprogramming
module Renderable
include Clearwater::Component
# Return a Struct-like component class. It isn't an Enumerable like Structs
# are, but arguments to the factory methods become methods on instances of
# the component.
#
# @param *attrs [*Symbol] A list of attributes the component will take as
# args to the constructor
# @return [Class] The component class
#
# @example
# UserList = Renderable.function(:users) do
# ul(users.map { |user|
# UserListItem.new(user)
# })
# end
def self.function *attrs, &render
Class.new do
include ::Renderable
attr_reader *attrs
define_method :initialize do |*values|
attrs.each_with_index do |attr, index|
# Setting the object property is faster than `instance_variable_set`
# since we don't need to coerce the attribute to a string. It's a
# simple optimization.
`self[#{attr}] = #{values[index]}`
end
end
define_method :render, &render
self
end
end
# Returns a component like `function` does, but performs render caching such
# that if subsequent instances of this class receive the same values to the
# constructor, it will not re-render the component.
def self.component *attrs, &render
superclass = function(*attrs, &render)
Class.new(superclass) do
include Clearwater::CachedRender
attr_reader :_values
def initialize *values
super
@_values = values
end
def should_render? previous
_values != previous._values
end
end
end
module_function
# TODO: Other events? Whatever, this is a proof of concept. :-)
%w(
change
click
input
submit
).each do |event_name|
define_method "on#{event_name}" do |handler=nil, &block|
on(event_name, handler, &block)
end
end
# Returns a hash using its
# Intended to be used like so:
# form(onsubmit(&SendCredentials), [
# input(type: :email, placeholder: 'Email', **oninput { |email| @email = email }),
# input(type: :password, placeholder: 'Password', **oninput { |value| @password = value }),
# button('Submit'),
# ])
def on event_name, handler=nil, &block
{ "on#{event_name}" => event(handler, &block) }
end
# Handle events and yield the value you actually care about rather than
# the event itself.
def event(handler=nil, &block)
proc do |event|
value = case event.type
when :input
event.target.value
# Changing a checkbox
when :change
input = event.target
# We don't care about a checkbox's value property, just whether
# it's checked.
if input[:type] == :checkbox
input.checked?
else
input.value
end
when :submit
event.prevent
event
else
event
end
(handler || block).call value
call
end
end
def rerender_on *events
events.reduce({}) do |hash, event|
{ **hash, **on(event) { call } }
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment