turbo_boost-commands 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.

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
@@ -16,7 +16,7 @@ import uuids from './uuids'
16
16
  function buildCommandPayload (id, element) {
17
17
  return {
18
18
  id, // uniquely identifies the command
19
- name: element.dataset.command,
19
+ name: element.getAttribute(schema.commandAttribute),
20
20
  elementId: element.id.length > 0 ? element.id : null,
21
21
  elementAttributes: elements.buildAttributePayload(element),
22
22
  startedAt: new Date().getTime()
@@ -32,7 +32,7 @@ function invokeCommand (event) {
32
32
  if (!element) return
33
33
  if (!delegates.isRegisteredForElement(event.type, element)) return
34
34
 
35
- const commandId = `command-${uuids.v4()}`
35
+ const commandId = `turbo-command-${uuids.v4()}`
36
36
  let driver = drivers.find(element)
37
37
  let payload = {
38
38
  ...buildCommandPayload(commandId, element),
@@ -1,7 +1,7 @@
1
1
  const schema = {
2
2
  frameAttribute: 'data-turbo-frame',
3
3
  methodAttribute: 'data-turbo-method',
4
- commandAttribute: 'data-command'
4
+ commandAttribute: 'data-turbo-command'
5
5
  }
6
6
 
7
7
  export default { ...schema }
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
data/fly.toml CHANGED
@@ -1,11 +1,13 @@
1
- # fly.toml file generated for turbo-boost-commands on 2022-09-19T05:16:01-06:00
1
+ # fly.toml file generated for turboboost-commands on 2022-12-23T12:55:35-07:00
2
2
 
3
- app = "turbo-boost-commands"
3
+ app = "turboboost-commands"
4
4
  kill_signal = "SIGINT"
5
5
  kill_timeout = 5
6
6
  processes = []
7
7
 
8
8
  [env]
9
+ # Must match the proxy config in the smallteam-tech/proxy repo
10
+ RAILS_RELATIVE_URL_ROOT = "/@turbo-boost/commands"
9
11
 
10
12
  [experimental]
11
13
  allowed_public_ports = []
@@ -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"
@@ -77,12 +77,14 @@ class TurboBoost::Commands::Runner
77
77
 
78
78
  def command_class_name
79
79
  return nil unless command_requested?
80
- command_name.split("#").first
80
+ name = command_name.split("#").first
81
+ name << "Command" unless name.end_with?("Command")
82
+ name
81
83
  end
82
84
 
83
85
  def command_method_name
84
86
  return nil unless command_requested?
85
- return "noop" unless command_name.include?("#")
87
+ return "perform" unless command_name.include?("#")
86
88
  command_name.split("#").last
87
89
  end
88
90
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TurboBoost
4
4
  module Commands
5
- VERSION = "0.0.1"
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.0",
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.1"
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
  }