Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use String-based template rendering only #5

Merged
merged 3 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ require:
AllCops:
TargetRubyVersion: 3.0
NewCops: enable
Exclude:
- lib/lifeform/phlex_renderable.rb

Lint/MissingSuper:
Enabled: false
Expand Down
22 changes: 13 additions & 9 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ PATH
remote: .
specs:
lifeform (0.11.0)
activesupport (>= 7.0)
phlex (>= 1.7)
hash_with_dot_access (>= 1.2)
sequel (>= 5.72)
serbea (>= 2.0)
zeitwerk (~> 2.5)

GEM
Expand All @@ -18,13 +19,14 @@ GEM
ast (2.4.2)
backport (1.2.0)
benchmark (0.2.1)
bigdecimal (3.1.4)
builder (3.2.4)
cgi (0.3.6)
concurrent-ruby (1.2.2)
diff-lcs (1.5.0)
e2mmap (0.1.0)
erb (4.0.2)
cgi (>= 0.3.3)
erubi (1.12.0)
hash_with_dot_access (1.2.0)
activesupport (>= 5.0.0, < 8.0)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
jaro_winkler (1.5.6)
Expand All @@ -47,10 +49,6 @@ GEM
parser (3.2.2.3)
ast (~> 2.4.1)
racc
phlex (1.8.1)
concurrent-ruby (~> 1.2)
erb (>= 4)
zeitwerk (~> 2.6)
racc (1.7.1)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
Expand Down Expand Up @@ -78,6 +76,12 @@ GEM
rubocop-rake (0.6.0)
rubocop (~> 1.0)
ruby-progressbar (1.13.0)
sequel (5.74.0)
bigdecimal
serbea (2.0.0)
activesupport (>= 6.0)
erubi (>= 1.10)
tilt (~> 2.0)
solargraph (0.45.0)
backport (~> 1.2)
benchmark
Expand Down
13 changes: 1 addition & 12 deletions lib/lifeform.rb
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
# frozen_string_literal: true

require "phlex"
require "active_support/core_ext/string/output_safety"

require "serbea/pipeline"
require "zeitwerk"
loader = Zeitwerk::Loader.for_gem
loader.setup

module Lifeform
class Error < StandardError; end

module RefineProcToString
refine Proc do
def to_s
call.to_s
end
end
end
end

if defined?(Bridgetown)
# Check compatibility
raise "The Lifeform support for Bridgetown requires v1.2 or newer" if Bridgetown::VERSION.to_f < 1.2

Bridgetown.initializer :lifeform do # |config|
require "lifeform/phlex_renderable" unless Phlex::HTML.instance_methods.include?(:render_in)
end
end
17 changes: 0 additions & 17 deletions lib/lifeform/capturing_renderable.rb

This file was deleted.

51 changes: 29 additions & 22 deletions lib/lifeform/form.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# frozen_string_literal: true

require "active_support/core_ext/string/inflections"
require "active_support/ordered_options"
require "hash_with_dot_access"

module Lifeform
FieldDefinition = Struct.new(:type, :library, :parameters)

# A form object which stores field definitions and can be rendered as a component
class Form < Phlex::HTML # rubocop:todo Metrics/ClassLength
include CapturingRenderable
class Form # rubocop:todo Metrics/ClassLength
include Lifeform::Renderable
extend Sequel::Inflections

MODEL_PATH_HELPER = :polymorphic_path

class << self
Expand All @@ -20,7 +21,7 @@ def inherited(subclass)
# Helper to point to `I18n.t` method
def t(...) = I18n.t(...)

def configuration = @configuration ||= ActiveSupport::OrderedOptions.new
def configuration = @configuration ||= HashWithDotAccess::Hash.new

# @param block [Proc, nil]
# @return [Hash<Symbol, FieldDefinition>]
Expand All @@ -41,7 +42,7 @@ def subforms = @subforms ||= {}

def field(name, type: :text, library: self.library, **parameters)
parameters[:name] = name.to_sym
fields[name] = FieldDefinition.new(type, Libraries.const_get(library.to_s.classify), parameters)
fields[name] = FieldDefinition.new(type, Libraries.const_get(camelize(library)), parameters)
end

def subform(name, klass, parent_name: nil)
Expand Down Expand Up @@ -92,7 +93,7 @@ def name_of_model(model)
model.to_model.model_name.param_key
else
# Or just use basic underscore
model.class.name.underscore.tr("/", "_")
underscore(model.class.name).tr("/", "_")
end
end

Expand Down Expand Up @@ -126,7 +127,7 @@ def initialize( # rubocop:disable Metrics/ParameterLists
)
@model, @url, @library_name, @parameters, @emit_form_tag, @parent_name =
model, url, library, parameters, emit_form_tag, parent_name
@library = Libraries.const_get(@library_name.to_s.classify)
@library = Libraries.const_get(self.class.send(:camelize, @library_name))
@subform_instances = {}

