Actioncable - an overview.
I am using jruby 9.2.16 (hence ruby 2.5.7) with rails 6.1.6.1.
I am not sure if only in development or only without ssl (wss) Actioncable can be used with the simple client side:
var ws = new WebSocket('ws://0.0.0.0:3000/channels');
ws.onmessage = function(e){ console.log(e.data); }
But at least I didn't get it running to do "Streaming from Channel" using wss in production, as it works locally (visible starting 'redis-cli' in terminal, and then 'monitor').
So I tried to implement the actioncable client scripts and hence 8 days got lost.
First I struggled with the fact that there is no description which is somehow complete. Many people publish particular solutions, but this is like gambling: maybe you are lucky.
Second, files are named in a way that seems to be general even though they are only about actioncable (the folder 'javascript', or 'application.js')
It is misleading to not call them 'actioncable_files' and 'actioncable_app.js' and voilĂ there are problems because of several files with the same name.
The next problem is, that lots has to be changed only, because the orderly structure of files is ignored. Javascripts no longer are in assets, but why?
They could be in assets/javascripts/actioncable/ ?
So manifest.js has to be changed, and even attributes have to be added in application.rb (attr_accessor :importmap). Something you find after some days in a forum.
but even more confusing: importmap is a gem, which requires some directories and which somehow has to be installed (rake app:update:bin, rails importmap:install) and someone wrote, the order of the gems were relevant but you cannot only deinstall actioncable gem, because rails depends on that. Dependencies could be organized using a permutation of priority.
importmaps is not working in firefox, so you need shims additionally
But in the end, everything what importmap does, looks like reinventing the wheel to me: it loads javascript files. Something, that can be done easily manually.
Also importing modules is possible in simple javascript. And what else then a javascript file shall it be, that the browser finally will work with?
Now importmap creates from a string '#rails/actioncable' another string 'actioncable.esm.js', which in my case (after 8 days of working 15 h to get it to work, still is not found automatically.
I cannot find that file, or any description where it is generated, or if it is only a link, or has to be compiled somehwere, but it looks to me, as if importmap is completely redundant and only makes things very complicated. I don't understand the benefit from writing a string 'xyz' which is translated in a laborious way to set up into another string 'xyz_2', which also might not be found. And if there are variables, they can be loaded directly using the same idea like action_cable_meta_tag.
Technically Actioncable is only doing what Faye did before. So why do we need all that so called "modern" way, which I think only is reinventing the wheel?
So, I would like to create a description on how to install actioncable in an easy way - without unnecessary tools and clearly.
But first I need to get it to work on my own. And therefore the question is: what shall I do due to:
GET http://0.0.0.0:3000/actioncable.esm.js net::ERR_ABORTED 404 (Not Found)
Thanks everyone for any idea!
I've never set up ActionCable before and never used it (except second hand through Turbo::Broadcastable). Seems like you had quite the journey. I used rails guides and rails github to set it up.
First of all, from importmap-rails:
Note: In order to use JavaScript from Rails frameworks like Action Cable, Action Text, and Active Storage, you must be running Rails 7.0+. This was the first version that shipped with ESM compatible builds of these libraries.
https://github.com/rails/importmap-rails#installation
Challenge accepted. I'll use mri for now (I'll try with your versions later, to see if anything weird comes up).
ActionCable
$ rails _6.1.6.1_ new cable --skip-javascript
$ cd cable
# https://github.com/rails/importmap-rails#installation
$ bin/bundle add importmap-rails
$ bin/rails importmap:install
# ActionCable guide seems rather crusty. Until this section:
# https://guides.rubyonrails.org/v6.1/action_cable_overview.html#connect-consumer
# A generator for the client side js is mentioned in the code comment.
$ bin/rails g channel chat
# Oops, generated server side rb as well.
# This should really be at the start of the guide.
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
# NOTE: just keep it simple
stream_from "some_channel"
end
end
The directory app/javascript seems generic, because it is. This is for all Javascript stuff, used by shakapacker, jsbundling-rails, importmap-rails and others. I've described it a bit here: https://stackoverflow.com/a/73174481/207090
// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
consumer.subscriptions.create("ChatChannel", {
connected() {
// NOTE: We have to check if our set up is online first,
// before chatting and authenticating or anything else.
console.log("ChatChannel connected")
},
disconnected() {},
received(data) {}
});
To broadcast a message, call this somewhere in your app [sic]:
ActionCable.server.broadcast("some_channel", "some message")
Ok, we have to make a controller anyway:
$ bin/rails g scaffold Message content
$ bin/rails db:migrate
$ open http://localhost:3000/messages
$ bin/rails s
Also, channels have to be imported somewhere, to load on the page. javascript_importmap_tags in the layout only imports application:
https://github.com/rails/importmap-rails#usage
<script type="module">import "application"</script>
<!-- ^ -->
<!-- this imports the pinned `application` -->
Makes sense to import channels in application.js. Can't import ./channels/index because it has require. We'd have to use node for it to work or do something else to import all the channels. Manual way is the simplest:
// app/javascript/channels/index.js
// NOTE: it works a little differently with importmaps that I haven't mentioned yet.
// skip this index file for now, and import channels in application.js
// app/javascript/application.js
import "./channels/chat_channel"
Browser console shows missing #rails/actioncable. Nobody told me to install it yet. Use pin command to add it:
https://github.com/rails/importmap-rails#using-npm-packages-via-javascript-cdns
$ bin/importmap pin #rails/actioncable
Refresh the browser:
ChatChannel connected chat_channel:5
We got our javascript on the page. Let's make it broadcast:
# app/controllers/messages_controller.rb
# POST /messages
def create
#message = Message.create(message_params)
ActionCable.server.broadcast("some_channel", #message.content)
end
<!-- app/views/messages/index.html.erb -->
<div id="chat"></div> <!-- output for broadcasted messages -->
<!-- since I have no rails ujs, for my purposes: bushcraft { remote: true } -->
<!-- v -->
<%= form_with model: Message.new, html: { onsubmit: "remote(event, this)" } do |f| %>
<%= f.text_field :content %>
<%= f.submit %>
<% end %>
<script type="text/javascript">
// you can skip this, I assume you have `rails_ujs` installed or `turbo`.
// { remote: true } or { local: false } is all you need on the form.
function remote(e, form) {
e.preventDefault();
fetch(form.action, {method: form.method, body: new FormData(form)})
form["message[content]"].value = ""
}
</script>
We know we're connected. The form is submitting to MessagesController#create without refreshing where we're broadcasting to "some_channel". All that's left is to do is output data on the page:
https://guides.rubyonrails.org/v6.1/action_cable_overview.html#client-server-interactions-subscriptions
// app/javascript/channels/chat_channel.js
// update received() function
received(data) {
document.querySelector("#chat")
.insertAdjacentHTML("beforeend", `<p>${data}</p>`)
}
ActionCable done. Now let's fix importmaps.
Importmaps
Something I didn't mention before and it is super important to understand.
Everything works, but, only in development, I explained why here:
https://stackoverflow.com/a/73136675/207090
URLs and relative or absolute paths will not be mapped and more importantly will bypass the asset pipeline sprockets. To actually use importmaps, all the files in app/javascript/channels have to be mapped, aka pinned, and then referred to only by the pinned name when importing.
# config/importmap.rb
# NOTE: luckily there is a command to help with bulk pins
pin_all_from "app/javascript/channels", under: "channels"
pin "application", preload: true
pin "#rails/actioncable", to: "https://ga.jspm.io/npm:#rails/actioncable#7.0.3-1/app/assets/javascripts/actioncable.esm.js"
# NOTE: the big reveal -> follow me ->------------------------------------------------------------------^^^^^^^^^^^^^^^^^^
# NOTE: this only works in rails 7+
# pin "#rails/actioncable", to: "actioncable.esm.js"
# `actioncable.esm.js` is in the asset pipeline so to speak and can be found here:
# https://github.com/rails/rails/tree/v7.0.3.1/actioncable/app/assets/javascripts
For some info on pin and pin_all_from:
https://stackoverflow.com/a/72855705/207090
You can see the importmaps this creates in the browser or in the terminal:
$ bin/importmap json
{
"imports": {
"application": "/assets/application-3ac17ae8a9bbfcdc9571d7ffac88746f5a76b18c149fdaf02fa7ed721b3e7c49.js",
"#rails/actioncable": "https://ga.jspm.io/npm:#rails/actioncable#7.0.3-1/app/assets/javascripts/actioncable.esm.js",
"channels": "/assets/channels/index-78e712d4a980790be34a2e859a2bd9a1121f9f3b508bd3f7de89889ff75828a0.js",
"channels/chat_channel": "/assets/channels/chat_channel-0a2f983da2629a4d7edef5b7f05a494670df3f99ec6a22a2e2fee91a5d1c1d05.js",
"channels/consumer": "/assets/channels/consumer-b0ce945e7ae055dba9cceb062a47080dd9c7794a600762c19d38dbde3ba8ff0d.js"
}# ^ ^
} # | |
# names you use urls browser uses
# | to import ^ to actually get it
# | |
# `---> importmaped to ----'
For importmaps info (not the importmap-rails gem):
https://github.com/WICG/import-maps
Importmaps do not import anything, they map name to url. If you make name look like url with /name, ./name, ../name, http://js.cdn/name there is nothing to map.
import "channels/chat_channel"
// stays unchanged and is now the same as
import "/assets/channels/chat_channel-0a2f983da2629a4d7edef5b7f05a494670df3f99ec6a22a2e2fee91a5d1c1d05.js"
// because we have an importmap for "channels/chat_channel"
You don't want to use the second form with an absolute path in you js files, because the digest hash changes on file updates to invalidate the browser cache (this is handled by sprockets).
Convert all the imports:
import consumer from "./consumer"
import "./channels/chat_channel"
to match the pinned names:
import consumer from "channels/consumer"
import "channels/chat_channel"
// import "channels" // is mapped to `channels/index`
// TODO: want to auto import channels in index file?
// just get all the pins named *_channel and import them,
// like stumulus-loading does for controllers:
// https://github.com/hotwired/stimulus-rails/blob/v1.1.0/app/assets/javascripts/stimulus-loading.js#L8
Jruby
Same setup on jruby. I just installed it and updated my Gemfile:
# Gemfile
ruby "2.5.7", engine: "jruby", engine_version: "9.2.16.0"
gem "activerecord-jdbcsqlite3-adapter"
gem "importmap-rails", "< 0.8" # after version 0.8.0 ruby >= 2.7 is required
First error, when starting the server:
NoMethodError: private method `importmap=' called for #<Cable::Application:0x496a31da>
importmap= method is defined here:
https://github.com/rails/importmap-rails/blob/v0.7.6/lib/importmap/engine.rb#L4
Rails::Application.send(:attr_accessor, :importmap)
In jruby it defines private methods when used this way:
>> A = Class.new
>> A.attr_accessor(:m)
>> A.new.m
NoMethodError (private method 'm' called for #<A:0x5f2f577>)
The fix is to override the definition in you app, or make those methods public:
# config/application.rb
module Cable
class Application < Rails::Application
# make them public
public :importmap, :importmap=
config.load_defaults 6.1
end
end
That's it. No other issues. You should expect some set backs anyway, because you're using jruby which is quite behind the mri. Ruby 2.5 EOL'd on Apr 05, 2021. You can't expect latest gems to play nice with old ruby versions.
I'm in the process of migrating a Rails 5.1.5 project, which uses CoffeeScript, from using sprockets to using webpacker. The project also uses select2.js. With sprockets, I did the following:
Install jquery-rails (jQuery is a dependency for select2).
Put select2.js code in vendor/assets/javscripts.
In application.js.coffee, add:
#= require select2
After that I was able to use select2 to in my application.js.coffee file:
$(document).on 'turbolinks:load' ->
$('select').select2
So far I've described the pretty standard way of including/using javascript libraries with sprockets.
However, with webpacker I can't make select2 work and I'm not sure why. I have two hypothesis:
I'm not importing/requiring it properly;
it doesn't find jQuery at some point of the load process;
So for jQuery, I did the following:
yarn add jquery
included in my environment.js:
const webpack = require('webpack');
environment.plugins.append('Provide', new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
}));
I've removed the jquery-rails gem, as well as #= require 'jquery' and tested that jquery works, so I guess I have correctly included it. However, I tried several ways of importing select2 (using es6 imports) and none of them worked. I tried:
import select2 from 'select2';
import select2 from 'select2/dist/js/select2'
import select2 from 'select2/dist/js/select2.js'
import 'select2/dist/js/select2.js'
I even tried to import it from the old vendor location by writing inside app/javascript/pack/application.js.coffee:
import '../../../vendor/assets/javascripts/select2'
I can confirm that the file contents is imported, as I put a console.log within the select2 file under node_modules/select2/dist/js/select.js and it did get printed. However, I also get the error TypeError: $(...).select2 is not a function when I execute $('select').select2() in the browser's dev tool console.
What am I missing or doing wrong?
P.S. I can provide much more info, but I didn't want my question to get any more bloated.
P.P.S. With my modest JS knowledge, I looked at the source code but couldn't recognize what exactly they are exporting and how am I supposed to import it.
I know this is an old post, but just in case someone else could benefit:
app/javascript/packs/application.js
...other requires...
require('select2')
window.Rails = Rails
import 'bootstrap'
...other imports...
import 'select2'
import 'select2/dist/css/select2.css'
$(document).on("turbolinks:load", () => {
$('.select2').select2()
})
My similar problem
I have stumble upon the same problem with another web component (Switchery):
I imported the component with yarn add switchery (no error)
I could import it correctly through WebPack with import 'switchery' (no error bundling the pack)
But when I was trying to use the Switchery object in the browser like they say in the doc:
var elem = document.querySelector('.js-switch');
var init = new Switchery(elem);
I would get the error: ReferenceError: Switchery is not defined
Note: I didn't want to install RequireJS as WebPack is supposed to do the same thing (and even better) nowadays.
My solution:
The problem was the webpack doesn't expose the pack-generated variables and classes in the global scope!
So to fix this, I needed to do two things:
Explicitly give a name to the imported class from Switchery:
import Switchery from 'switchery'
Use this Class only in the same JS file where the import was done
Testing hack:
If you want to try that out and "go back" to the mess that sprocket allowed, in the same file, you can expose "globally" the variable so you can use in from the browser:
import Switchery from 'switchery'
window.Switchery = Swicthery
now you can execute the switchery almost like in the example:
var init = new window.Switchery(elem);
Hope that helps...
In development, I can't figure out how to make the jquery-ui gem to make it's sweet magic available.
I included the gem in the project Gemfile, typed bundle install, updated app/assets/stylesheets/application.scss to have the line:
#import "jquery-ui/autocomplete";
and finally app/assets/javascripts/application.js to have the line:
//= require jquery-ui/autocomplete
more-or-less as described in https://github.com/joliss/jquery-ui-rails
When I try to load a page in development, rails complains saying
File to import not found or unreadable: jquery-ui/autocomplete.
Load paths:
/home/dm/contra/app/assets/images
/home/dm/contra/app/assets/javascripts
/home/dm/contra/app/assets/stylesheets
/home/dm/contra/vendor/assets/javascripts
/home/dm/contra/vendor/assets/stylesheets
/home/dm/.rvm/gems/ruby-2.2.1/gems/jquery-rails-4.0.5/vendor/assets/javascripts
/home/dm/.rvm/gems/ruby-2.2.1/gems/coffee-rails-4.1.0/lib/assets/javascripts
/home/dm/.rvm/gems/ruby-2.2.1/gems/angularjs-rails-1.4.8/vendor/assets/javascripts
/home/dm/.rvm/gems/ruby-2.2.1/gems/bootstrap-sass-3.3.6/assets/stylesheets
...
Here's what the directory layout of the jquery-ui-gem looks like on this machine:
contents of ~/.rvm/gems/ruby-2.2.1/gems/jquery-ui-rails-5.0.5/
app Gemfile History.md lib License.txt Rakefile README.md VERSIONS.md
cocntents of ~/.rvm/gems/ruby-2.2.1/gems/jquery-ui-rails-5.0.5/app/assets/javascripts/jquery-ui
accordion.js datepicker-da.js datepicker-fr-CH.js datepicker-ka.js datepicker-nn.js datepicker-ta.js effect-bounce.js effect-transfer.js
autocomplete.js datepicker-de.js datepicker-fr.js datepicker-kk.js datepicker-no.js datepicker-th.js effect-clip.js menu.js
button.js datepicker-el.js datepicker-gl.js datepicker-km.js datepicker-pl.js datepicker-tj.js effect-drop.js mouse.js
core.js datepicker-en-AU.js datepicker-he.js datepicker-ko.js datepicker-pt-BR.js datepicker-tr.js effect-explode.js position.js
datepicker-af.js datepicker-en-GB.js datepicker-hi.js datepicker-ky.js datepicker-pt.js datepicker-uk.js effect-fade.js progressbar.js
datepicker-ar-DZ.js datepicker-en-NZ.js datepicker-hr.js datepicker-lb.js datepicker-rm.js datepicker-vi.js effect-fold.js resizable.js
datepicker-ar.js datepicker-eo.js datepicker-hu.js datepicker-lt.js datepicker-ro.js datepicker-zh-CN.js effect-highlight.js selectable.js
datepicker-az.js datepicker-es.js datepicker-hy.js datepicker-lv.js datepicker-ru.js datepicker-zh-HK.js effect.js selectmenu.js
datepicker-be.js datepicker-et.js datepicker-id.js datepicker-mk.js datepicker-sk.js datepicker-zh-TW.js effect-puff.js slider.js
datepicker-bg.js datepicker-eu.js datepicker-is.js datepicker-ml.js datepicker-sl.js dialog.js effect-pulsate.js sortable.js
datepicker-bs.js datepicker-fa.js datepicker-it-CH.js datepicker-ms.js datepicker-sq.js draggable.js effect-scale.js spinner.js
datepicker-ca.js datepicker-fi.js datepicker-it.js datepicker-nb.js datepicker-sr.js droppable.js effect-shake.js tabs.js
datepicker-cs.js datepicker-fo.js datepicker-ja.js datepicker-nl-BE.js datepicker-sr-SR.js effect.all.js effect-size.js tooltip.js
datepicker-cy-GB.js datepicker-fr-CA.js datepicker.js datepicker-nl.js datepicker-sv.js effect-blind.js effect-slide.js widget.js
What am I failing to do right here?
You seem to be following the wrong README file. The one you are reading, is for Rails 5.0 and above.
Follow this doc for Rails versions before 5.0 - https://github.com/joliss/jquery-ui-rails/blob/v4.2.1/README.md
I've struggled with this in the past.. Just remove the /autocomplete part and leave the require and import to just jquery-ui and it will load the autocomplete by itself and the rest.
Got my autocomplete working with this coffeescript
jQuery -> $('#booking_product').autocomplete
source: ['']
I would like to use the material-ui component library in my Rails 4 app. I am currently using the react-rails gem to add .jsx compilation to the asset pipeline. I have added material-ui via rails-assets in the gemfile like so:
source 'https://rails-assets.org' do
gem 'rails-assets-material-ui'
end
And I have required the library in my application.js file like so:
//= require material-ui
However I keep getting the error "couldn't find file 'material-ui". How can I use the material-ui component library in my Rails app with the react-rails gem?
Ok so here is what I have working so far...
to the gemfile I have added:
gem 'react-rails'
gem "browserify-rails"
This gives us our react library, helpers and jsx compilation as well as the ability to use the require() sytax to require modules in our JS. browserify-rails also allows us to require npm modules in your Rails assets via a package.json file.
We can add the material-ui library to our app via this package.json file...
"dependencies" : {
"browserify": "~> 10.2.4",
"browserify-incremental": "^3.0.1",
"material-ui": "0.13.1"
},
The material ui library uses the require syntax to join all the different jsx component files together in the right order so this is why we need to use browserify-rails.
Next to keep our react code together I made a new directory in asset/javascripts called /react...
react
L /components
L react.js
L react-libraries.js
L theme.js
Now as part of 'material-ui' dependencies we have the react library. This means at the moment we have two copies of the library. One from the 'react-rails' gem and one from the 'material-ui' library dependencies from 'browserify-rails'. Lets use the one from 'material-ui' dependencies and leave the one from 'react-rails'.
in react.js:
//= require ./react-libraries
//= require react_ujs
//= require_tree ./components
Then in react-libraries.js
//React Library
React = require('react');
//Material Design Library
MaterialUi = require('material-ui/lib');
injectTapEventPlugin = require('react-tap-event-plugin'); injectTapEventPlugin();
//Material Design Library Custom Theme
MyRawTheme = require('./theme');
ThemeManager = require('material-ui/lib/styles/theme-manager');
Then we want to include all of this in the asset pipeline with...
//= require react/react
in application.js.
Now you can write your components in jsx files in /react/components/
You may also want to namespace your components with...
//Custom Components Namespace
Components = {};
in react-libraries.js
You can customise your theme in theme.js like this...
Colors = require('material-ui/lib/styles/colors');
ColorManipulator = require('material-ui/lib/utils/color-manipulator');
Spacing = require('material-ui/lib/styles/spacing');
module.exports = {
spacing: Spacing,
fontFamily: 'Roboto, sans-serif',
palette: {
primary1Color: Colors.grey300,
primary2Color: Colors.grey300,
primary3Color: Colors.lightBlack,
accent1Color: '#01A9F4',
accent2Color: Colors.grey100,
accent3Color: Colors.grey500,
textColor: Colors.darkBlack,
alternateTextColor: Colors.white,
canvasColor: Colors.white,
borderColor: Colors.grey300,
disabledColor: ColorManipulator.fade(Colors.darkBlack, 0.3)
}
};
Hope that helps :)
I am just getting started on a new project which I would like to write in ReactJS. I am trying to use Broserify to bundle everything so that I can have it in multiple js files.
However, when I try to bundle my react file (browserify main.js > bundle.js), I get this error:
"Error: Parsing file /Users/Kathleen/Documents/Referral_Site/main.js: Unexpected token (3:4)"
main.js looks like this:
var Lander = require('./lander');
React.render(
<Lander />,
document.getElementById('content')
);
What's breaking the parser? Thanks in advance!
EDIT: also, I included react in the surrounding html like this:
<script src="http://fb.me/react-0.12.2.js"></script>
is there some way that I need to include it in main.js as well?
You must compile JSX to plain JS first, there is plugin for browserify.
https://www.npmjs.com/package/reactify