turbo_boost-commands 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of turbo_boost-commands might be problematic. Click here for more details.

@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboBoost::Commands::ApplicationHelper
4
+ end
data/bin/standardize CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  bundle exec magic_frozen_string_literal
4
4
  bundle exec standardrb --fix
5
- yarn run prettier-standard "app/javascript/**/*.js"
5
+ yarn run prettier-standard package.json app/javascript/**/*.js
6
6
  yarn run rustywind --write test/dummy/app
7
7
  yarn run rustywind --write --custom-regex "(:\s[\"'])(.+)[\"']" test/dummy/app/views/_tailwind.yml.erb
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboBoost::Commands::AttributeHydration
4
+ extend self
5
+
6
+ def hydrate(value)
7
+ case value
8
+ when Array
9
+ value.map { |val| hydrate(val) }
10
+ when Hash
11
+ value.each_with_object(HashWithIndifferentAccess.new) do |(key, val), memo|
12
+ memo[key] = hydrate(val)
13
+ end
14
+ when String
15
+ parsed_value = parse_json(value)
16
+ hydrated_value = hydrate(parsed_value) unless parsed_value.nil?
17
+ hydrated_value ||= GlobalID::Locator.locate_signed(value) if possible_sgid_string?(value)
18
+ hydrated_value || value
19
+ else
20
+ value
21
+ end
22
+ rescue => error
23
+ Rails.logger.error "Failed to hydrate value! #{value}; #{error}"
24
+ value
25
+ end
26
+
27
+ def dehydrate(value)
28
+ return value unless has_sgid?(value)
29
+ case value
30
+ when Array
31
+ value.map { |val| dehydrate val }
32
+ when Hash
33
+ value.each_with_object(HashWithIndifferentAccess.new) do |(key, val), memo|
34
+ val = dehydrate(val)
35
+ memo[key] = convert_to_json?(key, val) ? val.to_json : val
36
+ end
37
+ else
38
+ implements_sgid?(value) ? value.to_sgid_param : value
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ # simple regular expressions checked before attempting specific hydration strategies
45
+ PREFIX_ATTRIBUTE_REGEXP = /\Aaria|data\z/i
46
+ JSON_REGEX = /.*(\[|\{).*(\}|\]).*/
47
+ SGID_PARAM_REGEX = /.{100,}/i
48
+
49
+ # Rails implicitly converts certain keys to JSON,
50
+ # so we check keys before performing JSON conversion
51
+ def prefixed_attribute?(name)
52
+ PREFIX_ATTRIBUTE_REGEXP.match? name.to_s
53
+ end
54
+
55
+ def convert_to_json?(key, value)
56
+ return false if prefixed_attribute?(key)
57
+ case value
58
+ when Array, Hash then true
59
+ else false
60
+ end
61
+ end
62
+
63
+ def possible_json_string?(value)
64
+ return false unless value.is_a?(String)
65
+ JSON_REGEX.match? value
66
+ end
67
+
68
+ def possible_sgid_string?(value)
69
+ return false unless value.is_a?(String)
70
+ SGID_PARAM_REGEX.match? value
71
+ end
72
+
73
+ def implements_sgid?(value)
74
+ value.respond_to?(:to_sgid_param) && value.try(:persisted?)
75
+ end
76
+
77
+ def find_sgid_value(value)
78
+ case value
79
+ when Array then value.find { |val| find_sgid_value val }
80
+ when Hash then find_sgid_value(value.values)
81
+ else implements_sgid?(value) ? value : nil
82
+ end
83
+ end
84
+
85
+ def has_sgid?(value)
86
+ find_sgid_value(value).present?
87
+ end
88
+
89
+ def parse_json(value)
90
+ return nil unless possible_json_string?(value)
91
+ JSON.parse value
92
+ rescue
93
+ nil
94
+ end
95
+ end
@@ -1,18 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "attribute_hydration"
4
+
3
5
  class TurboBoost::Commands::AttributeSet
4
- def initialize(prefix, attributes: {})
6
+ include TurboBoost::Commands::AttributeHydration
7
+
8
+ # These methods enable Ruby pattern matching
9
+ delegate :deconstruct, :deconstruct_keys, to: :to_h
10
+
11
+ def initialize(attributes = {}, prefix: nil)
5
12
  prefix = prefix.to_s
6
- attrs = attributes.to_h.transform_values(&:to_s)
13
+ attrs = hydrate(attributes)
7
14
 
8
15
  attrs.each do |key, value|
9
16
  key = key.to_s.strip
10
17
 
11
- next unless key.start_with?(prefix)
18
+ next unless prefix.blank? || key.start_with?(prefix)
12
19
 
13
- name = key.parameterize.underscore.delete_prefix("#{prefix}_")
14
- value = value.to_i if value.to_s.match?(/\A\d+\z/)
20
+ name = key.parameterize.underscore
21
+ name.delete_prefix!("#{prefix}_") unless prefix.blank?
22
+
23
+ # type casting
24
+ value = value.to_i if value.is_a?(String) && value.match?(/\A\d+\z/)
15
25
  value = value == "true" if value.is_a?(String) && value.match?(/\A(true|false)\z/i)