self.class.initialize_field_definitions!
Expand All @@ -139,15 +140,13 @@ def initialize( # rubocop:disable Metrics/ParameterLists
def verify_method
return if %w[get post].include?(parameters[:method].to_s.downcase)

@method_tag = Class.new(Phlex::HTML) do # TODO: break this out into a real component
def initialize(method:)
@method = method
end
method_value = @parameters[:method].to_s.downcase

def template
input type: "hidden", name: "_method", value: @method, autocomplete: "off"
end
end.new(method: @parameters[:method].to_s.downcase)
@method_tag = -> {
<<~HTML
<input type="hidden" name="_method" #{attribute_segment :value, method_value} autocomplete="off">
HTML
}

parameters[:method] = :post
end
Expand Down Expand Up @@ -191,13 +190,21 @@ def template(&block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComple
form_tag = library::FORM_TAG
parameters[:action] ||= url || (model ? helpers.send(self.class.const_get(:MODEL_PATH_HELPER), model) : nil)

send(form_tag, **attributes) do
unsafe_raw(add_authenticity_token) unless parameters[:method].to_s.downcase == "get"
unsafe_raw @method_tag&.() || ""
block ? yield_content(&block) : auto_render_fields
end
html -> {
<<~HTML
<#{form_tag}#{attrs -> { attributes }}>
#{add_authenticity_token unless parameters[:method].to_s.downcase == "get"}
#{@method_tag&.() || ""}
#{block ? capture(self, &block) : auto_render_fields}
</#{form_tag}>
HTML
}
end

def auto_render_fields = self.class.fields.map { |k, _v| render(field(k)) }
def auto_render_fields = html_map(self.class.fields) { |k, _v| render(field(k)) }

def render(field_object)
field_object.render_in(helpers || self)
end
end
end
95 changes: 95 additions & 0 deletions lib/lifeform/helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# frozen_string_literal: true

require "sequel/model/default_inflections"
require "sequel/model/inflections"

module Lifeform
module Helpers
def attributes_from_options(options)
segments = []
options.each do |attr, option|
attr = dashed(attr)
if option.is_a?(Hash)
option = option.transform_keys { |key| "#{attr}-#{dashed(key)}" }
segments << attributes_from_options(option)
else
segments << attribute_segment(attr, option)
end
end
segments.join(" ")
end

# Covert an underscored value into a dashed string.
#
# @example "foo_bar_baz" => "foo-bar-baz"
#
# @param value [String|Symbol]
# @return [String]
def dashed(value)
value.to_s.tr("_", "-")
end

# Create an attribute segment for a tag.
#
# @param attr [String] the HTML attribute name
# @param value [String] the attribute value
# @return [String]
def attribute_segment(attr, value)
"#{attr}=#{value.to_s.encode(xml: :attr)}"
end

def attrs(callback)
attrs_string = attributes_from_options(callback.() || {})

attrs_string = " #{attrs_string}" unless attrs_string.blank?

attrs_string
end

# Below is verbatim copied over from Bridgetown
# TODO: extract both out to a shared gem

module PipeableProc
include Serbea::Pipeline::Helper

attr_accessor :pipe_block, :touched

def pipe(&block)
return super(self.(), &pipe_block) if pipe_block && !block

self.touched = true
return self unless block

tap { _1.pipe_block = block }
end

def to_s
return self.().to_s if touched

super
end

def encode(...)
to_s.encode(...)
end
end

Proc.prepend(PipeableProc) unless defined?(Bridgetown::HTMLinRuby::PipeableProc)

def text(callback)
(callback.is_a?(Proc) ? html(callback) : callback).to_s.then do |str|
next str if str.respond_to?(:html_safe) && str.html_safe?

str.encode(xml: :attr).gsub(/\A"|"\Z/, "")
end
end

def html(callback)
callback.pipe
end

def html_map(input, &callback)
input.map(&callback).join
end
end
end
2 changes: 1 addition & 1 deletion lib/lifeform/libraries/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class Default
# @param attributes [Hash]
# @return [Input]
def self.object_for_field_definition(form, field_definition, attributes)
type_classname = field_definition[:type].to_s.classify
type_classname = Lifeform::Form.send(:camelize, field_definition[:type])
if const_defined?(type_classname)
const_get(type_classname)
else
Expand Down
35 changes: 19 additions & 16 deletions lib/lifeform/libraries/default/button.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,14 @@
module Lifeform
module Libraries
class Default
class Button < Phlex::HTML
using RefineProcToString
include CapturingRenderable
class Button
include Lifeform::Renderable

attr_reader :form, :field_definition, :attributes

WRAPPER_TAG = :form_button
BUTTON_TAG = :button

register_element WRAPPER_TAG

def initialize(form, field_definition, **attributes)
@form = form
@field_definition = field_definition
Expand All @@ -23,21 +20,27 @@ def initialize(form, field_definition, **attributes)
@attributes[:type] ||= "button"
end

def template(&block)
return if [email protected]? && !@if
def template(&block) # rubocop:disable Metrics/AbcSize
return "" if [email protected]? && !@if

wrapper_tag = dashed self.class.const_get(:WRAPPER_TAG)
button_tag = dashed self.class.const_get(:BUTTON_TAG)

wrapper_tag = self.class.const_get(:WRAPPER_TAG)
button_tag = self.class.const_get(:BUTTON_TAG)
label_text = block ? capture(self, &block) : @label.is_a?(Proc) ? @label.pipe : @label # rubocop:disable Style/NestedTernaryOperator

field_body = proc {
send(button_tag, **@attributes) do
unsafe_raw(@label.to_s) unless block
yield_content(&block)
end
field_body = html -> {
<<-HTML
<#{button_tag}#{attrs -> { @attributes }}>#{text -> { label_text }}</#{button_tag}>
HTML
}
return field_body.() unless wrapper_tag

send wrapper_tag, name: @attributes[:name], &field_body
return field_body unless wrapper_tag

html -> {
<<-HTML
<#{wrapper_tag}#{attrs -> { { name: @attributes[:name] } }}>#{field_body}</#{wrapper_tag}>
HTML
}
end
end
end
Expand Down
Loading
Loading