Giter Site home page Giter Site logo

stimulusreflex / cable_ready Goto Github PK

View Code? Open in Web Editor NEW
726.0 11.0 69.0 10.94 MB

Use simple commands on the server to control client browsers in real-time

Home Page: https://cableready.stimulusreflex.com

License: MIT License

Ruby 65.19% Shell 0.12% JavaScript 32.19% CSS 0.29% HTML 2.20%
ruby ruby-on-rails action-cable server-side-rendering real-time hacktoberfest javascript

cable_ready's Introduction

Welcome to CableReady πŸ‘‹

downloads License: MIT Documentation
semantic-release Ruby Code Style JavaScript Code Style
Prettier-Standard StandardRB Tests


CableReady helps you create great real-time user experiences by making it simple to trigger client-side DOM changes from server-side Ruby. It establishes a standard for interacting with the client via ActionCable web sockets. No need for custom JavaScript.

Please read the official ActionCable docs to learn more about ActionCable before proceeding.

πŸ“š Docs

πŸ’™ Community

  • Discord - primary support channel

πŸš€ Install

Rubygem

bundle add cable_ready

JavaScript

There are a few ways to install the CableReady JavaScript client, depending on your application setup.

ESBuild / Webpacker

yarn add cable_ready

Import maps:

# config/importmap.rb

# ...

pin 'cable_ready', to: 'cable_ready.js', preload: true

Rails Asset pipeline (Sprockets):

<!-- app/views/layouts/application.html.erb -->

<%= javascript_include_tag "cable_ready.umd.js", "data-turbo-track": "reload" %>

Checkout the documentation to continue!

πŸ™ Contributing

Code of Conduct

Everyone interacting with CableReady is expected to follow the Code of Conduct

Coding Standards

This project uses Standard and prettier-standard to minimize bike shedding related to code formatting.

Please run ./bin/standardize prior submitting pull requests.

πŸ“¦ Releasing

  1. Make sure that you run yarn and bundle to pick up the latest.
  2. Bump version number at lib/cable_ready/version.rb. Pre-release versions use .preN
  3. Run rake build and yarn build
  4. Commit and push changes to GitHub
  5. Run rake release
  6. Run yarn publish --no-git-tag-version
  7. Yarn will prompt you for the new version. Pre-release versions use -preN
  8. Commit and push changes to GitHub
  9. Create a new release on GitHub (here) and generate the changelog for the stable release for it

πŸ“ License

CableReady is released under the MIT License.

cable_ready's People

Contributors

acarpe avatar andreaslillebo avatar andrewerlanger avatar andrewmcodes avatar back2war avatar binarygit avatar dabit avatar dependabot[bot] avatar erlingur avatar excid3 avatar existentialmutt avatar grncdr avatar henrik avatar hopsoft avatar jaredcwhite avatar joshleblanc avatar julianrubisch avatar konnorrogers avatar leastbad avatar marcoroth avatar matt-yorkley avatar mlitwiniuk avatar n-rodriguez avatar palkan avatar paul avatar pskarlas avatar rickbenavidez avatar rrosen avatar sarriagada avatar sztheory avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cable_ready's Issues

OperationBuilder#operations_payload messes with keys

While working on a specific StimulusReflex, I was experiencing a weird bug, where underscored payload keys would come back dasherized.

So self.payload = {"foo_bar" => "baz"} on the JS came back as {"foo-bar" => "baz"}

I found out that this probably relates to OperationBuilder#operations_payload deep-camelizing all keys:

https://github.com/stimulusreflex/cable_ready/blob/master/lib/cable_ready/operation_builder.rb#L71-L73

The relevant test says this is a feature:

https://github.com/stimulusreflex/cable_ready/blob/master/test/lib/cable_ready/operation_builder_test.rb#L90-L94

However, it can lead to a confusing DX specifically in combination with StimulusReflex payloads. We could fix this maybe by explicitly opting out of the :payload hash key in operations_payload, although I'm a bit torn on this one.

Thoughts @leastbad @hopsoft ?

Using morph with a document fragment appears to do a full replace instead of a morph

Hello all! πŸ‘‹

This is my first time in CableReady land, so please excuse my ignorance as I try to explain the issue we're running into. πŸ™

We're using CableReady's morph functionality. We're also using a ViewComponent that we want to re-render multiple times. It works pretty much flawlessly for us!

cable_ready[channel].morph(
  selector: '#div-to-morph',
  html: ApplicationController.render(component)
)

πŸ‘

Then, we encountered a case where we need to use a permanent attribute name. We have an image that we don't want to re-render each time. We updated our code to the following:

cable_ready[channel].morph(
  selector: '#div-to-morph',
  html: ApplicationController.render(component),
  permanent_attribute_name: 'data-preview-permanent'
)
<div id="div-to-morph" class="<%= classes %>">
  <div data-preview-permanent>
    ... an image that should persist morphs...
  </div>
</div>

If we turn off browser caching, the image flickers (as it reloads) on each new morph call. This isn't caught when browser caching is on (unless you're using Safari, but that's another story for another time).