26
+
16
27
  instance_variable_set "@#{name}", value
17
28
 
18
29
  next if orig_respond_to_missing?(name, false)
@@ -22,6 +33,23 @@ class TurboBoost::Commands::AttributeSet
22
33
  end
23
34
  end
24
35
 
36
+ def to_h
37
+ instance_variables.each_with_object({}.with_indifferent_access) do |name, memo|
38
+ value = instance_variable_get(name)
39
+ value = value.to_h if value.is_a?(self.class)
40
+ memo[name.to_s.delete_prefix("@").to_sym] = value
41
+ end
42
+ end
43
+
44
+ def render_options
45
+ options = {
46
+ partial: renders,
47
+ assigns: assigns,
48
+ locals: locals
49
+ }
50
+ options.deep_symbolize_keys
51
+ end
52
+
25
53
  def respond_to?(name, include_all = false)
26
54
  respond_to_missing? name, include_all
27
55
  end
@@ -8,18 +8,19 @@ require_relative "attribute_set"
8
8
  # Commands are executed via a before_action in the Rails controller lifecycle.
9
9
  # They have access to the following methods and properties.
10
10
  #
11
+ # * controller .................. The Rails controller processing the HTTP request
12
+ # * css_id_selector ............. Returns a CSS selector for an element `id` i.e. prefixes with `#`
11
13
  # * dom_id ...................... The Rails dom_id helper
12
14
  # * dom_id_selector ............. Returns a CSS selector for a dom_id
13
- # * controller .................. The Rails controller processing the HTTP request
14
15
  # * element ..................... A struct that represents the DOM element that triggered the command
15
16
  # * morph ....................... Appends a Turbo Stream to morph a DOM element
16
17
  # * params ...................... Commands specific params (frame_id, element, etc.)
17
18
  # * render ...................... Renders Rails templates, partials, etc. (doesn't halt controller request handling)
18
19
  # * render_response ............. Renders a full controller response
19
20
  # * renderer .................... An ActionController::Renderer
21
+ # * state ....................... An object that stores ephemeral `state`
20
22
  # * turbo_stream ................ A Turbo Stream TagBuilder
21
23
  # * turbo_streams ............... A list of Turbo Streams to append to the response (also aliased as streams)
22
- # * state ....................... An object that stores ephemeral `state`
23
24
  #
24
25
  # They also have access to the following class methods:
25
26
  #
@@ -27,6 +28,11 @@ require_relative "attribute_set"
27
28
  #
28
29
  class TurboBoost::Commands::Command
29
30
  class << self
31
+ def css_id_selector(id)
32
+ return id if id.to_s.start_with?("#")
33
+ "##{id}"
34
+ end
35
+
30
36
  def preventers
31
37
  @preventers ||= Set.new
32
38
  end
@@ -70,6 +76,7 @@ class TurboBoost::Commands::Command
70
76
  attr_reader :controller, :turbo_streams
71
77
  alias_method :streams, :turbo_streams
72
78
 
79
+ delegate :css_id_selector, to: :"self.class"
73
80
  delegate :dom_id, to: :"controller.view_context"
