Skip to content

Instantly share code, notes, and snippets.

@frank-who
Last active March 19, 2021 11:35
Show Gist options
  • Save frank-who/6f083f84e6f2daea420b9d4b128d7934 to your computer and use it in GitHub Desktop.
Save frank-who/6f083f84e6f2daea420b9d4b128d7934 to your computer and use it in GitHub Desktop.
Stimulus slim view helpers
# app/helpers/application_helper.rb
module ApplicationHelper
include StimulusHelper::HelperMethods
end
div*stim(:search).controller(data_map: { url: request.url })
= simple_form_for :search,
url: search_path,
remote: true,
html: \
stim(:search) \
.target(:searchForm) \
.ajax(before: :showLoader, success: :showResults, error: :showError) \
.html(class: 'o-form') do |f|
= f.input :q,
input_html: stim(:search) \
.target(:input) \
.focus(:focusSearchInput) \
.html(class: 'a-input -search')
= f.button :button, 'Search', class: 'a-btn -hollow'
class StimulusHelper < BasicObject
module HelperMethods
def stim(controller)
helper_class = "#{controller}_stimulus_helper"
if !File.exists?(Rails.root.join('app', 'lib', 'stimulus_helpers', "#{helper_class}.rb"))
helper_class = 'stimulus_helper'
end
instance_variable_get(:"@_stimulus_#{controller}") ||
instance_variable_set(:"@_stimulus_#{controller}", helper_class.classify.constantize.new(controller))
end
def stims(*list)
return if list.nil?
result = Hash.new()
list.each { |h| h.each { |k, v| Array(result[k] ||= []).push(v) } }
result.transform_values { |v| v.size == 1 ? v.first : v }
end
end
attr_reader \
:identifier,
:attr_hash
def initialize(identifier, attr_hash={})
@identifier = identifier
@attr_hash = attr_hash
end
def controller(html_attrs={})
self_class.new(identifier, attr_hash.merge(controller_hash(html_attrs)))
end
def target(targets, html_attrs={})
self_class.new(identifier, attr_hash.merge(target_hash(targets, html_attrs)))
end
def target2(targets, html_attrs={})
self_class.new(identifier, attr_hash.merge(target2_hash(targets, html_attrs)))
end
def html(html_attrs)
self_class.new(identifier, attr_hash.merge(html_attrs))
end
def ajax(actions)
actions = actions.map { |k, v| ["ajax:#{k}".to_sym, v] }.to_h
self_class.new(identifier, attr_hash.merge(action_hash(actions)))
end
def action(actions)
self_class.new(identifier, attr_hash.merge(action_hash(actions)))
end
def blur(actions)
self_class.new(identifier, attr_hash.merge(action_hash(blur: actions)))
end
def click(actions)
self_class.new(identifier, attr_hash.merge(action_hash(click: actions)))
end
def change(actions)
self_class.new(identifier, attr_hash.merge(action_hash(change: actions)))
end
def focus(actions)
self_class.new(identifier, attr_hash.merge(action_hash(focus: actions)))
end
def input(actions)
self_class.new(identifier, attr_hash.merge(action_hash(input: actions)))
end
def keydown(actions)
self_class.new(identifier, attr_hash.merge(action_hash(keydown: actions)))
end
def keyup(actions)
self_class.new(identifier, attr_hash.merge(action_hash(keyup: actions)))
end
private
def self_class
(class << self; self end).superclass
end
def method_missing(*args, &block)
reload! unless loaded?
@target_hash.send(*args, &block)
end
def loaded?
!!@loaded
end
def reload!
@target_hash = attr_hash
@loaded = true
end
# ==== Hash builders
def controller_string
identifier.to_s.dasherize
end
def controller_hash(html_attrs={})
data_map = controller_attributes_for(:data_map, html_attrs: html_attrs) # TODO: Deprecate: Progressively enhance data_map to populate values
values = controller_attributes_for(:values, suffix: '-value', html_attrs: html_attrs)
classes = controller_attributes_for(:classes, suffix: '-class', html_attrs: html_attrs)
{ 'data-controller': controller_string }
.merge(html_attrs || {})
.merge(data_map)
.merge(values)
.merge(classes)
end
def target_string(target)
if target.to_s.match?(/\./)
target
else
[controller_string, target.to_s.camelize(:lower)].join('.')
end
end
def target_hash(targets, html_attrs={})
if targets.blank?
{}
else
targets = [targets].flatten.map { |t| target_string(t) }.join(' ')
{ 'data-target': targets }.merge(html_attrs || {})
end
end
def target2_string(target)
target.to_s.camelize(:lower)
end
def target2_hash(targets, html_attrs={})
if targets.blank?
{}
else
targets = [targets].flatten.map { |t| target2_string(t) }.join(' ')
{ "data-#{controller_string}-target": targets }.merge(html_attrs || {})
end
end
def action_string(event, action)
return if action.blank?
if action.is_a?(::Array)
action.map { |a| [event, [controller_string, a.to_s.camelize(:lower)].join('#')].join('->') }.join(' ')
else
[event, [controller_string, action.to_s.camelize(:lower)].join('#')].join('->')
end
end
def action_hash(actions)
if actions.blank?
{}
else
{ 'data-action': actions.map { |k, v| action_string(k, v) }.join(' ') }
end
end
def controller_attributes_for(key, suffix: nil, html_attrs: {})
[html_attrs.delete(key) || {}].each_with_object({}) do |i, h|
i.each { |k, v| h["data-#{controller_string}-#{k.to_s.underscore.dasherize}#{suffix}".to_sym] = v }
end
end
end
require 'rails_helper'
SingleCov.covered!
RSpec.describe StimulusHelper do
subject { described_class.new(:folder__controller) }
describe 'full usage' do
let(:call) do
call = subject
.controller(data_map: { id: 123 }, classes: { will_hide: 'u-hide' }, values: { item_id: 123 }, id: 'element')
.target(:moving_target, class: 'css2')
.target2(:moving_target, class: 'css2')
.action(click: :doSomething)
.html(role: 'user')
end
it do
expect(call.keys.map(&:to_s).sort).to match_array(
%w[
class
data-action
data-controller
data-folder--controller-id
data-folder--controller-target
data-folder--controller-will-hide-class
data-folder--controller-item-id-value
data-target
id
role
]
)
end
it { expect(call[:'data-controller']).to eq('folder--controller') }
it { expect(call[:'data-target']).to eq('folder--controller.movingTarget') }
it { expect(call[:'data-folder--controller-target']).to eq('movingTarget') }
it { expect(call[:'data-action']).to eq('click->folder--controller#doSomething') }
it { expect(call[:'data-folder--controller-id']).to eq(123) }
it { expect(call[:'data-folder--controller-will-hide-class']).to eq('u-hide') }
it { expect(call[:'data-folder--controller-item-id-value']).to eq(123) }
it { expect(call[:class]).to eq('css2') }
it { expect(call[:id]).to eq('element') }
it { expect(call[:role]).to eq('user') }
end
describe ".target('')" do
it { expect(subject.target('').to_h).to eq({}) }
end
describe ".target2('')" do
it { expect(subject.target2('').to_h).to eq({}) }
end
describe ".action('')" do
it { expect(subject.action('').to_h).to eq({}) }
end
describe '.ajax' do
it { expect(subject.ajax(success: :doSomething, complete: :doSomethingElse).to_h).to eq({ 'data-action': 'ajax:success->folder--controller#doSomething ajax:complete->folder--controller#doSomethingElse' }) }
end
describe '.click' do
it { expect(subject.click(:do_something).to_h).to eq({ 'data-action': 'click->folder--controller#doSomething' }) }
end
describe '.change' do
it { expect(subject.change(:do_something).to_h).to eq({ 'data-action': 'change->folder--controller#doSomething' }) }
end
describe '.focus' do
it { expect(subject.focus(:do_something).to_h).to eq({ 'data-action': 'focus->folder--controller#doSomething' }) }
end
describe '.input' do
it { expect(subject.input(:do_something).to_h).to eq({ 'data-action': 'input->folder--controller#doSomething' }) }
end
describe '.keydown' do
it { expect(subject.keydown(:do_something).to_h).to eq({ 'data-action': 'keydown->folder--controller#doSomething' }) }
end
describe '.keyup' do
it { expect(subject.keyup(%i[do_something another]).to_h).to eq({ 'data-action': 'keyup->folder--controller#doSomething keyup->folder--controller#another' }) }
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment