Skip to content

Instantly share code, notes, and snippets.

@serradura
Last active April 28, 2022 12:09
Show Gist options
  • Save serradura/f8881a5318b6a453cef99a18c3190eea to your computer and use it in GitHub Desktop.
Save serradura/f8881a5318b6a453cef99a18c3190eea to your computer and use it in GitHub Desktop.
UI (UI::Component)
# frozen_string_literal: true
# == Gemfile ==
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'u-test', '0.9.0'
gem 'nokogiri', '>= 1.5'
end
# == lib ==
module UI
module DataAccessor
def data=(value)
@data = value
end
def data; @data; end
def content
@data[:content] || @data['content'.freeze] if @data.respond_to?(:[])
end
end
end
module UI
require 'cgi'
require 'nokogiri/html'
class Component
def self.fn(&block)
new(block)
end
def self.call(data = {})
new.call(data)
end
def self.to_proc
-> (data = {}) { call(data) }
end
def self.template(&block)
define_method(:__template) { block }
end
def self.defaults(data = {})
define_method(:__defaults) { data }
end
def initialize(fn = nil)
@fn = fn || __template
@defaults = respond_to?(:__defaults) ? __defaults : {}
end
def call(data = nil)
CGI.unescapeHTML(build_html_with(data))
end
def to_proc
-> (data) { call(data) }
end
private
def build_html_with(data)
builder = Nokogiri::HTML::Builder.new
builder.context = builder
builder.extend(DataAccessor)
builder.data = apply_defaults_to(data)
builder.instance_eval(&@fn)
builder.to_html.sub!(/<!DOCTYPE.*[^>]/, '')
end
def apply_defaults_to(data)
return data || @defaults unless data.is_a?(Hash)
return @defaults.merge(data) if @defaults.is_a?(Hash)
data.empty? ? @defaults : data
end
end
end
module UI
def self.component(&block)
UI::Component.fn(&block)
end
end
# == test ==
class UITest < Microtest::Test
class HelloWorld < UI::Component
template do
span.bar.foo! {
text("Hello World")
}
end
end
def test_hello_world_component
expected = "<span class=\"bar\" id=\"foo\">Hello World</span>\n"
assert expected == HelloWorld.call
end
class Greet < UI::Component
defaults name: 'John Doe'
template do
span.bar.foo! {
text("Hello #{data[:name]}")
}
end
end
def test_greet_component
expected_default_name = "<span class=\"bar\" id=\"foo\">Hello John Doe</span>\n"
assert(expected_default_name == Greet.call)
expected_rodrigo_name = "<span class=\"bar\" id=\"foo\">Hello Rodrigo</span>\n"
assert(expected_rodrigo_name == Greet.call(name: 'Rodrigo'))
expected_collection = [expected_default_name, expected_rodrigo_name]
assert(expected_collection == [{}, {name: 'Rodrigo'}].map(&Greet))
end
class Number < UI::Component
defaults 0
template do
strong { text(data) }
end
end
def test_number_component
expected_number_zero = "<strong>0</strong>\n"
assert(expected_number_zero == Number.call)
expected_number_one = "<strong>1</strong>\n"
assert(expected_number_one == Number.call(1))
expected_collection = [expected_number_zero, expected_number_one]
assert(expected_collection == [nil, 1].map(&Number))
end
def test_fn_component
number = UI::Component.fn do
strong { text(data) }
end
expected_number_one = "<strong>1</strong>\n"
assert(expected_number_one == number.call(1))
assert([expected_number_one] == [1].map(&number))
end
def test_component_composition
container = UI.component { div data }
number = UI.component do
strong { text(content) }
end
assert("<div><strong>1</strong>\n</div>\n" == container.call(number.(content: 1)))
assert(["<div><strong>1</strong>\n</div>\n"] == [content: 1].map(&number).map(&container))
end
end
# == test runner ==
Microtest.call
# == ideas ==
# -- Feature: global aliases --
class UI::Button < UI::Component
set name: :button, global: true
template { button { data } }
end
class UI::Button1 < UI::Component
global_name :button
template { button { data } }
end
class UI::Button2 < UI::Component
template { button.btn2 { data } }
end
UI::Components.add :button2, UI::Button2
UI::Components.add :button3 do
template { button.btn3! { data } }
end
class UI::Button4 < UI::Component
set name: :button4
template { button { data } }
end
UI::Components.add UI::Button4
UI.button.call('My button') # => "<button>My Button</button>\n"
UI.button1.call('My button') # => "<button>My Button</button>\n"
UI.button2.call('My button') # => "<button class=\"btn2\">My Button</button>\n"
UI.button3.call('My button') # => "<button id=\"btn3\">My Button</button>\n"
UI.button4.call('My button') # => "<button>My Button</button>\n"
UI.button { 'My button' } # => "<button>My Button</button>\n"
UI.button1 { 'My button' } # => "<button>My Button</button>\n"
UI.button2 { 'My button' } # => "<button class=\"btn2\">My Button</button>\n"
UI.button3 { 'My button' } # => "<button id=\"btn3\">My Button</button>\n"
UI.button4 { 'My button' } # => "<button>My Button</button>\n"
# -- Feature: Namespace --
class UI::Button5 < UI::Component
namespace :foo
set name: :button5
template { button { data } }
end
# class UI::Button5 < UI::Component
# set name: :button5, namespace: :foo
# template { button { data } }
# end
UI.button5 # => raises NoMethodError
UI[:foo].button5.call('My button') # => "<button>My Button</button>\n"
UI[:foo].button5 { 'My button' } # => "<button>My Button</button>\n"
foo_components = UI[:foo]
foo_components.button5 # => "<button>My Button</button>\n"
# -- Feature: SETUP --
class UI::Hello1 < UI::Component
template { span data[:name] }
setup do |data|
data[:name] = upcase(data[:name])
end
def upcase(value)
String(value).upcase
end
end
hello1 = UI::Hello1.new
hello1.call name: :foo # => "<span>FOO</span>\n"
hello1.upcase :foo # => 'FOO'
class UI::Hello2 < UI::Component
template { span data[:name] }
def setup(data)
data[:name] = upcase(data[:name])
end
private
def upcase(value)
String(value).upcase
end
end
hello2 = UI::Hello2.new
hello2.call name: :foo # => "<span>FOO</span>\n"
hello2.upcase :foo # NoMethodError (private method)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment