Last active
April 28, 2022 12:09
-
-
Save serradura/f8881a5318b6a453cef99a18c3190eea to your computer and use it in GitHub Desktop.
UI (UI::Component)
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
# 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