74
81
  delegate(
75
82
  :controller_action_prevented?,
@@ -86,7 +93,7 @@ class TurboBoost::Commands::Command
86
93
  end
87
94
 
88
95
  def dom_id_selector(...)
89
- "##{dom_id(...)}"
96
+ css_id_selector dom_id(...)
90
97
  end
91
98
 
92
99
  # Same method signature as ActionView::Rendering#render (i.e. controller.view_context.render)
@@ -105,12 +112,14 @@ class TurboBoost::Commands::Command
105
112
  ivars&.each { |key, value| controller.instance_variable_set "@#{key}", value }
106
113
  end
107
114
 
108
- def morph(selector, html)
115
+ def morph(html:, id: nil, selector: nil)
116
+ selector ||= css_id_selector(id)
109
117
  turbo_streams << turbo_stream.invoke("morph", args: [html], selector: selector)
110
118
  end
111
119
 
112
120
  # default command invoked when method not specified
113
- def noop
121
+ # can be overridden in subclassed commands
122
+ def perform
114
123
  end
115
124
 
116
125
  def params
@@ -119,11 +128,11 @@ class TurboBoost::Commands::Command
119
128
 
120
129
  def element
121
130
  @element ||= begin
122
- attributes = params[:element_attributes]
123
- OpenStruct.new attributes.merge(
124
- aria: TurboBoost::Commands::AttributeSet.new(:aria, attributes: attributes),
125
- dataset: TurboBoost::Commands::AttributeSet.new(:data, attributes: attributes)
126
- )
131
+ attributes = params[:element_attributes].to_h
132
+ TurboBoost::Commands::AttributeSet.new(attributes.merge(
133
+ aria: TurboBoost::Commands::AttributeSet.new(attributes, prefix: "aria"),
134
+ data: TurboBoost::Commands::AttributeSet.new(attributes, prefix: "data")
135
+ ))
127
136
  end
128
137
  end
129
138
 
@@ -3,6 +3,8 @@
3
3
  require_relative "runner"
4
4
 
5
5
  class TurboBoost::Commands::ControllerPack
6
+ include TurboBoost::Commands::AttributeHydration
7
+
6
8
  attr_reader :command, :controller, :runner
7
9
 
8
10
  delegate(
@@ -3,9 +3,11 @@
3
3
  require "turbo-rails"
4
4
  require "turbo_boost/streams"
5
5
  require_relative "version"
6
+ require_relative "patches"
6
7
  require_relative "command"
7
8
  require_relative "controller_pack"
8
9
  require_relative "../../../app/controllers/concerns/turbo_boost/commands/controller"
10
+ require_relative "../../../app/helpers/turbo_boost/commands/application_helper"
9
11
 
10
12
  module TurboBoost::Commands
11
13
  def self.config
@@ -24,8 +26,15 @@ module TurboBoost::Commands
24
26
  initializer "turbo_boost_commands.configuration" do
25
27
  Mime::Type.register "text/vnd.turbo-boost.html", :turbo_boost
26
28
 
27
- ActiveSupport.on_load(:action_controller_base) do
29
+ ActiveSupport.on_load :action_controller_base do
30
+ # `self` is ActionController::Base
28
31
  include TurboBoost::Commands::Controller
32
+ helper TurboBoost::Commands::ApplicationHelper
33
+ end
34
+
35
+ ActiveSupport.on_load :action_view do
36
+ # `self` is ActionView::Base
37
+ ActionView::Helpers::TagHelper::TagBuilder.prepend TurboBoost::Commands::Patches::ActionViewHelpersTagHelperTagBuilderPatch
29
38
  end
30
39
  end
31
40
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../attribute_hydration"
4
+
5
+ module TurboBoost::Commands::Patches::ActionViewHelpersTagHelperTagBuilderPatch
6
+ def tag_options(options, ...)
7
+ dehydrated_options = TurboBoost::Commands::AttributeHydration.dehydrate(options)
8
+ super(dehydrated_options, ...)
9
+ end
10
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboBoost::Commands::Patches
4
+ end
5
+
6
+ require_relative "patches/action_view_helpers_tag_helper_tag_builder_patch"
@@ -72,18 +72,19 @@ class TurboBoost::Commands::Runner
72
72
 
73
73
  def command_name
74
74
  return nil unless command_requested?
75
- # binding.pry
76
75
  command_params[:name]
77
76
  end
78
77
 
79
78
  def command_class_name
80
79
  return nil unless command_requested?
81
- command_name.split("#").first
80
+ name = command_name.split("#").first
81
+ name << "Command" unless name.end_with?("Command")
82
+ name
82
83
  end
83
84
 
84
85
  def command_method_name
85
86
  return nil unless command_requested?
86
- return "noop" unless command_name.include?("#")
87
+ return "perform" unless command_name.include?("#")
87
88
  command_name.split("#").last
88
89
  end
89
90
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TurboBoost
4
4
  module Commands
5
- VERSION = "0.0.2"
5
+ VERSION = "0.0.3"
6
6
  end
7
7
  end
data/package.json CHANGED
@@ -1,13 +1,24 @@
1
1
  {
2
2
  "name": "@turbo-boost/commands",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Commands to help you build robust reactive applications with Rails & Hotwire.",
5
- "main": "app/javascript/index.js",
5
+ "keywords": [
6
+ "hotwire",
7
+ "hotwired",
8
+ "rails",
9
+ "turbo",
10
+ "turbo-boost"
11
+ ],
12
+ "type": "module",
13
+ "main": "app/assets/builds/@turbo-boost/commands.js",
14
+ "files": [
15
+ "app/assets/builds"
16
+ ],
6
17
  "repository": "https://github.com/hopsoft/turbo_boost-commands",
7
18
  "author": "Nate Hopkins (hopsoft) <natehop@gmail.com>",
8
19
  "license": "MIT",
9
20
  "dependencies": {
10
- "@turbo-boost/streams": ">= 0.0.2"
21
+ "@turbo-boost/streams": ">= 0.0.4"
11
22
  },
12
23
  "peerDependencies": {
13
24
  "@hotwired/turbo-rails": ">= 7.2.0"
@@ -20,6 +31,6 @@
20
31
  "rustywind": "^0.15.1"
21
32
  },
22
33
  "scripts": {
23
- "build": "esbuild app/javascript/index.js --bundle --minify --sourcemap --format=esm --target=es2017 --analyze --outfile=app/assets/builds/@turbo-boost/commands.js"
34
+ "build": "esbuild app/javascript/index.js --bundle --minify --sourcemap --format=esm --target=es2020,chrome58,firefox57,safari11 --analyze --outfile=app/assets/builds/@turbo-boost/commands.js"
24
35
  }
25
36
  }