My co-worker @kylefox looked into CableReady and Morphom and learned that this does not happen if you change the morphdom call in CableReady to use template.innerHTML instead of template.content. (This change does cause issues in StimulusReflex, though)

Some deeper digging in Morphdom shows that when morphdom is given a DocumentFragment, it appears to do a full replace, versus morphing/diffing.

I believe this is not an issue when using childrenOnly.

However, in our case- we don't use childrenOnly, we need the entire component morphed, including the parent.

From our understanding, morph currently only diffs if you're using children_only: true otherwise, it appears to do a full replace.


@kylefox put together a sample application with a walkthrough video to help support our findings.

I find this difficult to explain properly given I don't fully understand all the implications. However, I'm more than glad to help explain more/hop on a Zoom call to discuss further.

If we're mis-understanding the library, my apologies as well!

Thanks for all your hard work!

cable_ready:channel generator has some issues

  1. rails destroy cable_ready:channel Sailor does not seem to destroy anything.
  2. It can't seem to locate the Stimulus controller template?

In this case I'd previously run the generator with "y" to broadcast_to, "Sailor" for the resource name, and "y" for the Stimulus controller. It complained about not finding the controller template in any of my source paths, as shown below.

~/testcr  $ rails destroy cable_ready:channel Sailor
Running via Spring preloader in process 69822
    generate  channel
       rails  generate channel sailor
Are you streaming to a resource using broadcast_to? (y/N) y
Which resource are you streaming for? (Sailor) 
Are you going to use a Stimulus controller to subscribe to this channel? (y/N) y
Could not find "app/javascript/controllers/%file_name%_controller.js" in any of your source paths. Your current source paths are: 
/Users/leastbad/.rvm/gems/ruby-2.6.5/gems/cable_ready-5.0.0.pre0/lib/generators/cable_ready/templates

Happy to help troubleshoot. Let me know how I can be of assistance.

Warning in Ruby 3.0

After upgrading to Ruby 3.0, the following warning gets printed when running system specs right before the first reflex invocation in a spec example:

.../ruby/gems/3.0.0/gems/cable_ready-4.5.0/lib/cable_ready/channel.rb:14: 
warning: finalizer references object to be finalized

Link to the line in question.

ObjectSpace.define_finalizer self, -> { CableReady.config.delete_observer config_observer }

Updatable's ActiveStorage implementation broadcasts to multiple/wrong keys

This issue emerged when I was first trying to integrate has_one_attached in the same way we have has_many_attached. I added has_one_attached to Updatable like so:

      def has_one_attached(name, **options)
        option = options.delete(:enable_updates)
        broadcast = option.present?
        result = super
        enrich_attachments_with_updates(name, option) if broadcast
        result
      end

After I wired up the User model:

class User < ApplicationRecord
  # ...
  has_one_attached :avatar, enable_updates: true
end

I got the following test failure from :

unexpected invocation: #<Mock:server>.broadcast("gid://dummy/Dugong/1:avatar", {:changed => ["id", "name", "record_type", "record_id", "blob_id", "created_at"]})

which is wrong simply because Dugong has no avatar AS association.

cc @andrewerlanger

CR is overriding dom_id

By the recommendations we include CableReady::Broadcaster in our application controller. This then includes CableReady::Indentifable which overrides Rails provided dom_id.

This produces different results to what is expected and should be either renamed, or removed to use dom_id as Rails expected output.

#165 reintroducing webpack issue

#165 is reverting changes from #169 which is leading to next error in webpack@5:

ERROR in ./node_modules/stimulus_reflex/node_modules/cable_ready/javascript/index.js 28:2-9
Should not import the named export 'version' (imported as 'version') from default-exporting module (only default export is available soon)

Make JS DOMOperations extendable

Hi there! First, thank you for this nice gem :)

Second, I think it would be nice to make DOMOperations extendable to build our own feature on CableReady.

Ruby is already extendable with a bit of monkey patching :

# frozen_string_literal: true

require 'cable_ready/channel'

# Patch CableReady::Channel to add our own operations

module Concerto
  module CoreExt
    module CableReadyPatch

      def notify(options = {})
        add_operation(:notify, options)
      end


      def stub
        super.merge(notify: [])
      end

    end
  end
end

CableReady::Channel.prepend(Concerto::CoreExt::CableReadyPatch)

For JS it's doable but it needs a patch :

The patch :

export default { perform, isTextInput, DOMOperations }

Then in application :

import CableReady  from 'cable_ready/cable_ready_patched'

CableReady.DOMOperations['notify'] = (detail) =>
  AsyncEvents.display_growl_message(detail)
cable_ready['MyChannel'].notify(opts)
cable_ready.broadcast_to(user.private_channel, 'MyChannel')

Note: the example make use of this PR : #38

Thank you!

Operations go missing before broadcasting

Hello

First off, thank you for this excellent gem. I'm running 4.3.0, and developing on OSX with Ruby 2.7.1 and Rails 6 master.

I believe I've found a concurrency problem in the CableReady::Channels singleton.

My app will occasionally enqueue 1000s of Sidekiq jobs in a very short period of time, each of which will generate a small number of morph operations and then broadcast them. A rather small percentage of these jobs will consistently fail inside cable_ready, be re-queued, and later succeed once the queue has shrunk.

