These days you have way too many options, tools, gems, transpilers, compilers, modules and frameworks to make javascript work with your Rails application. I wanted a simple and useful way to keep my code clean and organized.
I use several concepts which I will be explaining in the following sections. I hope you will find it helpful for your projects.
First of all, some heads-ups, I still use Sprockets and the Assets Pipeline, I believe we will all be using Webpack in the near future but so far, my experience with it brought me more pain than gain.
I've dropped jQuery and embraced ES2015, Sprockets didn't play nice with ES2015 Modules so I keep on using the Revealing Module Pattern, which works pretty well for me.
In summary,
- No jQuery
- No Webpack
- Plain ECMAScript
- Sprockets-Asset Pipeline
- No ES Module syntaxes
- No ES classes
And these are the topics I tend to cover in the next posts
- Namespacing
- Revealing Module Pattern
- Mutation Observer
- Fetch
- HTML & M10n
- AJAX
- Form submission
I've created these modules to be used in a Rails application but they can also be uses in a plain HTML/JS page.
[1] Namespacing
One of the issues you may have if you are not careful writing JavaScript is polluting the global space. As your app becomes bigger you may start suffering name collisions in your code.
In order to keep my code clean, I use namespaces and directories structure,
└─ myApp/
└── assets/
├── javascripts/
│ ├── myApp/
│ │ ├── controllers/
│ │ ├── core/
│ │ └── dom/
│ ├── myApp.init.js
│ └── myApp.js
└── application.js
Where myApp is the name of the Rails application. Inside /assets/javascripts/ you can use any name, I usually use the same application name. So far, I use three main directories controllers, core and dom, I will explain later how I use them.
Finally I use two initialization files myApp.js and myApp.init.js.
The application.js file is the standard sprockets manifesto file.
The Root File - myApp.js
The only responsibility of this file is to set all the namespaces. The convention is one namespace per directory.
window.myApp = {};
window.myApp.Core = {};
window.myApp.DOM = {};
window.myApp.Controllers = {}; `
The Manifest - application.js
As said before, this is the standard Sprockets manifesto file. We don't have anything yet to include from within the directories, only the root files. myApp.init file will be explained in the next section, Revealing Module Pattern.
It is worth mentioning that the first required file is vendor.js. This is a manifest file I add in a vendor directory at the Rails app root. I find very usefull to keep all 3rd party scripts separated.
//= require vendor
//= require myApp/myApp
//= require myApp/myApp.init
//= require myApp/dom/...
//= require myApp/core/...
//= require myApp/controllers/...
If you are including the 3rd party scripts in this fashion, remember to add the path to the Assets paths in your assets.rb file
Rails.application.config.assets.paths << Rails.root.join("vendor", "assets")
[2] Revealing Module Pattern
I won't be explaining this pattern here. You can find all you need to know at Todd Motto's blog - Mastering The Module Pattern.
In the scaffolding below, myApp is the name of your application (or the name of your choice) and myNS is the namespace where you want your module to be, don't forget to create the directory in the file tree and create the namespace in the root file.
I called the module with window.myApp so I have all the functionality available without polluting the global space.
window.myApp.myNS.myModule = ((myApp) => {
const settings = {
settingName: settingValue
};
const actionName = () => {
// do something
};
return {
settings, publicActionName: actionName
};
})(window.myApp);
If you need to use JQuery you can include jQuery as a parameter in the module and have its functionality available within the module. It would go like this,
window.myApp.myModule = ((myApp, $) => { })(window.myApp, window.jQuery);
I always add a settings hash at the beginning of every module, I find it useful for configuring the module behaviour. Then come the actions, this is where the stuff is done.
Finally, the return section of the pattern, where the private is made public. You may want to change the name, if you wish to keep the same name just write it once, privateName: privateName is not necessary.
Initialization file - myApp.init.js
The initialization file allows you to run any required process before anything else.
Again, myApp is the current namespace and Init is the module name. The initialization file is one of the simplest modules. On its more basic form, it has only one goal, to kick off the Observer. This nice guy will enable us to do very useful stuff but, we will talk about it in the next section. Needless to say, I've added a method to be executed at the action start for illustrative purporses. Notice I kept the _welcomeMessage method private, it won't be available for the rest of the modules.
As mentioned before, this is your opportunity to add any action you wish to execute before anything else. This is the first piece of code to be executed and the only one I run by myself. The rest of the code will be triggered by events, this is not a limitation of the framework, you can run any module code at any time.
The last three lines of code is where I execute the start();
action.
window.myApp.Init = ((myApp) => {
const settings = {
message: "Starting myApp..."
};
const _welcomeMessage = (msg) => {
console.log(msg);
};
const start = () => {
_welcomeMessage(settings.message);
myApp.Core.Observer.start();
};
return {
start, settings
};
})(window.myApp);
(function(myApp) {
myApp.Init.start();
}(window.myApp));
[3] Mutation Observer
As with the Revealing Module Pattern, I won't be explaining the Observer itself, but how to use it. You can find very good information at the Mozilla Developer Network - MutationObserver.
The observer allows to watch any changes in the DOM tree and I use it to create Event Listeners.
Observer.js
The Observer is part of the Core namespace. As every file, it follows the revealing module pattern.
Below the complete code of the module. I will go step by step.
window.myApp.Core.Observer = ((myApp) => {
const settings = {
selector: 'controller',
triggerSeparator: '->',
controllerSeparator: '#',
renderCustomEvent: 'render'
};
const start = (selector = settings.selector) => {
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
const target = document;
const config = { childList: true, subtree: true };
const observer = new MutationObserver((mutations) => {
let traversedNodes = new Array();
for (let currentMutation = 0; currentMutation < mutations.length; currentMutation++) {
if (mutations[currentMutation].type === 'childList' && mutations[currentMutation].addedNodes.length) {
for (let currentAddedNode = 0; currentAddedNode < mutations[currentMutation].addedNodes.length; currentAddedNode++) {
if (mutations[currentMutation].addedNodes[currentAddedNode].nodeType === 1) {
let traversedNodeFlag = false;
for (let currentTraversedNode = 0; currentTraversedNode < traversedNodes.length; currentTraversedNode++) {
if (traversedNodes[currentTraversedNode] === mutations[currentMutation].addedNodes[currentAddedNode].parentNode) {
traversedNodeFlag = true;
break;
}
}
traversedNodes.push(mutations[currentMutation].addedNodes[currentAddedNode]);
if (traversedNodeFlag !== true) {
let behavioralNodeList = mutations[currentMutation].addedNodes[currentAddedNode].querySelectorAll(`[data-${selector}]`);
for (let currentBehavioralNode = 0; currentBehavioralNode < behavioralNodeList.length; currentBehavioralNode++) {
let behaviors = behavioralNodeList[currentBehavioralNode].dataset[settings.selector].trim().split(/\s+/);
for (let currentBehavior = 0; currentBehavior < behaviors.length; currentBehavior++) {
let trigger = behaviors[currentBehavior].substring(0, behaviors[currentBehavior].lastIndexOf(settings.triggerSeparator)); // trigger->controller#action
let controller = behaviors[currentBehavior].substring(trigger.length + 2, behaviors[currentBehavior].lastIndexOf(settings.controllerSeparator));
let action = behaviors[currentBehavior].substring(trigger.length + controller.length + 3);
if (window.myApp.Controllers[controller] === undefined) throw new Error(`Controller 'window.myApp.Controllers.${controller}' is not defined`);
if (window.myApp.Controllers[controller][action] === undefined) throw new Error(`Action 'window.myApp.Controllers.${controller}.${action}' is not defined`);
behavioralNodeList[currentBehavioralNode].addEventListener(trigger, window.myApp.Controllers[controller][action], false);
if (trigger == settings.renderCustomEvent) behavioralNodeList[currentBehavioralNode].dispatchEvent(new CustomEvent(settings.renderCustomEvent));
}
}
}
}
}
}
}
});
observer.observe(target, config);
};
return {
settings,
start
};
})(window.myApp);
It is a bit lengthy but it is actually very simple.
As every module, it has its settings hash which allows to customize a few options.
The module has only one action, start(selector) which is the one we call from the initialization file. This is how it starts. The selector parameter will be used to identify which DOM elements are candidates for event binding.
const start = (selector = settings.selector)
The next lines are for setting up the Mutation Observer object.
First, I setup the right interface according to the browser the visitor is using. The target is the DOM node the Observer will be observing, in this case, I want to observe the whole document. At last, I set what kind of changes I want to observe. The observer allows to observer attributes and even content changes. I only need to observe changes in the DOM tree, the default values are already false, but I'd rather be explicit.
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
const target = document;
const config = { childList: true, subtree: true, characterData: false, attributes: false };
Next, the observer is created. It uses a callback function with a parameter called mutations which registers the observed mutations. It follows an ugly bunch of nested loop-ifs, it traverses all the mutations in mutations, if the mutation is of the type childlist and the addedNodes is not empty, it means that a new node was created in the DOM tree. So I loop through all the added nodes. Then, I check the nodeType of the current added node is an ELEMENTNODE and avoid anything else (TEXT, CDATA, DOCUMENTTYPE, etc).
If the parentNode of the current node has already been examined, I skip the current node since it was already examined as a child node. This way I avoid duplicating event listeners on the same elements.
I push the value of the new traversed node to an array so it is considered in the next loop run.
If the node has not been traversed before, I examined it looking for [data-${selector}], this means if I keep the default setting, I look for elements with [data-controller] data attribute from the current node down to all its children nodes. That's why I can avoid this step when traversing the children.
const observer = new MutationObserver((mutations) => {
let traversedNodes = new Array();
for (let currentMutation = 0; currentMutation < mutations.length; currentMutation++) {
if (mutations[currentMutation].type === 'childList' && mutations[currentMutation].addedNodes.length) {
for (let currentAddedNode = 0; currentAddedNode < mutations[currentMutation].addedNodes.length; currentAddedNode++) {
if (mutations[currentMutation].addedNodes[currentAddedNode].nodeType === Node.ELEMENT_NODE) {
let traversedNodeFlag = false;
for (let currentTraversedNode = 0; currentTraversedNode < traversedNodes.length; currentTraversedNode++) {
if (traversedNodes[currentTraversedNode] === mutations[currentMutation].addedNodes[currentAddedNode].parentNode) {
traversedNodeFlag = true;
break;
}
}
traversedNodes.push(mutations[currentMutation].addedNodes[currentAddedNode]);
The next lines are pretty simple, I loop over the Node List from the querySelectorAll, then I split, trim and substring the attribute value. By default, set in settings, the naming convention is triggerName->controllerName#actionName, if the controller or action is not defined, it throws an error, if it is all OK, an event listener is added.
Finally, if the event is renderCustomEvent, the event is dispatch immediately after adding the listener. This allows to run your action after the DOM element is rendered.
if (traversedNodeFlag !== true) {
let behavioralNodeList = mutations[currentMutation].addedNodes[currentAddedNode].querySelectorAll(`[data-${selector}]`);
for (let currentBehavioralNode = 0; currentBehavioralNode < behavioralNodeList.length; currentBehavioralNode++) {
let behaviors = behavioralNodeList[currentBehavioralNode].dataset[settings.selector].trim().split(/\s+/);
for (let currentBehavior = 0; currentBehavior < behaviors.length; currentBehavior++) {
let trigger = behaviors[currentBehavior].substring(0, behaviors[currentBehavior].lastIndexOf(settings.triggerSeparator));
let controller = behaviors[currentBehavior].substring(trigger.length + 2, behaviors[currentBehavior].lastIndexOf(settings.controllerSeparator));
let action = behaviors[currentBehavior].substring(trigger.length + controller.length + 3);
if (window.myApp.Controllers[controller] === undefined) throw new Error(`Controller 'window.myApp.Controllers.${controller}' is not defined`);
if (window.myApp.Controllers[controller][action] === undefined) throw new Error(`Action 'window.myApp.Controllers.${controller}.${action}' is not defined`);
behavioralNodeList[currentBehavioralNode].addEventListener(trigger, window.myApp.Controllers[controller][action], false);
if (trigger == settings.renderCustomEvent) behavioralNodeList[currentBehavioralNode].dispatchEvent(new CustomEvent(settings.renderCustomEvent));
}
}
}
The module finishes with its return section, making public the action start and it settings.
return {
settings,
start
};
[4] Fetch
Another Core module is the one which handles all AJAX calls. Since the development of Promises, Async/Await and Fetch, javascript provides very simple tools to create our own AJAX calls without the usage of any library.
As I did with the Observer, I show the whole module and then go into details.
window.myApp.Core.Ajax = ((myApp) => {
const settings ={
method: 'GET',
body: null,
options: {
mode: 'cors',
cache: 'no-cache',
credentials: 'include',
headers: {"Accept": "application/json"},
redirect: 'follow',
referrer: 'about:client'
},
callbacks: {
runBeforeSend: (event) => {
// code to run always before the call
},
runOnFailure: (failure) => {
console.log("myApp.Core.Ajax failed Fetch.", failure);
},
runOnError: (response) => {
console.log('myApp AJAX Response Error: ', response);
},
runOnSuccess: (response) => {
console.log('myApp AJAX Response Success: ', response);
},
runOnComplete: (event) => {
// code to run always after the call
}
}
};
const fetch = async ({url, event, callbacks = settings.callbacks,
method = settings.method, body = settings.body, options = settings.options}) => {
let init = {
method: method,
body: body,
mode: options.mode,
cache: options.cache,
credentials: options.credentials,
headers: options.headers,
redirect: options.redirect,
referrer: options.referrer
};
event.preventDefault();
callbacks.runBeforeSend(event);
try {
let response = await window.fetch(url, init);
let data = await response.json();
response.data = data;
response.ok ? callbacks.runOnSuccess(response) : callbacks.runOnError(response);
}
catch (failure) {
callbacks.runOnFailure(failure);
}
callbacks.runOnComplete(event);
};
return {
settings, fetch
};
})(window.myApp);
Again the now standard revealing module pattern is used. The settings hash includes values to be used by the fetch action which uses the ES fetch api. I've set 'GET' as the default method, null for body, and a series of options wich allow to work with json. You can review the options at MDN.
const settings ={
method: 'GET',
body: null,
options: {
mode: 'cors',
cache: 'no-cache',
credentials: 'include',
headers: {"Accept": "application/json"},
redirect: 'follow',
referrer: 'about:client'
},
...
};
The next setting defines the callback. You have 5 stages to run your callbacks.
- runBeforeSend
This callback will be run before executing the fetch api call. - runOnFailure
This callback will in case of failure. Most of the time, network or server problems. The fetch api call cannot be successfully completed. - runOnError
The main difference with runOnFailure is that the call is completed but the response is not OK. This means the response HTTP code is not in the 200s. - runOnSuccess
The call has been completed successfully. The response HTTP code is in the 200s. - runOnComplete
This is always executed after the call.
callbacks: {
runBeforeSend: (event) => {
// code to run always before the call
},
runOnFailure: (failure) => {
console.log("myApp.Core.Ajax failed Fetch.", failure);
},
runOnError: (response) => {
console.log('myApp AJAX Response Error: ', response);
},
runOnSuccess: (response) => {
console.log('myApp AJAX Response Success: ', response);
},
runOnComplete: (event) => {
// code to run always after the call
}
}
Finally, the fetch action. This action is an asynchronous function with 6 parameters. I use named parameters when the functions has more than 3. It can get too verbose but it saves me time when reusing a function after a long time and it also avoids misplacing arguments.
I set the init hash used by the fetch api, for further details on this options, please review MDN.
I prevent the execution of the default for the event and execute the callback runBeforeSend.
Within the try block I catch any exception thrown by fetch and execute the runOnFailurecallback.
I call await window.fetch with the defined url and init. Await allows to wait for fetch to return a promise. This promise is converted to json to extract the data.
If the response HTTP code is in the 200s (response.ok?), runOnSuccess is executed, otherwise I execute runOnError.
Once the try block is completed, runOnComplete is executed.
const fetch = async ({url, event, callbacks = settings.callbacks,
method = settings.method, body = settings.body, options = settings.options}) => {
let init = {
method: method,
body: body,
mode: options.mode,
cache: options.cache,
credentials: options.credentials,
headers: options.headers,
redirect: options.redirect,
referrer: options.referrer
};
event.preventDefault();
callbacks.runBeforeSend(event);
try {
let response = await window.fetch(url, init);
let data = await response.json();
response.data = data;
response.ok ? callbacks.runOnSuccess(response) : callbacks.runOnError(response);
}
catch (failure) {
callbacks.runOnFailure(failure);
}
callbacks.runOnComplete(event);
};
The return section of the module includes settings and fetch action.
The usage of the action Core.Ajax.fecth is really simple and will be shown in the next chapters.
[5] HTML & M10n
A few comments about how I organize my html code.
First, I use HAML in my Rails applications, I find it much more readable and quicker to write than HTML and, they are equivalent.
Based on the Atomic Design Methodology, I used a much simpler approach. All my html files are consisted of one of two types, Containers and Block. They all have an id with a prefix, c- and b-, respectively.
Any container and block can include others containers and blocks, there are no restrictions on that. The only simple rule I follow is that you can only change the contents of a container while blocks are replaced as a whole.
This is how it looks like,
<div id="c-document">
<div id="c-header">
<div id="b-title">
Welcome
</div>
<div id="b-subtitle">
Containers and Blocks
</div>
</div>
<div id="c-body">
<div id="b-content">
Lorem Ipsum Dolor Sit Amet Lorem Ipsum Dolor Sit Amet
</div>
</div>
<div id="c-footer">
Thanks for reading
</div>
</div>
In Rails every container and block is actually a partial which are updated asynchronously.
M10n and how to update your content
I created a new module within the DOM namespace. I called it M10n (Manipulation) and in its basic form it only includes two actions updateContainer and replaceBlock.
The code is really simple, I use the settings to remove the prefixes from the id and change the innerHTML for containers and outerHTML for blocks. If the prefix is missing/wrong, I throw an exception.
window.myApp.DOM.M10n = ((myApp) => {
const settings = {
containerPrefix:'c-',
blockPrefix: 'b-'
};
const updateContainer = (id, contentHTML) => {
if (id.substring(0, settings.containerPrefix.length) === settings.containerPrefix) {
document.getElementById(id).innerHTML = contentHTML;
}
else {
throw `Error: Container ID must begin with '${settings.containerPrefix}'`;
}
};
const replaceBlock = (id, newBlockHTML) => {
if (id.substring(0,settings.blockPrefix.length) === settings.blockPrefix) {
document.getElementById(id).outerHTML = newBlockHTML;
}
else {
throw `Error: Block ID must begin with '${settings.blockPrefix}'`;
}
};
return {
settings, updateContainer, replaceBlock
};
})(window.myApp);
[6] AJAX
I have been explaining different modules in the previous posts and now it is time to show how I use all this functionality on a Rails app. The example is extremely simple, the user clicks on a button and a HTML container is updated.
main.html
This file is the template. You can set a Rails Controller Action to render it as root in your routes.rb file.
The button has a data-controller attribute, click->Sample#changeContainer. It means, the Observer will add a listener to this element for the event click which will execute the Action changeContainer of the Sample Controller.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>
Javascript in Ruby on Rails
</title>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
<%= csrf_meta_tags %>
<body>
<div id="c-root">
<div id="c-top">
<button data-controller="click->Sample#changeContainer">
Button
</button>
</div>
</div>
<div id="c-bottom">
<div id="c-output">
This is the Output container
No output yet.
</div>
</div>
</body>
</head>
</html>
_b-output.html
This is the partial we are going to render from the RoR Controller to update de template.
<div id="b-output">
Content has been modified.
</div>
sample.js
This is the javascript Controller. It has one Action, changeController. I use the url '/show' which needs to be added to the Rails routes.rb file to redirect the call to the proper Rails Controller#Action.
It takes the default callbacks and modifies the runOnSuccess so it can update the container with the html_content generated by the Rails Controller. Then, I call the fetch action from the Core.Ajax module.
window.myApp.Controllers.Sample = ((myApp) => {
const changeContainer = (event) => {
let url = '/show';
let callbacks = myApp.Core.Ajax.settings.callbacks;
callbacks.runOnSuccess = (response) => {
Jrm.DOM.M10n.updateContainer('c-output', response.data.html_content);
};
myApp.Core.Ajax.fetch({url: url, event: event, callbacks:callbacks});
};
return {
changeContainer
};
})(window.myApp);
sample_controller.rb
The Rails Controller has a show action. [Side note: I always try to keep the Rails Controllers with only REST actions: index, show, new, create, edit, update, destroy. If I need a different action, it always means that a new controller is required]
The show action just renders the partial in json format and sets the HTTP code as 200 (:accepted). This json is the response the fetch method will get and use to update the container contents.
class SampleController < ApplicationController
def show
respond_to do |format|
format.json do
render(
json: {
html_content: render_to_string(
formats: [:html],
partial: 'b-output',
layout: false,
locals: {}
)
},
status: :accepted
)
end
end
end
end
I haven't included the manifesto root and initialization files, these files are required to make all this work.
[7] Forms
In the previous section, I showed you how I handle AJAX requests with a simple example. The example was a Get request to update the contents of a DIV. In this last section I will show how I process forms submissions with AJAX.
Core.Form
The Core.Form module includes only one action, submit. Its code is pretty simple. It has one condition, the DOM element (buttons most of the times) that launches the event must be within the form.
The Core.Form.submit action uses the Core.Ajax.fetch action define in the previous posts. The only difference is how I set the parameters for fetch.
URL, method and body, are set from the form which is identified from the event's currentTarget. This is why the currentTarget has to be within the form. It is possible to change this and identify the form with other mechanisms but I find this way very userful.
window.myApp.Core.Form = ((myApp) => {
const settings ={
};
const submit = (event, callbacks) => {
let form = event.currentTarget.form;
let url = form.attributes.action.value;
let method = form.attributes.method.value;
let body = new FormData(form);
myApp.Core.Ajax.fetch({url: url, event: event, callbacks: callbacks, method: method, body: body});
};
return {
settings, submit
};
})(window.Jrm);
The controller calling this action should setup the callbacks and pass them over to Core.Form.submit.
Conclusions
All these basic patterns and modules gives you the foundations to start creating on top of it all your Controllers to manage and manipulate your Rails templates. As you start using them, you will be able to identify and add more base functionality to the core modules and keep your controllers DRY.
You will be increasing the separation of your JS code from your templates codes and keep them logically organized in Controller and Actions.
I hope you find this approach helpful for your next initiatives.
Feel free to share your comments or questions.