diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 4129c5cb..4d8382ce 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -14,3 +14,6 @@ Explain value.
- [ ] My code follows the style guidelines of this project
- [ ] Checks (StandardRB & Prettier-Standard) are passing
+- [ ] This is not a documentation update
+
+Please note that the best way to suggest changes or updates to the [documentation](https://docs.stimulusreflex.com) is to [join Discord](https://discord.gg/XveN625) and leave a note in the #docs channel. Any documentation updates posted as PRs cannot be accepted at this time. :heart:
diff --git a/.gitignore b/.gitignore
index 74b763db..a122a45d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@
tmp/
node_modules
.vscode
+/javascript/dist
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 00000000..df4fdf7a
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1 @@
+javascript/dist
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 79729ff3..fb7399c9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,27 +2,89 @@
## [Unreleased](https://github.com/hopsoft/stimulus_reflex/tree/HEAD)
-[Full Changelog](https://github.com/hopsoft/stimulus_reflex/compare/v3.3.0...HEAD)
+[Full Changelog](https://github.com/hopsoft/stimulus_reflex/compare/v3.4.0.pre3...HEAD)
-**Breaking changes:**
+**Merged pull requests:**
-- remove isolate concept and make behavior default [\#332](https://github.com/hopsoft/stimulus_reflex/pull/332) ([leastbad](https://github.com/leastbad))
+- Do not run sanity check on `rails generate stimulus\_reflex:config` [\#362](https://github.com/hopsoft/stimulus_reflex/pull/362) ([RolandStuder](https://github.com/RolandStuder))
+- xpath fix [\#360](https://github.com/hopsoft/stimulus_reflex/pull/360) ([leastbad](https://github.com/leastbad))
+
+## [v3.4.0.pre3](https://github.com/hopsoft/stimulus_reflex/tree/v3.4.0.pre3) (2020-11-11)
+
+[Full Changelog](https://github.com/hopsoft/stimulus_reflex/compare/v3.4.0.pre2...v3.4.0.pre3)
+
+**Merged pull requests:**
+
+- Allow to supress warnings for sanity checks [\#359](https://github.com/hopsoft/stimulus_reflex/pull/359) ([RolandStuder](https://github.com/RolandStuder))
+- serializeForm: only append given input if element is submit button [\#357](https://github.com/hopsoft/stimulus_reflex/pull/357) ([marcoroth](https://github.com/marcoroth))
+- Update package.json to 3.4.0-pre2 [\#356](https://github.com/hopsoft/stimulus_reflex/pull/356) ([marcoroth](https://github.com/marcoroth))
+- Fix elementToXPath import [\#355](https://github.com/hopsoft/stimulus_reflex/pull/355) ([julianrubisch](https://github.com/julianrubisch))
+- Add guard clause to return valid empty form data [\#354](https://github.com/hopsoft/stimulus_reflex/pull/354) ([julianrubisch](https://github.com/julianrubisch))
+- simplify xpath functions [\#353](https://github.com/hopsoft/stimulus_reflex/pull/353) ([leastbad](https://github.com/leastbad))
+- pass reflex id to reflex [\#352](https://github.com/hopsoft/stimulus_reflex/pull/352) ([joshleblanc](https://github.com/joshleblanc))
+
+## [v3.4.0.pre2](https://github.com/hopsoft/stimulus_reflex/tree/v3.4.0.pre2) (2020-11-06)
+
+[Full Changelog](https://github.com/hopsoft/stimulus_reflex/compare/v3.4.0.pre1...v3.4.0.pre2)
**Closed issues:**
+- Regression in version 3.4.0-pre1: Cannot find module `cable\_ready` [\#350](https://github.com/hopsoft/stimulus_reflex/issues/350)
+
+**Merged pull requests:**
+
+- move `cable\_ready` to development dependencies [\#351](https://github.com/hopsoft/stimulus_reflex/pull/351) ([marcoroth](https://github.com/marcoroth))
+
+## [v3.4.0.pre1](https://github.com/hopsoft/stimulus_reflex/tree/v3.4.0.pre1) (2020-11-03)
+
+[Full Changelog](https://github.com/hopsoft/stimulus_reflex/compare/v3.4.0.pre0...v3.4.0.pre1)
+
+## [v3.4.0.pre0](https://github.com/hopsoft/stimulus_reflex/tree/v3.4.0.pre0) (2020-11-02)
+
+[Full Changelog](https://github.com/hopsoft/stimulus_reflex/compare/v3.3.0...v3.4.0.pre0)
+
+**Implemented enhancements:**
+
+- Move StimulusReflex::Channel to app/ and allow for a configurable parent channel [\#346](https://github.com/hopsoft/stimulus_reflex/pull/346) ([leastbad](https://github.com/leastbad))
+- tab isolation mode v2 [\#335](https://github.com/hopsoft/stimulus_reflex/pull/335) ([leastbad](https://github.com/leastbad))
+- Delegate flash to the request [\#334](https://github.com/hopsoft/stimulus_reflex/pull/334) ([hopsoft](https://github.com/hopsoft))
+- Opt-in form serialization & params overriding [\#325](https://github.com/hopsoft/stimulus_reflex/pull/325) ([s-s](https://github.com/s-s))
+- Exit on failed sanity check, provide config to skip exit [\#318](https://github.com/hopsoft/stimulus_reflex/pull/318) ([RolandStuder](https://github.com/RolandStuder))
+
+**Fixed bugs:**
+
+- Console exception when reflex updates a page that didn't trigger the update [\#336](https://github.com/hopsoft/stimulus_reflex/issues/336)
+- AlpineJS components not reinitalised after reflex [\#329](https://github.com/hopsoft/stimulus_reflex/issues/329)
+- Encoding changes from UTF-8 to ASCII-8BIT [\#202](https://github.com/hopsoft/stimulus_reflex/issues/202)
+
+**Closed issues:**
+
+- ActionController::RoutingError with Rails 6 Engines [\#342](https://github.com/hopsoft/stimulus_reflex/issues/342)
+- Wrong input name parsing [\#321](https://github.com/hopsoft/stimulus_reflex/issues/321)
- Stimulus' controllers are not reconnecting after reflex, why? [\#314](https://github.com/hopsoft/stimulus_reflex/issues/314)
- Rendering issue [\#289](https://github.com/hopsoft/stimulus_reflex/issues/289)
- Documentation Request for a Rails 6.x app with 5.2 defaults [\#265](https://github.com/hopsoft/stimulus_reflex/issues/265)
**Merged pull requests:**
+- Fix serializeForm initialization [\#349](https://github.com/hopsoft/stimulus_reflex/pull/349) ([marcoroth](https://github.com/marcoroth))
+- \[docs\] StimulusReflex.debug= on left hand side [\#348](https://github.com/hopsoft/stimulus_reflex/pull/348) ([drnic](https://github.com/drnic))
+- Fix page morphs inside Rails engines [\#344](https://github.com/hopsoft/stimulus_reflex/pull/344) ([leastbad](https://github.com/leastbad))
+- Use Webpacker folder if available [\#343](https://github.com/hopsoft/stimulus_reflex/pull/343) ([coorasse](https://github.com/coorasse))
+- feat: create a more robust package.json [\#340](https://github.com/hopsoft/stimulus_reflex/pull/340) ([ParamagicDev](https://github.com/ParamagicDev))
+- Make StimulusReflex configurable and add an initializer [\#339](https://github.com/hopsoft/stimulus_reflex/pull/339) ([RolandStuder](https://github.com/RolandStuder))
+- Aliases method\_name to action\_name [\#338](https://github.com/hopsoft/stimulus_reflex/pull/338) ([obie](https://github.com/obie))
+- remove isolate concept and make behavior default [\#332](https://github.com/hopsoft/stimulus_reflex/pull/332) ([leastbad](https://github.com/leastbad))
- add signed/unsigned accessors to element [\#330](https://github.com/hopsoft/stimulus_reflex/pull/330) ([joshleblanc](https://github.com/joshleblanc))
- merge environment into ApplicationController and descendants [\#328](https://github.com/hopsoft/stimulus_reflex/pull/328) ([leastbad](https://github.com/leastbad))
+- Move form-data merge logic to the server-side [\#327](https://github.com/hopsoft/stimulus_reflex/pull/327) ([marcoroth](https://github.com/marcoroth))
- fix for PR\#317 which was preventing server messages [\#326](https://github.com/hopsoft/stimulus_reflex/pull/326) ([leastbad](https://github.com/leastbad))
- introduce tab isolation mode [\#324](https://github.com/hopsoft/stimulus_reflex/pull/324) ([leastbad](https://github.com/leastbad))
+- Force request encodings to be UTF-8 instead of ASCII-8BIT after a reflex [\#320](https://github.com/hopsoft/stimulus_reflex/pull/320) ([marcoroth](https://github.com/marcoroth))
- Append short section about resetting a form [\#319](https://github.com/hopsoft/stimulus_reflex/pull/319) ([julianrubisch](https://github.com/julianrubisch))
- lifecycle refactor: introduce new finalize stage, global reflexes dictionary [\#317](https://github.com/hopsoft/stimulus_reflex/pull/317) ([leastbad](https://github.com/leastbad))
- Update events.md [\#316](https://github.com/hopsoft/stimulus_reflex/pull/316) ([gahia](https://github.com/gahia))
+- Proposal: Reduce bundle size and add a bundler for Stimulus Reflex javascript [\#315](https://github.com/hopsoft/stimulus_reflex/pull/315) ([ParamagicDev](https://github.com/ParamagicDev))
## [v3.3.0](https://github.com/hopsoft/stimulus_reflex/tree/v3.3.0) (2020-09-22)
@@ -269,7 +331,7 @@
- Loosen Rails requirement to 5.2 with instructions [\#205](https://github.com/hopsoft/stimulus_reflex/pull/205) ([jasoncharnes](https://github.com/jasoncharnes))
- Fix undefined is not an object for Object.keys in log.js [\#201](https://github.com/hopsoft/stimulus_reflex/pull/201) ([marcoroth](https://github.com/marcoroth))
- Small typo/grammar fix in quickstart doc. [\#198](https://github.com/hopsoft/stimulus_reflex/pull/198) ([acoffman](https://github.com/acoffman))
-- Add halted lifecycle event [\#193](https://github.com/hopsoft/stimulus_reflex/pull/193) ([seb1441](https://github.com/seb1441))
+- Add halted lifecycle event [\#193](https://github.com/hopsoft/stimulus_reflex/pull/193) ([websebdev](https://github.com/websebdev))
- 147 extract multiple checkbox values [\#175](https://github.com/hopsoft/stimulus_reflex/pull/175) ([julianrubisch](https://github.com/julianrubisch))
## [v3.2.1](https://github.com/hopsoft/stimulus_reflex/tree/v3.2.1) (2020-05-09)
@@ -308,11 +370,11 @@
- Replace uuid4 dependency with function in repo [\#181](https://github.com/hopsoft/stimulus_reflex/pull/181) ([jonathan-s](https://github.com/jonathan-s))
- Allow channel exceptions to be rescuable [\#180](https://github.com/hopsoft/stimulus_reflex/pull/180) ([dark-panda](https://github.com/dark-panda))
- add console log messages for every reflex call [\#163](https://github.com/hopsoft/stimulus_reflex/pull/163) ([marcoroth](https://github.com/marcoroth))
-- add reflex callbacks [\#160](https://github.com/hopsoft/stimulus_reflex/pull/160) ([seb1441](https://github.com/seb1441))
+- add reflex callbacks [\#160](https://github.com/hopsoft/stimulus_reflex/pull/160) ([websebdev](https://github.com/websebdev))
**Fixed bugs:**
-- Pluralize the generated class name, so that will match with the file name [\#178](https://github.com/hopsoft/stimulus_reflex/pull/178) ([darkrubyist](https://github.com/darkrubyist))
+- Pluralize the generated class name, so that will match with the file name [\#178](https://github.com/hopsoft/stimulus_reflex/pull/178) ([dark88888](https://github.com/dark88888))
**Closed issues:**
@@ -446,6 +508,7 @@
**Implemented enhancements:**
- Reload session prior to each reflex accessing it [\#131](https://github.com/hopsoft/stimulus_reflex/pull/131) ([hopsoft](https://github.com/hopsoft))
+- tweak prettier-standard and add actions caching [\#125](https://github.com/hopsoft/stimulus_reflex/pull/125) ([andrewmcodes](https://github.com/andrewmcodes))
**Closed issues:**
@@ -495,7 +558,6 @@
**Implemented enhancements:**
-- tweak prettier-standard and add actions caching [\#125](https://github.com/hopsoft/stimulus_reflex/pull/125) ([andrewmcodes](https://github.com/andrewmcodes))
- More defense in the received handler [\#107](https://github.com/hopsoft/stimulus_reflex/pull/107) ([hopsoft](https://github.com/hopsoft))
**Fixed bugs:**
diff --git a/Gemfile.lock b/Gemfile.lock
index 95d00db5..a4c152e3 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,8 +1,7 @@
PATH
remote: .
specs:
- stimulus_reflex (3.3.0)
- cable_ready (>= 4.3.0)
+ stimulus_reflex (3.4.0.pre3)
nokogiri
rack
rails (>= 5.2)
@@ -11,56 +10,56 @@ PATH
GEM
remote: https://rubygems.org/
specs:
- actioncable (6.0.3.3)
- actionpack (= 6.0.3.3)
+ actioncable (6.0.3.4)
+ actionpack (= 6.0.3.4)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailbox (6.0.3.3)
- actionpack (= 6.0.3.3)
- activejob (= 6.0.3.3)
- activerecord (= 6.0.3.3)
- activestorage (= 6.0.3.3)
- activesupport (= 6.0.3.3)
+ actionmailbox (6.0.3.4)
+ actionpack (= 6.0.3.4)
+ activejob (= 6.0.3.4)
+ activerecord (= 6.0.3.4)
+ activestorage (= 6.0.3.4)
+ activesupport (= 6.0.3.4)
mail (>= 2.7.1)
- actionmailer (6.0.3.3)
- actionpack (= 6.0.3.3)
- actionview (= 6.0.3.3)
- activejob (= 6.0.3.3)
+ actionmailer (6.0.3.4)
+ actionpack (= 6.0.3.4)
+ actionview (= 6.0.3.4)
+ activejob (= 6.0.3.4)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (6.0.3.3)
- actionview (= 6.0.3.3)
- activesupport (= 6.0.3.3)
+ actionpack (6.0.3.4)
+ actionview (= 6.0.3.4)
+ activesupport (= 6.0.3.4)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actiontext (6.0.3.3)
- actionpack (= 6.0.3.3)
- activerecord (= 6.0.3.3)
- activestorage (= 6.0.3.3)
- activesupport (= 6.0.3.3)
+ actiontext (6.0.3.4)
+ actionpack (= 6.0.3.4)
+ activerecord (= 6.0.3.4)
+ activestorage (= 6.0.3.4)
+ activesupport (= 6.0.3.4)
nokogiri (>= 1.8.5)
- actionview (6.0.3.3)
- activesupport (= 6.0.3.3)
+ actionview (6.0.3.4)
+ activesupport (= 6.0.3.4)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
- activejob (6.0.3.3)
- activesupport (= 6.0.3.3)
+ activejob (6.0.3.4)
+ activesupport (= 6.0.3.4)
globalid (>= 0.3.6)
- activemodel (6.0.3.3)
- activesupport (= 6.0.3.3)
- activerecord (6.0.3.3)
- activemodel (= 6.0.3.3)
- activesupport (= 6.0.3.3)
- activestorage (6.0.3.3)
- actionpack (= 6.0.3.3)
- activejob (= 6.0.3.3)
- activerecord (= 6.0.3.3)
+ activemodel (6.0.3.4)
+ activesupport (= 6.0.3.4)
+ activerecord (6.0.3.4)
+ activemodel (= 6.0.3.4)
+ activesupport (= 6.0.3.4)
+ activestorage (6.0.3.4)
+ actionpack (= 6.0.3.4)
+ activejob (= 6.0.3.4)
+ activerecord (= 6.0.3.4)
marcel (~> 0.3.1)
- activesupport (6.0.3.3)
+ activesupport (6.0.3.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@@ -93,8 +92,8 @@ GEM
nio4r (2.5.4)
nokogiri (1.10.10)
mini_portile2 (~> 2.4.0)
- parallel (1.19.2)
- parser (2.7.1.4)
+ parallel (1.20.0)
+ parser (2.7.2.0)
ast (~> 2.4.1)
pry (0.12.2)
coderay (~> 1.1.0)
@@ -104,50 +103,51 @@ GEM
rack (2.2.3)
rack-test (1.1.0)
rack (>= 1.0, < 3)
- rails (6.0.3.3)
- actioncable (= 6.0.3.3)
- actionmailbox (= 6.0.3.3)
- actionmailer (= 6.0.3.3)
- actionpack (= 6.0.3.3)
- actiontext (= 6.0.3.3)
- actionview (= 6.0.3.3)
- activejob (= 6.0.3.3)
- activemodel (= 6.0.3.3)
- activerecord (= 6.0.3.3)
- activestorage (= 6.0.3.3)
- activesupport (= 6.0.3.3)
+ rails (6.0.3.4)
+ actioncable (= 6.0.3.4)
+ actionmailbox (= 6.0.3.4)
+ actionmailer (= 6.0.3.4)
+ actionpack (= 6.0.3.4)
+ actiontext (= 6.0.3.4)
+ actionview (= 6.0.3.4)
+ activejob (= 6.0.3.4)
+ activemodel (= 6.0.3.4)
+ activerecord (= 6.0.3.4)
+ activestorage (= 6.0.3.4)
+ activesupport (= 6.0.3.4)
bundler (>= 1.3.0)
- railties (= 6.0.3.3)
+ railties (= 6.0.3.4)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
- railties (6.0.3.3)
- actionpack (= 6.0.3.3)
- activesupport (= 6.0.3.3)
+ railties (6.0.3.4)
+ actionpack (= 6.0.3.4)
+ activesupport (= 6.0.3.4)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
rainbow (3.0.0)
rake (13.0.1)
redis (4.2.2)
- regexp_parser (1.7.1)
+ regexp_parser (1.8.2)
rexml (3.2.4)
- rubocop (0.89.1)
+ rubocop (1.2.0)
parallel (~> 1.10)
- parser (>= 2.7.1.1)
+ parser (>= 2.7.1.5)
rainbow (>= 2.2.2, < 4.0)
- regexp_parser (>= 1.7)
+ regexp_parser (>= 1.8)
rexml
- rubocop-ast (>= 0.3.0, < 1.0)
+ rubocop-ast (>= 1.0.1)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0)
- rubocop-ast (0.3.0)
- parser (>= 2.7.1.4)
- rubocop-performance (1.7.1)
- rubocop (>= 0.82.0)
+ rubocop-ast (1.1.1)
+ parser (>= 2.7.1.5)
+ rubocop-performance (1.8.1)
+ rubocop (>= 0.87.0)
+ rubocop-ast (>= 0.4.0)
ruby-progressbar (1.10.1)
sprockets (4.0.2)
concurrent-ruby (~> 1.0)
@@ -156,26 +156,27 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
- standard (0.5.2)
- rubocop (~> 0.89.1)
- rubocop-performance (~> 1.7.1)
+ standard (0.9.0)
+ rubocop (= 1.2.0)
+ rubocop-performance (= 1.8.1)
standardrb (1.0.0)
standard
thor (1.0.1)
thread_safe (0.3.6)
- tzinfo (1.2.7)
+ tzinfo (1.2.8)
thread_safe (~> 0.1)
unicode-display_width (1.7.0)
websocket-driver (0.7.3)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
- zeitwerk (2.4.0)
+ zeitwerk (2.4.1)
PLATFORMS
ruby
DEPENDENCIES
bundler (~> 2.0)
+ cable_ready (>= 4.3.0)
pry
pry-nav
rake
diff --git a/README.md b/README.md
index feb25693..af6f6e1f 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -54,7 +54,7 @@ And, it's fast.
It works seamlessly with the Rails tooling you already know and love.
-- Server-rendered HTML, delivered in miliseconds over the wire via Websockets
+- Server-rendered HTML, delivered in milliseconds over the wire via Websockets
- ERB templates and partials, with first-class [ViewComponent](https://github.com/github/view_component) support
- [Russian doll caching](https://edgeguides.rubyonrails.org/caching_with_rails.html#russian-doll-caching) and [ActiveJob](https://guides.rubyonrails.org/active_job_basics.html)
- [StimulusJS](https://stimulusjs.org/) and [Turbolinks](https://www.youtube.com/watch?v=SWEts0rlezA)
@@ -77,7 +77,7 @@ This project strives to live up to the vision outlined in [The Rails Doctrine](h
## 👩👩👧 Discord Community
-Please join over 600 of us on [Discord](https://discord.gg/XveN625) for support getting started, as well as active discussions around Rails, StimulusJS and CableReady.
+Please join over 700 of us on [Discord](https://discord.gg/XveN625) for support getting started, as well as active discussions around Rails, StimulusJS and CableReady.
![](https://img.shields.io/discord/629472241427415060)
diff --git a/lib/stimulus_reflex/channel.rb b/app/channels/stimulus_reflex/channel.rb
similarity index 85%
rename from lib/stimulus_reflex/channel.rb
rename to app/channels/stimulus_reflex/channel.rb
index a1082e69..bf784e4a 100644
--- a/lib/stimulus_reflex/channel.rb
+++ b/app/channels/stimulus_reflex/channel.rb
@@ -1,16 +1,6 @@
# frozen_string_literal: true
-module ApplicationCable
- class Channel < ActionCable::Channel::Base
- def initialize(connection, identifier, params = {})
- super
- application_channel = Rails.root.join("app", "channels", "application_cable", "channel.rb")
- require application_channel if File.exist?(application_channel)
- end
- end
-end
-
-class StimulusReflex::Channel < ApplicationCable::Channel
+class StimulusReflex::Channel < StimulusReflex.configuration.parent_channel.constantize
def stream_name
ids = connection.identifiers.map { |identifier| send(identifier).try(:id) || send(identifier) }
[
@@ -35,13 +25,21 @@ def receive(data)
reflex_name = reflex_name.end_with?("Reflex") ? reflex_name : "#{reflex_name}Reflex"
arguments = (data["args"] || []).map { |arg| object_with_indifferent_access arg }
element = StimulusReflex::Element.new(data)
- permanent_attribute_name = data["permanent_attribute_name"]
- params = data["params"] || {}
+ permanent_attribute_name = data["permanentAttributeName"]
+ form_data = Rack::Utils.parse_nested_query(data["formData"])
+ params = form_data.deep_merge(data["params"] || {})
begin
begin
reflex_class = reflex_name.constantize.tap { |klass| raise ArgumentError.new("#{reflex_name} is not a StimulusReflex::Reflex") unless is_reflex?(klass) }
- reflex = reflex_class.new(self, url: url, element: element, selectors: selectors, method_name: method_name, permanent_attribute_name: permanent_attribute_name, params: params)
+ reflex = reflex_class.new(self,
+ url: url,
+ element: element,
+ selectors: selectors,
+ method_name: method_name,
+ permanent_attribute_name: permanent_attribute_name,
+ params: params,
+ reflex_id: data["reflexId"])
delegate_call_to_reflex reflex, method_name, arguments
rescue => invoke_error
message = exception_message_with_backtrace(invoke_error)
diff --git a/docs/README.md b/docs/README.md
index 1057014e..7ce563a0 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -20,7 +20,7 @@ This architecture eliminates the complexity imposed by full-stack frontend frame
## Introducing: Morphs
-v3.3 introduces the concept of **Morphs** to StimulusReflex.
+v3.3 introduced the concept of **Morphs** to StimulusReflex.
{% embed url="https://www.youtube.com/watch?v=utxCm3uLhIE" caption="" %}
diff --git a/docs/authentication.md b/docs/authentication.md
index 9f936826..a057aa5e 100644
--- a/docs/authentication.md
+++ b/docs/authentication.md
@@ -10,21 +10,7 @@ If you're just trying to bootstrap a proof-of-concept application on your local
### Encrypted Session Cookies
-You can use your default Rails encrypted cookie-based sessions to isolate your users into their own sessions. This works great even if your application doesn't have a login system.
-
-{% code title="app/controllers/application\_controller.rb" %}
-```ruby
-class ApplicationController < ActionController::Base
- before_action :set_action_cable_identifier
-
- private
-
- def set_action_cable_identifier
- cookies.encrypted[:session_id] = session.id.to_s
- end
-end
-```
-{% endcode %}
+You can use your Rails session to isolate your users so that they don't see each other's updates. This works great even if your application doesn't have a login system.
{% code title="app/channels/application\_cable/connection.rb" %}
```ruby
@@ -33,7 +19,7 @@ module ApplicationCable
identified_by :session_id
def connect
- self.session_id = cookies.encrypted[:session_id]
+ self.session_id = request.session.id
end
end
end
@@ -329,7 +315,7 @@ module ApplicationCable
def connect
self.current_user = env["warden"].user
- self.session_id = cookies.encrypted[:session_id]
+ self.session_id = request.session.id
reject_unauthorized_connection unless self.current_user || self.session_id
end
end
@@ -337,9 +323,9 @@ end
```
{% endcode %}
-This makes use of the ability to declare multiple `identified_by` values in a single connection class. Note that you still have to set the encrypted cookie value in your `application_controller.rb` and delegate both `current_user` and `session_id` to the connection so you can access these values in your Reflex action methods.
+This makes use of the ability to declare multiple `identified_by` values in a single connection class. Note that you still have to delegate both `current_user` and `session_id` to the connection so you can access these values in your Reflex action methods.
-Note that this approach could make some operations more complicated, because you cannot take for granted that a connection is attached to a valid user content. Please ensure that you are double-checking that all destructive mutations are properly guarded based on whatever policies you have in place.
+This approach could make some operations more complicated, because you cannot take for granted that a connection is attached to a valid user. Please ensure that you are double-checking that all destructive mutations are properly guarded based on whatever policies you have in place.
## Multi-Tenant Applications
@@ -375,7 +361,53 @@ The `before_reflex` callback is the best place to handle privilege checks, becau
### Pundit
-The trusty [pundit](https://github.com/varvet/pundit) gem allows you to set up policy classes that you can use to lock down Reflex action methods in a structured way. The following example assumes that you have a `current_user` in scope and an `application_policy.rb` already in place. In this application, the `User` model has a boolean attribute called `admin`.
+The trusty [pundit](https://github.com/varvet/pundit) gem allows you to set up policy classes that you can use to lock down Reflex action methods in a structured way. Reflexes are similar enough to controllers that if you include the `Pundit` module, you can take advantage of the `authorize` method.
+
+Pundit expects you to have a `current_user` in scope and a policy matching the name of your Reflex action. In the following example we create a `sing?` policy for our `sing` Reflex action in `song_policy.rb`
+
+{% code title="app/policies/song\_policy.rb" %}
+```ruby
+class SongPolicy < ApplicationPolicy
+ def sing?
+ user.sings_in_key?
+ end
+end
+```
+{% endcode %}
+
+{% code title="app/reflexes/song\_reflex.rb" %}
+```ruby
+class SongReflex < ApplicationReflex
+ include Pundit
+
+ def sing
+ @song = Song.find(params[:song_id])
+ authorize @song
+ # sing your heart out, baby!
+ end
+end
+```
+{% endcode %}
+
+Pundit will match your Reflex action to the right policy. If the `authorize` call fails, a `Pundit::NotAuthorizedError` will be raised, which you can handle in your Reflex action or leave unhandled so that it bubbles up and gets picked up by a 3rd-party error handling mechanism such as [Sentry](https://sentry.io) or [HoneyBadger](https://www.honeybadger.io/).
+
+{% code title="app/reflexes/application\_reflex.rb" %}
+```ruby
+class ApplicationReflex < StimulusReflex::Reflex
+ rescue_from Pundit::NotAuthorizedError do |exception|
+ # handle authorization issue
+ end
+end
+```
+{% endcode %}
+
+If you're using Pundit to safeguard data from being accessed by bad actors and unauthorized parties - due to bugs in your code - that's probably the correct approach. _However..._ you might also want to explicitly validate policies so that you can react to them in your browser:
+
+#### Explitic policy validation
+
+You can also ask Pundit to validate a policy explicitly and then [abort the Reflex](https://docs.stimulusreflex.com/reflexes#aborting-a-reflex) before it begins. This is an action that can be handled by the client via the **halted** lifecycle event.
+
+The following example assumes that you have a `current_user` in scope and an `application_policy.rb` already in place. In this application, the `User` model has a boolean attribute called `admin`.
{% code title="app/policies/example\_reflex\_policy.rb" %}
```ruby
diff --git a/docs/deployment.md b/docs/deployment.md
index c4cc7b3b..314d2114 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -135,7 +135,9 @@ Yes.
We're excited to announce that StimulusReflex now works with [AnyCable](https://github.com/anycable/anycable), a library which allows you to use any WebSocket server \(written in any language\) as a replacement for your Ruby WebSocket server. You can read more about the dramatic scalability possible with AnyCable in [this post](https://evilmartians.com/chronicles/anycable-actioncable-on-steroids).
-We'd love to hear your battle stories regarding the number of simultaneous connections you can achieve both with and without AnyCable. Anecdotal evidence suggests that you can realistically squeeze ~4000 connections with native ActionCable, whereas AnyCable should allow roughly 10,000 connections **per node**. Of course, the message delivery speed will dip as you start to approach the upper limit, so if you are working on a project successful enough to have this problem, you are advised to switch.
+We'd love to hear your battle stories regarding the number of simultaneous connections you can achieve both with and without AnyCable. Anecdotal evidence suggests that you can realistically squeeze ~4000 connections with native ActionCable, whereas AnyCable should allow roughly 10,000 connections **per node**. We've even [seen reports](https://nebulab.it/blog/actioncable-vs-anycable-fight/) that ActionCable can benchmark at 20,000 connections, while AnyCable maxes out around 60,000 because it runs out of TCP ports to allocate.
+
+Of course, the message delivery speed - and even delivery _success_ rate - will dip as you start to approach the upper limit, so if you are working on a project successful enough to have this problem, you are advised to switch.
Getting to this point required significant effort and cooperation between members of both projects. You can try out the AnyCable v1.0 release today.
diff --git a/docs/lifecycle.md b/docs/lifecycle.md
index 7a919259..6da72934 100644
--- a/docs/lifecycle.md
+++ b/docs/lifecycle.md
@@ -190,7 +190,7 @@ Adapting the Generic example, we've refactored our controller to capture the `be
{% hint style="warning" %}
Adding a declarative Reflex such as `Foo#action` to your element does **not** automatically attach an instance of the _foo_ Stimulus controller to the element.
-This coupling would only add an unneccesary constraint, as you can call any Reflex from any Stimulus controller.
+This coupling would only add an unneccessary constraint, as you can call any Reflex from any Stimulus controller.
If you want to run Reflex lifecycle callbacks on your element, you need to use `data-controller="foo"` to attach it.
diff --git a/docs/morph-modes.md b/docs/morph-modes.md
index 1685591a..6ac622df 100644
--- a/docs/morph-modes.md
+++ b/docs/morph-modes.md
@@ -394,7 +394,7 @@ Your user clicks a button. Something happens on the server. The browser is notif
Nothing morphs are [Remote Procedure Calls](https://en.wikipedia.org/wiki/Remote_procedure_call), implemented on top of ActionCable.
-Sometimes you want to take advantage of the chasis and infrastructure of StimulusReflex, without any assumptions or expectations about changing your DOM afterwards. The bare metal nature of Nothing morphs means that the time between initiating a Reflex and receiving a confirmation can be low single-digit miliseconds, if you don't do anything to slow it down.
+Sometimes you want to take advantage of the chasis and infrastructure of StimulusReflex, without any assumptions or expectations about changing your DOM afterwards. The bare metal nature of Nothing morphs means that the time between initiating a Reflex and receiving a confirmation can be low single-digit milliseconds, if you don't do anything to slow it down.
Nothing morphs usually initiate a long-running process, such as making calls to APIs or supervising I/O operations like file uploads or video transcoding. However, they are equally useful for emitting signals; you could send messages into a queue, tell your media player to play, or tell your Arduino to launch the rocket.
diff --git a/docs/patterns.md b/docs/patterns.md
index 26b5e579..837bea37 100644
--- a/docs/patterns.md
+++ b/docs/patterns.md
@@ -265,6 +265,16 @@ end
```
{% endcode %}
+If you're working on translations and would like to have your `.yml` files automatically reload when the browser refreshes, we've got you covered:
+
+{% code title="app/controllers/application\_controller.rb" %}
+```ruby
+class ApplicationController < ActionController::Base
+ before_action -> { I18n.backend.reload! } if Rails.env.development?
+end
+```
+{% endcode %}
+
### The Current pattern
Several years ago, DHH [introduced](https://www.youtube.com/watch?v=D7zUOtlpUPw) the [Current](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html) pattern to Rails 5.1. It's easy to work with Current objects inside of your Reflex classes using a `before_reflex` callback in your `ApplicationReflex`.
@@ -473,7 +483,7 @@ Now, you can refactor your view template like this:
We've got your back.
{% endhint %}
-Now, let's revisit our `ExampleReflex` class. When the user clicks the button, it calls our `api` action. The `@api_status` is set to `:loading` and `wait_for_it` gets called specifying the `success` action as the callback. Since `wait_for_it` operates asyncronously in its own thread, the action immediately sends the template back to the client to notify them that a slow process has started.
+Now, let's revisit our `ExampleReflex` class. When the user clicks the button, it calls our `api` action. The `@api_status` is set to `:loading` and `wait_for_it` gets called specifying the `success` action as the callback. Since `wait_for_it` operates asynchronously in its own thread, the action immediately sends the template back to the client to notify them that a slow process has started.
{% tabs %}
{% tab title="example\_reflex.rb" %}
diff --git a/docs/setup.md b/docs/setup.md
index 35ff26fc..3d61e121 100644
--- a/docs/setup.md
+++ b/docs/setup.md
@@ -150,3 +150,71 @@ To use Rails 5.2 with StimulusReflex, you'll need the latest Action Cable packag
There's nothing about StimulusReflex 3+ that shouldn't work fine in a Rails 5.2 app if you're willing to do a bit of manual package dependency management.
{% endhint %}
+## Running "Edge"
+
+If you are interested in running the latest version of StimulusReflex, you can point to the `master` branch on Github:
+
+{% code title="package.json" %}
+```javascript
+"dependencies": {
+ "stimulus_reflex": "hopsoft/stimulus_reflex#master"
+}
+```
+{% endcode %}
+
+{% code title="Gemfile" %}
+```ruby
+gem "stimulus_reflex", github: "hopsoft/stimulus_reflex"
+```
+{% endcode %}
+
+Once you have updated your `Gemfile` and `package.json` you need to run the following commands from the root folder of your project:
+
+```bash
+bundle install
+yarn install --check-files
+cd node_modules/stimulus_reflex/javascript
+yarn install
+yarn run build
+cd ../../..
+```
+
+Finally, restart your server\(s\) and refresh your page to see the latest.
+
+{% hint style="success" %}
+It is really important to **always make sure that your Ruby and Javascript package versions are the same**!
+{% endhint %}
+
+### Running a branch to test a Github Pull Request
+
+Sometimes you want to test a new feature or bugfix before it is officially merged with the `master` branch. You can adapt the "Edge" instructions and run code from anywhere.
+
+Using [\#335 - tab isolation mode v2](https://github.com/hopsoft/stimulus_reflex/pull/335) as an example, we first need the Github username of the author and the name of their local branch associated with the PR. In this case, the answers are `leastbad` and `isolation_optional`. This is a branch on the forked copy of the main project; a pull request is just a proposal to merge the changes in this branch into the `master` branch of the main project repository.
+
+{% code title="package.json" %}
+```javascript
+"dependencies": {
+ "stimulus_reflex": "leastbad/stimulus_reflex#isolation_optional"
+}
+```
+{% endcode %}
+
+{% code title="Gemfile" %}
+```ruby
+gem "stimulus_reflex", github: "leastbad/stimulus_reflex", branch: "isolation_optional"
+```
+{% endcode %}
+
+Once you have updated your `Gemfile` and `package.json` you need to run the following commands from the root folder of your project:
+
+```bash
+bundle install
+yarn install --check-files
+cd node_modules/stimulus_reflex/javascript
+yarn install
+yarn run build
+cd ../../..
+```
+
+Finally, restart your server\(s\) and refresh your page to see the latest.
+
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index df93e1b1..155f21a7 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -70,7 +70,7 @@ You can also set debug mode after you've initialized StimulusReflex. This is esp
{% code title="app/javascript/controllers/index.js" %}
```javascript
StimulusReflex.initialize(application, { consumer })
-if (process.env.RAILS_ENV === 'development') StimulusReflex.debug = true
+StimulusReflex.debug = process.env.RAILS_ENV === 'development'
```
{% endcode %}
@@ -130,7 +130,7 @@ const application = Application.start(document.documentElement, {
const context = require.context('controllers', true, /_controller\.js$/)
application.load(definitionsFromContext(context))
StimulusReflex.initialize(application, { consumer })
-if (process.env.RAILS_ENV === 'development') StimulusReflex.debug = true
+StimulusReflex.debug = process.env.RAILS_ENV === 'development'
```
{% endcode %}
@@ -185,7 +185,7 @@ Do you have your `config/cable.yml` set up properly? We strongly recommend that
{% endhint %}
{% hint style="info" %}
-Are you using `ApplicationController.render` to regenerate partials that make use of view helpers? Are those helpers generating URL routes that point to `example.com`? You can fix this by setting up your [default\_url\_options](https://docs.stimulusreflex.com/deployment#set-your-default_url_options-for-each-environment).
+Are you using `ApplicationController.render` to regenerate partials that make use of view helpers? Are those helpers generating URL routes that point to `example.com`? You can fix this by setting up your [default\_url\_options](https://docs.stimulusreflex.com/deployment#set-your-default_url_options-for-each-environment).
{% endhint %}
{% hint style="info" %}
diff --git a/javascript/.prettierignore b/javascript/.prettierignore
new file mode 100644
index 00000000..1521c8b7
--- /dev/null
+++ b/javascript/.prettierignore
@@ -0,0 +1 @@
+dist
diff --git a/javascript/log.js b/javascript/log.js
index 95e9d489..499fa674 100644
--- a/javascript/log.js
+++ b/javascript/log.js
@@ -17,7 +17,9 @@ function success (event) {
reflex.totalOperations > 1
? ` ${reflex.completedOperations}/${reflex.totalOperations}`
: ''
- const duration = `${new Date() - reflex.timestamp}ms`
+ const duration = reflex.timestamp
+ ? `in ${new Date() - reflex.timestamp}ms`
+ : 'CLONED'
const operation = event.type
.split(':')[1]
.split('-')
@@ -26,7 +28,7 @@ function success (event) {
const halted = (serverMessage && serverMessage.subject == 'halted') || false
console.log(
`\u2193 reflex \u2193 ${target} \u2192 ${selector ||
- '\u221E'}${progress} in ${duration}`,
+ '\u221E'}${progress} ${duration}`,
{ reflexId, morph, operation, halted }
)
}
@@ -34,10 +36,12 @@ function success (event) {
function error (event) {
const { detail } = event || {}
const { reflexId, target, serverMessage } = detail.stimulusReflex || {}
- const duration = `${new Date() - reflexes[reflexId].timestamp}ms`
+ const duration = reflex.timestamp
+ ? `in ${new Date() - reflex.timestamp}ms`
+ : 'CLONED'
const payload = detail.stimulusReflex
console.log(
- `\u2193 reflex \u2193 ${target} in ${duration} %cERROR: ${serverMessage.body}`,
+ `\u2193 reflex \u2193 ${target} ${duration} %cERROR: ${serverMessage.body}`,
'color: #f00;',
{ reflexId, payload }
)
diff --git a/javascript/package.json b/javascript/package.json
index 343d8472..c3ef1aab 100644
--- a/javascript/package.json
+++ b/javascript/package.json
@@ -1,6 +1,6 @@
{
"name": "stimulus_reflex",
- "version": "3.3.0",
+ "version": "3.4.0-pre3",
"description": "Build reactive applications with the Rails tooling you already know and love.",
"keywords": [
"ruby",
@@ -28,26 +28,36 @@
},
"license": "MIT",
"author": "Nathan Hopkins ",
- "main": "./stimulus_reflex.js",
+ "source": "./stimulus_reflex.js",
+ "main": "./dist/stimulus_reflex.js",
+ "module": "./dist/stimulus_reflex.module.js",
+ "esmodule": "./dist/stimulus_reflex.modern.js",
"scripts": {
- "postinstall": "node scripts/post_install.js",
+ "prepare": "yarn build",
+ "postinstall": "node ./scripts/post_install.js",
"prettier-standard:check": "yarn run prettier-standard --check *.js **/*.js",
"prettier-standard:format": "yarn run prettier-standard *.js **/*.js",
- "test": "yarn run mocha --require @babel/register"
+ "test": "yarn run mocha --require @babel/register --require esm",
+ "build": "microbundle --target browser --format modern,es,cjs --no-strict",
+ "dev": "microbundle watch --target browser --format modern,es,cjs --no-strict"
},
- "dependencies": {
+ "peerDependencies": {
"@rails/actioncable": ">= 6.0",
"cable_ready": ">= 4.3.0",
- "form-serialize": ">= 0.7.2",
"stimulus": ">= 1.1"
},
"devDependencies": {
"@babel/core": "^7.6.2",
"@babel/preset-env": "^7.6.2",
"@babel/register": "^7.6.2",
+ "@rails/actioncable": "^6.0.3-3",
"assert": "^2.0.0",
+ "cable_ready": "^4.4.0-pre0",
+ "esm": "^3.2.25",
"jsdom": "^16.0.1",
+ "microbundle": "^0.12.3",
"mocha": "^8.0.1",
- "prettier-standard": "^16.1.0"
+ "prettier-standard": "^16.1.0",
+ "stimulus": "^1.1.1"
}
}
diff --git a/javascript/stimulus_reflex.js b/javascript/stimulus_reflex.js
index 31f74907..5a4e86e2 100644
--- a/javascript/stimulus_reflex.js
+++ b/javascript/stimulus_reflex.js
@@ -1,11 +1,10 @@
import { Controller } from 'stimulus'
import CableReady from 'cable_ready'
-import serializeForm from 'form-serialize'
import { defaultSchema } from './schema'
import { getConsumer } from './consumer'
import { dispatchLifecycleEvent } from './lifecycle'
import { allReflexControllers } from './controllers'
-import { uuidv4, debounce, emitEvent } from './utils'
+import { uuidv4, debounce, emitEvent, serializeForm } from './utils'
import Log from './log'
import {
attributeValue,
@@ -14,7 +13,7 @@ import {
extractElementDataset,
findElement
} from './attributes'
-import { extractReflexName } from './utils'
+import { extractReflexName, elementToXPath, xPathToElement } from './utils'
// A lambda that does nothing. Very zen; we are made of stars
const NOOP = () => {}
@@ -37,6 +36,9 @@ window.reflexes = {}
// Indicates if we should log calls to stimulate, etc...
let debugging
+// Should Reflex playback be restricted to the tab that called it?
+let isolationMode
+
// Subscribes a StimulusReflex controller to an ActionCable channel.
// controller - the StimulusReflex controller to subscribe
//
@@ -45,17 +47,18 @@ const createSubscription = controller => {
const { channel } = controller.StimulusReflex
const subscription = { channel, ...actionCableParams }
const identifier = JSON.stringify(subscription)
- let totalOperations
- let reflexId
controller.StimulusReflex.subscription =
actionCableConsumer.subscriptions.findAll(identifier)[0] ||
actionCableConsumer.subscriptions.create(subscription, {
received: data => {
if (!data.cableReady) return
+
if (data.operations['dispatchEvent'])
return CableReady.perform(data.operations)
- totalOperations = 0
+
+ let totalOperations = 0
+ let reflexData
;['morph', 'innerHtml'].forEach(operation => {
if (data.operations[operation] && data.operations[operation].length) {
if (data.operations[operation][0].stimulusReflex) {
@@ -65,17 +68,35 @@ const createSubscription = controller => {
)
)
if (urls.length !== 1 || urls[0] !== location.href) return
- totalOperations += data.operations[operation].length
- reflexId = data.operations[operation][0].stimulusReflex.reflexId
+
+ totalOperations++
+
+ if (!reflexData)
+ reflexData = data.operations[operation][0].stimulusReflex
}
}
})
+
+ const { reflexId } = reflexData
+
+ if (!reflexes[reflexId] && !isolationMode) {
+ const element = xPathToElement(reflexData.xpath)
+ const controllerElement = xPathToElement(reflexData.cXpath)
+ element.reflexController = stimulusApplication.getControllerForElementAndIdentifier(
+ controllerElement,
+ reflexData.reflexController
+ )
+ element.reflexData = reflexData
+ dispatchLifecycleEvent('before', element, reflexId)
+ registerReflex(reflexData)
+ }
+
if (reflexes[reflexId]) {
reflexes[reflexId].totalOperations = totalOperations
reflexes[reflexId].pendingOperations = 0
reflexes[reflexId].completedOperations = 0
+ CableReady.perform(data.operations)
}
- if (reflexes[reflexId]) CableReady.perform(data.operations)
},
connected: () => {
actionCableSubscriptionActive = true
@@ -112,7 +133,7 @@ const extendStimulusController = controller => {
//
// - target - the reflex target (full name of the server side reflex) i.e. 'ReflexClassName#method'
// - element - [optional] the element that triggered the reflex, defaults to this.element
- // - options - [optional] an object that contains at least one of attrs, reflexId, selectors
+ // - options - [optional] an object that contains at least one of attrs, reflexId, selectors, resolveLate, serializeForm
// - *args - remaining arguments are forwarded to the server side reflex method
//
stimulate () {
@@ -128,6 +149,7 @@ const extendStimulusController = controller => {
element.validity &&
element.validity.badInput
) {
+ if (debugging) console.warn('Reflex aborted: invalid numeric input')
return
}
const options = {}
@@ -135,7 +157,13 @@ const extendStimulusController = controller => {
args[0] &&
typeof args[0] == 'object' &&
Object.keys(args[0]).filter(key =>
- ['attrs', 'selectors', 'reflexId', 'resolveLate'].includes(key)
+ [
+ 'attrs',
+ 'selectors',
+ 'reflexId',
+ 'resolveLate',
+ 'serializeForm'
+ ].includes(key)
).length
) {
const opts = args.shift()
@@ -148,6 +176,8 @@ const extendStimulusController = controller => {
const resolveLate = options['resolveLate'] || false
const datasetAttribute = stimulusApplication.schema.reflexDatasetAttribute
const dataset = extractElementDataset(element, datasetAttribute)
+ const xpath = elementToXPath(element)
+ const cXpath = elementToXPath(this.element)
const data = {
target,
args,
@@ -157,7 +187,10 @@ const extendStimulusController = controller => {
selectors,
reflexId,
resolveLate,
- permanent_attribute_name:
+ xpath,
+ cXpath,
+ reflexController: this.identifier,
+ permanentAttributeName:
stimulusApplication.schema.reflexPermanentAttribute
}
const { subscription } = this.StimulusReflex
@@ -176,21 +209,21 @@ const extendStimulusController = controller => {
setTimeout(() => {
const { params } = element.reflexData || {}
+ const formData =
+ options['serializeForm'] == false
+ ? ''
+ : serializeForm(element.closest('form'), { element })
+
element.reflexData = {
...data,
- params: {
- ...params,
- ...serializeForm(element.closest('form'), {
- hash: true,
- empty: true
- })
- }
+ params,
+ formData
}
subscription.send(element.reflexData)
})
- reflexes[reflexId] = { finalStage: 'finalize' }
+ const promise = registerReflex(data)
if (debugging) {
Log.request(
@@ -202,17 +235,6 @@ const extendStimulusController = controller => {
)
}
- const promise = new Promise((resolve, reject) => {
- reflexes[reflexId].promise = {
- resolve,
- reject,
- data
- }
- })
-
- promise.reflexId = reflexId
-
- if (debugging) promise.catch(NOOP)
return promise
},
@@ -242,6 +264,25 @@ const extendStimulusController = controller => {
})
}
+const registerReflex = data => {
+ const { reflexId } = data
+ reflexes[reflexId] = { finalStage: 'finalize' }
+
+ const promise = new Promise((resolve, reject) => {
+ reflexes[reflexId].promise = {
+ resolve,
+ reject,
+ data
+ }
+ })
+
+ promise.reflexId = reflexId
+
+ if (debugging) promise.catch(NOOP)
+
+ return promise
+}
+
// Registers a Stimulus controller and extends it with StimulusReflex behavior
//
// controller - the Stimulus controller
@@ -383,9 +424,10 @@ const getReflexRoots = element => {
// * isolate - [false] restrict Reflex playback to the tab which initiated it
//
const initialize = (application, initializeOptions = {}) => {
- const { controller, consumer, debug, params } = initializeOptions
+ const { controller, consumer, debug, params, isolate } = initializeOptions
actionCableConsumer = consumer
actionCableParams = params
+ isolationMode = !!isolate
stimulusApplication = application
stimulusApplication.schema = { ...defaultSchema, ...application.schema }
stimulusApplication.register(
diff --git a/javascript/test/utils.serializeForm.test.js b/javascript/test/utils.serializeForm.test.js
new file mode 100644
index 00000000..9590c936
--- /dev/null
+++ b/javascript/test/utils.serializeForm.test.js
@@ -0,0 +1,570 @@
+import assert from 'assert'
+import { JSDOM } from 'jsdom'
+import { serializeForm } from '../utils'
+
+describe('formSerialize', () => {
+ context('basic', () => {
+ it('should output an empty string if no form is present', () => {
+ const dom = new JSDOM('')
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected = ''
+ assert.equal(actual, expected)
+ })
+
+ it('should serialize empty form', () => {
+ const dom = new JSDOM('')
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected = ''
+ assert.equal(actual, expected)
+ })
+
+ it('should serialize basic form with single input', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected = 'foo=bar'
+ assert.equal(actual, expected)
+ })
+
+ it('should serialize inputs with no values', () => {
+ const dom = new JSDOM('')
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected = 'foo='
+ assert.equal(actual, expected)
+ })
+
+ it('should serialize text input with spaces in name', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected = 'name 1=StimulusReflex'
+ assert.equal(actual, expected)
+ })
+
+ it('should serialize from with multiple inputs', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected = 'foo=bar 1&foo.bar=bar 2&baz.foo=bar 3'
+ assert.equal(actual, expected)
+ })
+
+ it('should serialize text input and textarea', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected =
+ 'name=StimulusReflex&description=An exciting new way to build modern, reactive, real-time apps with Ruby on Rails.'
+ assert.equal(actual, expected)
+ })
+
+ it('should ignore disabled inputs', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected = 'foo=bar 1'
+ assert.equal(actual, expected)
+ })
+ })
+
+ context('', () => {
+ it('should serialize checkboxes', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected = 'foo=on&baz=on'
+ assert.equal(actual, expected)
+ })
+
+ it('should serialize checkbox array', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected = 'foo[]=bar&foo[]=baz'
+ assert.equal(actual, expected)
+ })
+
+ it('should serialize checkbox array with one input', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected = 'foo[]=bar'
+ assert.equal(actual, expected)
+ })
+ })
+
+ context('', () => {
+ it('should serialize radio button with no checked input', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected = ''
+ assert.equal(actual, expected)
+ })
+
+ it('should serialize radio button', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected = 'foo=bar1'
+ assert.equal(actual, expected)
+ })
+
+ it('should serialize radio button with empty input', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected = 'foo='
+ assert.equal(actual, expected)
+ })
+
+ it('should serialize radio and checkbox with the same key', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected = 'foo=bar1&foo=bar3'
+ assert.equal(actual, expected)
+ })
+ })
+
+ context(' - buttons', () => {
+ it('should not serialize buttons', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const element = dom.window.document.querySelector('input[type="submit"]')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected = 'submit=submit'
+ assert.equal(actual, expected)
+ })
+ })
+
+ context(' - brackets notation', () => {
+ it('should serialize text inputs with brackets notation', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected = 'name[]=StimulusReflex&name[]=CableReady'
+ assert.equal(actual, expected)
+ })
+
+ it('should serialize text inputs with nested brackets notation', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected =
+ 'account[name]=Foo Dude&account[email]=foobar@example.org&account[address][city]=Qux&account[address][state]=CA&account[address][empty]='
+ assert.equal(actual, expected)
+ })
+
+ it('should serialize text inputs with brackets notation and nested numbered index', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected =
+ 'person[address][23][city]=Paris&person[address][45][city]=London'
+ assert.equal(actual, expected)
+ })
+
+ it('should serialize text inputs with brackets notation and nested non-numbered index', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected =
+ 'person[address][23_id][city]=Paris&person[address][45_id][city]=London'
+ assert.equal(actual, expected)
+ })
+
+ it('should serialize text inputs with non-indexed bracket notation', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected =
+ 'people[][name]=fred&people[][name]=bob&people[][name]=bubba'
+ assert.equal(actual, expected)
+ })
+
+ it('should serialize text inputs with non-indexed nested bracket notation', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected = 'user[tags][]=cow&user[tags][]=milk'
+ assert.equal(actual, expected)
+ })
+
+ it('should serialize text inputs with indexed bracket notation', () => {
+ const dom = new JSDOM(
+ ''
+ )
+ const form = dom.window.document.querySelector('form')
+ const actual = serializeForm(form, { w: dom.window })
+ const expected =
+ 'people[2][name]=bubba&people[2][age]=15&people[0][name]=fred&people[0][age]=12&people[1][name]=bob&people[1][age]=14&people[][name]=frank&people[3][age]=2'
+ assert.equal(actual, expected)
+ })
+ })
+
+ context('