The error is:

NoMethodError (undefined method `broadcast' for nil:NilClass):
cable_ready-4.3.0/lib/cable_ready/channels.rb:56:in `block (2 levels) in broadcast'

And occurs on here:

https://github.com/hopsoft/cable_ready/blob/master/lib/cable_ready/channels.rb#L56

channels.each { |channel| @channels[channel.identifier].broadcast(clear) }

From what I can see with some debug statements is the @channels instance var is an empty hash on line 56, despite having to have had keys/values on line 52 to select on line 54.

Unfortunately I'm not very familiar with this kind of problem, so not sure if there's anything I can do to mitigate it from my application code. I'm currently working around it by forcing Sidekiq to have a concurrency of 1, but I'm not sure how performant that would be in production.

My job class for reference:

class BroadcastJob < ActiveJob::Base
  queue_as :default
  sidekiq_options retry: 5
  include CableReady::Broadcaster

  def perform(object)
    cable_ready["subscription-#{object.subscription_id}"].morph(
      selector: "#object_#{object.id}",
      html: renderer.render(partial: "subscriptions/row", locals: { object: object })
    )

    cable_ready["month-#{object.month_id}"].morph(
      selector: "#object_#{object.id}",
      html: renderer.render(partial: "months/row", locals: { object: object })
    )

    cable_ready.broadcast
  end

  private

  def renderer
    @renderer ||= ApplicationController.renderer.new(https: true)
  end
end

Many thanks for any assistance you can provide!

Uncaught TypeError: operations.forEach is not a function

I am getting this error all of a sudden, and was trying to figure it out but couldn't find any resources.

Uncaught TypeError: operations.forEach is not a function
    at Object.perform (cable_ready.js:11)
    at Subscription.received (progress_controller.js:14)
    at action_cable.js:536
    at Array.map (<anonymous>)
    at Subscriptions.notify (action_cable.js:535)
    at Connection.message (action_cable.js:369)

The error message is referencing:

import { xpathToElement, dispatch } from './utils';
import activeElement from './active_element';
import OperationStore from './operation_store';
import actionCable from './action_cable';

var perform = function perform(operations) {
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
    emitMissingElementWarnings: true
  };
  var batches = {};
  operations.forEach(function (operation) {
    if (!!operation.batch) batches[operation.batch] = batches[operation.batch] ? ++batches[operation.batch] : 1;
  });
  operations.forEach(function (operation) {
    var name = operation.operation;
...
...

Any help would be appreciated!

Specify CableReadyBroadcastJob queue in initializer

  1. It seems like the queue_as value in the job class should default to :default but be customizable via the CR initializer.
  2. Following this logic a bit further, what if we added a keyword parameter queue to the broadcast_later and broadcast_later_to method signature that could override?

broadcast_later(queue: "cable_ready")

What do you think, @julianrubisch?

`inverse_of` returns `nil` when enriching `has_many` association with updates

inverse_of returns nil

I'm running into a strange issue where inverse_of returns nil although I have declared it in the model.

For example, I have two models, Listing which has many Listings::PublishActions.

# Listing
has_many :publish_actions, class_name: "Listings::PublishAction", dependent: :destroy, foreign_key: :listing_id, enable_updates: true, inverse_of: :listing

# Listing::PublishAction
belongs_to :listing, class_name: "Listing"

Inside of enrich_association_with_updates, this line gets the inverse_of reflection:

inverse_of = reflection.inverse_of&.name&.to_s

This is returning nil for me, and I get an error from the collection registry.

inverse_name has the proper value

Although this is nil, inverse_name has the proper key, and the options have the inverse_of declaration inside of it as well:

[5] pry(Listing)> self
=> Listing(id: integer, team_id: integer, name: string, created_at: datetime, updated_at: datetime)
[6] pry(Listing)> name
=> :publish_actions
[7] pry(Listing)> reflect_on_association(name)
=> #<ActiveRecord::Reflection::HasManyReflection:0x00007f4b1e4d97f0
 @active_record=
  Listing(id: integer, team_id: integer, name: string, created_at: datetime, updated_at: datetime),
 @class_name="Listings::PublishAction",
 @inverse_name=:listing,
 @inverse_of=nil,
 @klass=
  Listings::PublishAction(id: integer, listing_id: integer, started_at: datetime, completed_at: datetime, target_count: integer, performed_count: integer, scheduled_for: datetime, sidekiq_jid: string, created_by_id: integer, approved_by_id: integer, created_at: datetime, updated_at: datetime),
 @name=:publish_actions,
 @options=
  {:class_name=>"Listings::PublishAction",
   :dependent=>:destroy,
   :foreign_key=>:listing_id,
   :inverse_of=>:listing,
   :extend=>[ExtendHasMany::ClassMethods, ObfuscatesId::ClassMethods]},
 @plural_name="publish_actions",
 @scope=nil>
[8] pry(Listing)> reflect_on_association(name).inverse_of
=> nil

This doesn't happen however when firing up the Rails console and grabbing the reflection that way:

> rails c
Loading development environment (Rails 7.0.4.2)
irb(main):001:0> Listing.reflect_on_association(:publish_actions).inverse_of
=> 
#<ActiveRecord::Reflection::BelongsToReflection:0x00007f50ddbdb098                                
 @active_record=Listings::PublishAction (call 'Listings::PublishAction.connection' to establish a connection),
 @klass=nil,                                                                                      
 @name=:listing,                                                                                  
 @options={:class_name=>"Listing"},                                                               
 @plural_name="listings",                                                                         
 @scope=nil>   

Also, everything works by simply removing enable_updates: true, which led me to believe it's something going on here. For what it's worth, it's happening in a Bullet Train application, but it doesn't seem like it's something there. I wish I could give more details, but that's about all I can glean from the code right now.

documentation: Mention cable.yml adapter configuration

I was following along with the Setup documentation, just trying to get the most basic example to work, and was completely stumped. Nothing I did seemed to update the page. After much frustration, I nearly gave up on trying to use cable ready altogether, but luckily stumbled across a user post on this GoRails video: https://gorails.com/episodes/how-to-use-cable-ready mentioning the cable.yml file. So finally after changing my development environment to adapter: postgresql, everything started to work. It's not totally clear to me why the adapter: async was not working, so perhaps I'm missing something, but suffice it to say, changing this file was key to getting things setup.

I fully recognize that this is more of an ActionCable specific thing than a CableReady thing, but for me, part the appeal of CableReady is that it simplifies using ActionCable, aka, I don't need to be an ActionCable expert to use it. So all that said, since it seems like I'm not the first person to get hung up by this cable.yml config file, perhaps it's worth mentioning in the setup docs? Even a single line like:
"Be sure you're using the correct adapter in your config/cable.yml file"
would have helped me tremendously.

Just a suggestion. Happy to submit a PR to update the docs if that would help.

Upgrading to v5.0.0.pre10 breaks Rails' named route helpers

After upgrading cable_ready from v5.0.0.pre9 to v5.0.0.pre10, lots of request and system specs in my Rails 7 application started failing. After some debugging, I realized that it was because named route helpers (e.g. sessions_path or account_path(user)) in my views started raising all sorts of weird errors, for example:

Screenshot 2023-03-03 at 17 28 34

Screenshot 2023-03-03 at 17 37 02

After some further debugging (with the gem cloned locally and bundled from /vendor), I was able to narrow down the problem to this commit - a7c607e - and then this line:

https://github.com/stimulusreflex/cable_ready/blob/v5.0.0.pre10/lib/tasks/cable_ready/cable_ready.rake#L3

With that include commented out, all of my specs start working again as normal!

Question: Would you consider this intended behaviour?

Hi

I've got a fairly complex ActionCable broadcast updating multiple UI blocks on a page when a user comes online or goes offline. Here is a redacted version of the UI I am working on.

Healthforce - Nurse Sethu Huel 2020-05-26 14-07-21

  • Red block: Updates using textContent
  • Green block: Updates using addCssClass / removeCssClass
  • Blue block: Updates by adding and remove pink blocks using insertAdjacentHTML and remove

So everything is working fine using the code below:

channel = cable_ready['UserStatus']
channel.text_content(selector: '[data-target="user.onlineCount"]', text: User.online.count)
channel.add_css_class(selector: '[data-target="user.button"]', name: 'some-css')
channel.insert_adjacent_html(selector: '[data-target="user.list"]', html: '<div>...</div>')
channel.broadcast

My question is... was this intended behaviour, and if not, how do you propose I deal with my use-case? I found a caveat which made me wonder if I am doing the correct thing and that's when try and use channel.remove(selector: '#whatever') before channel.insert_adjacent_html. The html component never appears and I am guessing this has to do with the order of remove in relation to insert_adjacent_html in https://github.com/hopsoft/cable_ready/blob/master/lib/cable_ready/channel.rb#L201-L220. So it adds it and then removes because of the order of data.operations.

Finalizer warning using Ruby 3.2.1

Hi folks.

When shutting down a cable-ready app running under MRI Ruby 3.2.1 we've been getting the following warning:

bin/rails: warning: Exception in finalizer #<Proc:0x00007f4384e74d88 ~/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/cable_ready-4.5.0/lib/cable_ready/channel.rb:14 (lambda)>
~/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/cable_ready-4.5.0/lib/cable_ready/channel.rb:14:in `block in initialize': wrong number of arguments (given 1, expected 0) (ArgumentError)

It looks like this has only been recently introduced (see ruby pull 3444, but harks back to an issue Mike Perham identified back in 2010.

We ended up monkey-patching the CableReady::Channel like so:

module CableReady
  class Channel
    attr_reader :identifier, :enqueued_operations

    def initialize(identifier)
      @identifier = identifier
      reset
      CableReady.config.operation_names.each { |name| add_operation_method name }

      config_observer = self
      CableReady.config.add_observer config_observer, :add_operation_method
      ObjectSpace.define_finalizer self, self.class.finalize_observer(config_observer)
    end

    def self.finalize_observer(config_observer)
      proc { CableReady.config.delete_observer config_observer }
    end
  end
end

This seems to work on Ruby 3.2.1, but I've no idea if this is correct or is applicable on other Rubies. I'm not super comfortable with how ObjectSpace.define_finalize works, so I'm not inclined to suggest this as a solution - just hoping it might inform someone with better chops than me.

Cheers.

Can't make `redirect_to` with `cablecar` functionnal with `rc2`

This code is functional for pre10:

        render operations: cable_car.redirect_to(
          url: MY_URL
        )

Response is:

[{"url":"/","operation":"redirectTo"}]

Unfortunately, when bumping to rc2, with the following code:

        render cable_ready: cable_car.redirect_to(
          url: MY_URL
        )

(only changing deprecated operations: to cable_ready) it does not redirect my app, probably because the response is actually empty.

  • Note 1: if I use operations: instead of cable_ready: I had a deprecated warning, but the response is still empty.
DEPRECATED: CableReady's `render operations:` call has been renamed to `render cable_ready:`. Please update your render call.
  • Note 2: Running this request is a spec environnement seems ok (at a request level, not browser one), but not in browser.
  • Note 3:
        cable_car.redirect_to(
          url: return_url_or_default(after_sign_in_path_for(spree_current_user))
        )

seems ok and is equal to: #<CableReady::CableCar:0x00007fe3be5ecf28 @enqueued_operations=[{"url"=>"/", "operation"=>"redirectTo"}], @identifier="CableCar", @previous_selector=nil>

(at least json seems fine)

  • Note 4: debugging with firefox give me another clue: the response is not empty and seems to be valid (ie. the right json). On Chrome, the response is still empty. Which seems to be a bit strange.

updates_for: skip updates

Hey everyone! Big thanks for updates_for – it's been an absolute joy to work with πŸ™Œ

One issue that I've come across is that, because updates_for requires an open Redis connection, CRUDing records will fail where you wouldn’t ordinarily have started a Redis server: seeds and data migrations (using data-migrate) in my case.

Here’s an example:

# app/models/chapter.rb
class Chapter < ActiveRecord
  include CableReady::Updatable

  enable_updates
end 

# db/seeds.rb
puts β€œDestroying chapters…”
Chapter.destroy_all

puts β€œCreating chapters…”
Chapter.create!
# …
bin/rails db:seed

Destroying chapters…
rails aborted!
Redis::CannotConnectError: Error connecting to Redis on localhost:6379 (Errno::ECONNREFUSED)

Spinning up a Redis server to run seeds has been ok (though not desirable) for me, but not being able to run (and, more importantly, roll back) data migrations without one seems a little more ✨ wild ✨.

I’m wondering whether it might make sense to add an option a la β€œskip updates”. Here are two ideas from an initial brainstorm:

1. A global config option that skips the relevant callbacks for predefined paths:

config.skip_updates_from += [Rails.root.join(β€œ/db”)]

I’d personally like this from a DX perspective as I can’t see a need to broadcast changes from anywhere in the /db directory, so a blanket exclusion would make sense.

I’m not 100% sure what a reliable implementation would look like though – intuition tells me that foraging through the callstack in an after_commit conditional could be very error-prone. It’s also a pretty rigid approach (though maybe that’s what you’d want here?).

2. A skip_updates method that yields a given block (credit to @andrewerlanger for this idea, which I also like love):

# db/seeds.rb

cable_ready.skip_updates do
  Chapter.destroy_all
  Chapter.create!
end

This would, of course, be way more flexible. On the other hand, it would require more thinking on the DX side, and thinking is hard.

I’d love to get your thoughts on this! πŸ€Έβ€β™€οΈ

ActiveRecord dependency breaks rspec on non AR Rails implementations

I have an app running with StimulusReflex 4.x 3.4 fine, this app have no AR whatsoever, as we use other DB which AR do not support. The migration to 5.0pre is making RSpec tests crash because AR is being required for the new identifiable feature.

I think this only happens with RSpec on test environment, as when setting eager load to true in my dev env the app works fine.

Thanks for the awesome work!

updates_for: add option to opt out of morphing if element is currently focused

I've seen a lot of edge cases right now when re-applying focus didn't work as intended and work done in the meantime (entering text etc.) was lost.

this is especially annoying when the updates are triggered by another user and you lose your work.

I think we should add an option (or even make it the default?) to not morph any specific updates_for element that currently has focus (or one of its shadow children)

thoughts @leastbad @erlingur ?

Inlcude changed keys when broadcasting to model class? (updates_for :only)

My use case is that I would like to put this in a view:

<%= updates_for StaffMember, only: 'current_task' do %>
 <%# show what people are working on #%>
<% end %>

That is: trigger a refresh when the current_task of any StaffMember changes. However, it seems like broadcasts to the subscription ID of a model class are always an empty hash, so the :only option is effectively ignored.

I'm pretty sure that's happening here:

def broadcast_update(model)
ActionCable.server.broadcast(model.class, {})
ActionCable.server.broadcast(model.to_global_id, model.respond_to?(:previous_changes) ? {changed: model.previous_changes.keys} : {})
end

Could we include the changed attributes when broadcasting to the model.class as well? I feel like it would at least be harmless, but maybe I've missed the reasoning for doing it this way.

`MissingElement` is not defined on initialize

Hi! πŸ‘‹

Firstly, thanks for your work on this project! πŸ™‚

I had some issues with setting the option 'onMissingElement' to 'event' in my project today. Here are the error that I had:

"Uncaught ReferenceError: MissingElement is not defined"

I looked into sources to patch it in my project, and I think that the MissingElement import is missing in the "javascript/index.js" file

Thanks,
Antoine

updates_for: Support multiple enable_updates per model

Hi there! I've had a lot of fun playing with updates_for since it went live so many thanks for sharing it with the world.

Just wondering whether it might make sense to allow for multiple enable_updates on a given model, which then map to different views.

For example, let's say I have a Blog model with a show page and an index page. And let's just pretend these blogs get updated quite frequently by their authors.

On the index page I only need to update the name of a given blog if it's changed, so I might do something like this:

class Blog < ApplicationRecord
  enable_updates on: :update, if: -> { saved_change_to_name? }
end

Now I start building the show page and want to enable updates if any field changes. I could go back to my enable_updates and remove the if Proc, but then my index page blogs would be constantly fetching updates even when none of the fields changed are visible on that page.

One thing I love about updates_for is its simplicity, so I'm hesitant to propose anything that takes away from that. That said, I also feel like allowing for more granular setting of when/where to enable updates would have widespread application beyond something like the contrived example above.

Penny for your thoughts :)

`updates_for` Makes Redundant Calls

I think the order in which an updates_for_element checks whether it shouldUpdate may be a little off.

Let's say I have the following html:

<%= updates_for @item, only: :foo do %>
  ...
<% end %>

And then I update a field not specified in the only option:

@item.update(bar: true)

If we look at the update(data) method of updates_for_element.js, the logic will play out in the following order:

  1. We create a new Block out of the element (L49).
  2. We fetch the html for the element (L65). This part is the problem, I think. Ideally, we shouldn't be requesting the html until we know the element should actually be updated, but we haven't run that check yet.
  3. We process the block (L85). It's only now that we realize the element shouldn't be updated, and so we don't proceed with the actual updating. But this means the fetch in step 2. above was redundant: we asked the server to render the full html for the page but didn't do anything with it.

I think the better approach here would be to check whether the update should be performed before we go and pester the server, so something along the lines of:

let blocks = Array.from(
  document.querySelectorAll(this.query),
  element => new Block(element)
)

// filter out any blocks that don't require updates
blocks = blocks.filter(block => block.shouldUpdate(data))

// return here if there are no blocks that require updates
if (blocks.length === 0) return

// carry on with the fetch logic only for blocks that require updates
...

Not sure if I'm missing something here, but I played around with the above locally and it seemed to work well.

@julianrubisch @leastbad penny for your thoughts :)

dom_id is not consistent with Rails

First, TY for SR and CR! I'm new here, so pardon if this has been discussed at length already.

In short, I have noticed that CR's dom_id implementation deviates significantly from Rails. It adds the "#" prefix and downcases the identifier. The reason why this is important (IMO), is that I am using dom_id for a channel identifier and thus makes usage of dom_id in a reflex that sends a message to a channel via CR incompatible.

Here is an example:

class MyRoomChannel < ApplicationCable::Chanell
  def subscribed
    stream_from (dom_id(my_model) << "_messages")
  end
end

class MyMessageReflex < ApplicationReflex
  def update
    model_instance = element.signed[:secure]
    channel = dom_id(model_instance) << "_messages"
    cable_ready[channel].console_log(message: "reflex update message").broadcast
  end
end 

This seems like it should work, but doesn't. It actually doesn't work in my case for 2 reasons:

  1. A hash prefix is added to the dom_id
  2. Also the dom_id is downcased which breaks for me because I'm using hashids which are mixed casing.

Helper generator Vite problems

The helpers_generator here fails with Vite:

main_folder = defined?(Webpacker) ? Webpacker.config.source_path.to_s.gsub("#{Rails.root}/", "") : "app/javascript"
filepath = [
"#{main_folder}/controllers/index.js",
"#{main_folder}/controllers/index.ts",
"#{main_folder}/packs/application.js",
"#{main_folder}/packs/application.ts"
]
.select { |path| File.exist?(path) }
.map { |path| Rails.root.join(path) }
.first

It assumes the main_folder is "app/javascript" and the entrypoint directory is named "packs".

A slight adjustment makes it work for either:

    main_folder = defined?(Webpacker) ? Webpacker.config.source_path.to_s.gsub("#{Rails.root}/", "") : "app/javascript"
++  main_folder = "app/frontend" if defined? ViteRuby

    filepath = [
      "#{main_folder}/controllers/index.js",
      "#{main_folder}/controllers/index.ts",
      "#{main_folder}/packs/application.js",
      "#{main_folder}/packs/application.ts",
++    "#{main_folder}/entrypoints/application.js",
++    "#{main_folder}/entrypoints/application.ts"
    ]

There are some similar install tasks in SR that also break in Vite but just need a very slightly different path. And there's this Webpacker-specific line that gets added during setup:

StimulusReflex.debug = process.env.RAILS_ENV === 'development'

which throws an error with Vite. It works with a slight (Vite-specific) adjustment:

--StimulusReflex.debug = process.env.RAILS_ENV === 'development'
++StimulusReflex.debug = import.meta.env.DEV

I'm wondering if these could be generalised a bit. I haven't tried with jsbundling-rails, but it also uses ESBuild so I assume it would be similar to Vite in regards to default paths..?

Originally posted by @Matt-Yorkley in #179 (comment)

Idea: ability to pass any object which responds to `to_dom_selector` as a selector

Trying out the cable_car functionality completely standalone (aka literally a blank folder with gem "cable_ready", "~> 5.0.0.pre1" in the Gemfileβ€”pretty cool that it works!), and right away I noticed that if I pass any random object in as a selector (custom object, Set, whatever), basically it ends up as whatever the to_json representation is in the dispatch. (I know dom_id is a thing but that only works for ActiveRecord.) Needless to say, the to_json output of most (all?) objects isn't actually anything suitable for a selector.

What would be really nifty is if there was some kind of check to see if the object responds to something CableReady is optional looking for, say, to_dom_selector. That way you could write this:

class FancyPants
  def to_dom_selector
    "#fancy-pants"
  end
end

cable_ready.inner_html(FancyPants.new, html: "<p>Very fancy!</p>")

Basically my goal is I never want to write selectors directly in CableCar operation statements, I'd always prefer those come from encapsulated components of one sort or another. (This is the same complaint I have with Turbo…I never want to write Turbo Frame IDs at the point I'm rendering Turbo Streams).

If there's interest in something like this, I could work on a PR, but I wanted to see what you think first. Thanks!

No such generator `cable_ready:stream_from`

Going through the docs which says to run the cable_ready:stream_from generator. But seems that doesn't exist πŸ€·β€β™‚οΈ

$ rails generate cable_ready:stream_from
Could not find generator 'cable_ready:stream_from'. (Rails::Command::Base::CorrectableError)

Same issue with the cable_ready:initializer generator.

Are the docs up to date?

Return object when `onMissingElement` is set to `exception`

Hello there,

I was wondering if it was possible to return an object when onMissingElement is set to 'exception'.

Sometimes I need to broadcast cards positioned according to a predefined order to my users. However, depending on what the user is looking for, the order may change or the card I'm using to position the new element with an insertAdjacentHtml may not be present on the page. I therefore use a specific method that allows me to deal with the error and find the position of the new card if necessary.

To make things easier, I'd need to know the operation that triggered the error, in order to retrieve the html and change the selector if necessary.

Something like this in cable_ready.js would be really great

const warning = `CableReady ${name ||
          ''} operation failed due to missing DOM element for selector: '${
          operation.selector
        }'`
       
switch (options.onMissingElement) {
     case 'ignore':
         break
     case 'event':
         dispatch(document, 'cable-ready:missing-element', {
              warning,
              operation
           })
           break
     case 'exception':
        throw { name: 'MissingDOMError', operation: operation }
     default:
          console.warn(warning)
      }
}

Thanks,
Antoine

Using an array with addCssClass does not update the classList correctly

While using cable_ready's addCssClass with an array of class names, the classList of the element(s) are updated incorrectly.
They are updated, but the class names are separated by a comma (,) instead of a space.

Eg:
Using

add_css_class(selector: '#foo', name: ["btn-warning", "pulsating"])

will update the '#foo' element's classList with btn-warning,pulsating instead of btn-warning pulsating

Additional info:
This error message is shown in the console
warning

shouldMorph doesn't work well with some form elements

First of all, I want to thank you for this awesome gem (and for Stimulus Reflex too). It has been fun using them!

I noticed that performing a full page morph is causing some issues with select and input tags. The first issue I ran into is that select tags that have a "blank" option selected are being cleared out even if their values are not changed. After some debugging I was able to connect these issues with the shouldMorph method.

I've created this repo to show the issues described here.

First issue: select tags are cleared out

select

Code

# app/views/tests/index.html.erb
<%= select_tag :foo, raw('<option>Test</option>'), prompt: 'Blank Option' %>
<%= select_tag :bar, raw('<option>Test</option>'), prompt: 'Blank Option', data: { reflex: 'change->test#change' } %>
# app/controllers/tests_controller.rb
class TestsController < ApplicationController
  def index
  end
end
# app/reflexes/test_reflex.rb
class TestReflex < ApplicationReflex
  def change
  end
end

This is what morphdom does if we don't use shouldMorph at all:

// This line gets called at some point, sending the current blank option and the blank option
// from the backend, which are the same
syncBooleanAttrProp(fromEl, toEl, "selected");

source

// This method will set the "selected" to the blank option
function syncBooleanAttrProp(fromEl, toEl, name) {
  if (fromEl[name] !== toEl[name]) {
    fromEl[name] = toEl[name];
    if (fromEl[name]) {
      fromEl.setAttribute(name, "");
    } else {
      fromEl.removeAttribute(name);
    }
  }
}

source

// Later on this method is called to morph the select input
SELECT: function (fromEl, toEl) {
    if (!toEl.hasAttribute("multiple")) {
      var selectedIndex = -1;
[...]
          if (nodeName === "OPTION") {

            // There's a child with selected here! It will change selectedIndex from -1 to 0

            if (curChild.hasAttribute("selected")) {
              selectedIndex = i;
              break;
            }
            i++;
          }
[...]
      // This works just fine because there's an option that has the "selected" attribute!
      // fromEl.selectedIndex = 0
      // selectedIndex = 0

      fromEl.selectedIndex = selectedIndex;
    }
}

source

Now this is what happens if we use shouldMorph from cable_ready:

var shouldMorph = function shouldMorph(permanentAttributeName) {
  return function (fromEl, toEl) {

    // current blank option is equal to the blank option from the backend, so the option is not morphed

    if (fromEl.isEqualNode(toEl)) return false;

source

// This gets called eventually
SELECT: function SELECT(fromEl, toEl) {
    if (!toEl.hasAttribute('multiple')) {
      var selectedIndex = -1;
[...]
          if (nodeName === 'OPTION') {

            // We don't have any child option with the "SELECTED" attribute set, so this never gets executed

            if (curChild.hasAttribute('selected')) {
              selectedIndex = i;
              break;
            }
[...]
      // This deselects the blank option because it didn't find any 'selected' option
      // fromEl.selectedIndex = 0
      // selectedIndex = -1

      fromEl.selectedIndex = selectedIndex;
    }
  }

source

Second issue: Input stays with the same value

This issue is similar to the one explained above, so I'm going to skip the details. This input is not being cleared out because shouldMorph returns false.

input

Code

# app/views/tests/index.html.erb
<%= select_tag :foo, raw('<option>Test</option>'), prompt: 'Blank Option', data: { reflex: 'change->test#change' } %>
<%= text_field_tag :baz, @baz %>
# app/controllers/tests_controller.rb
class TestsController < ApplicationController
  def index
    @baz = ""
  end
end
# app/reflexes/test_reflex.rb
class TestReflex < ApplicationReflex
  def change
  end
end

I think these issues might be part of the reason why LiveView is not using isEqualNode, as @snewcomer mentioned here.

Event Dispatch: help with serialization

Hi guys. Can you help me with a question?
I'm using cable_ready to dispatch a custom DOM event. When I retrieve event.detail it is in camelCase, I would like it to keep my attributes format. Example: user_id, not userId

   def trigger_dom_evt_new_driver_position(user_id, tracking)
     #...
     cable_ready[stream_name].dispatch_event(
        name: evt_id,
        detail:  { user_id: user_id, tracking: TrackingBlueprint.render_as_hash(tracking) },
        selector: "#operational-map-controller"
     )
     cable_ready.broadcast
   end

is there any configuration for serialization?

thank you very much

current_user concerns?

My apologies if this isn't the right place for this, but when playing with cableready over the weekend I ran into an issues where one of my partials is using a method current_user.is_admin?` to determine if a piece of code should be rendered on the frontend if the user has admin rights. I commented out the code just to continue with my exploration, however I wonder if anyone has considered how to handle user permissions with something like cableready?

Jest Error

(updated with more backtrace)\

After adding cable_ready package to my project, I can no longer run tests using jest. I get:

 .../node_modules/cable_ready/javascript/cable_ready.js:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import morphdom from 'morphdom'
                                                                                             ^^^^^^

    SyntaxError: Cannot use import statement outside a module

      at ScriptTransformer._transformAndBuildScript (node_modules/@jest/transform/build/ScriptTransformer.js:537:17)
      at ScriptTransformer.transform (node_modules/@jest/transform/build/ScriptTransformer.js:579:25)
      at Object.<anonymous> (node_modules/stimulus_reflex/dist/stimulus_reflex.js:1:123)

Code works when using it, I just can't run javascript tests anymore.

cable_ready 4.4.4

play_sound operation hijacks sound controls

In #98 a new play_sound operation is added. The PR describes:

First, it uses the worlds shortest viable MP3 (93 bytes!) to overcome Apple's annoying "click to play" constraint on Mobile Safari. This process should be transparent and invisible. It does this by capturing the first interaction with the page and using that to immediately start and then stop playback; this "unlocks" the DOM object used to play the sound, document.audio.

This method has some serious drawbacks:

  • With a strict Content Security Policy, the loaded audio file will generate an error on all pages:
Refused to load data:audio/mpeg;base64,... because it does not appear in the media-src directive of the Content Security Policy.
  • Because the tab now has audio, all audio controls on a Mac are now linked to the tab. Clicking "play" on a keyboard starts the MP3 file instead of controlling other audio sources.
  • When the page is opened on an iPhone, all other audio is paused to play the empty MP3 file. In our case it's a webview inside our native iPhone app.

We are forced to downgrade to 4.4.6, although we have no intention to use the play_sound operation. Maybe the audio for Mobile Safari hack should only be activated when using the play_sound operation?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.