-
-
Save 1gor/d93dec3438504035c623cd2c3f34bb6b to your computer and use it in GitHub Desktop.
"Functional" components in Clearwater
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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