- you want to add some javscript to a specific page
- you need to load that specific script after all the libraries are loaded
- you want to keep your application.js file at the bottom of the body
- you want to keep those page specific scripts from becoming one giant ball of immediately executed code
- you want to be able to test this code
- you need to pass some data to the page specific script
# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>JSKit Example</title>
<%= stylesheet_link_tag "application", media: "all" %>
<%= csrf_meta_tags %>
</head>
<body>
<%= yield %>
<%= javascript_include_tag "application" %>
<%= yield :page_specific_scripts %>
</body>
</html>
# app/views/pages/index.html.erb
<%= content_for :page_specific_scripts do %>
<%= javascript_include_tag "controllers/pages.js" %>
<script>
var pagesController = new App.PagesController("Hello <%= current_user.username %>");
pagesController.index(message);
</script>
<% end %>
This solves the problem and is pretty clean. However, it is a lot of boilerplate for every page that needs javascript. Not only that, your views need to know the details of your JavaScript implementation. We can do better. That's where JSKit comes in
# Gemfile
gem "rails_jskit"
// app/assets/javascripts/application.js
//= require lodash
//= require rails_jskit
//= require_tree ./controllers
# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>JskitExample</title>
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= csrf_meta_tags %>
</head>
<body>
<%= yield %>
<%= javascript_include_tag 'application' %>
<%= jskit %>
</body>
</html>
That's all the setup you need. Now to implement the previous functionality we simply need to create a JSKit pages controller.
// app/assets/javascripts/controllers/pages_controller.js
App.createController("Pages", {
actions: ["index"],
index: function(message) {
alert(message)
}
})
class PagesController < ApplicationController
def index
set_action_payload("Hello #{current_user.username}")
end
end
There's less boilerplate. We don't have to use the DOM to pass data into our JavaScript. We also don't have to worry about instantiating a controller object and calling the apropriate method. We've managed to get rid of the JavaScript from out templates.
We can also test the controller pretty easily:
// spec/javscripts/controllers/pages_controller_spec.js
describe("PagesController", function() {
var subject;
var dispatcher;
beforeEach(function() {
dispatcher = JSKit.Dispatcher.create()
subject = App.PagesController.create({ dispatcher: dispatcher })
})
describe("#index", function() {
it("alerts the message", function() {
expect(subject.index("Hello")).to.alert("Hello")
})
})
})
JSKit works by emitting events based on your current controller and action. There are 3 JSKit events emitted for each rendered page. These 3 events give you all the control you need over when a specific piece of JavaScript is executed. The previous example's events would look like this
App.Dispatcher.trigger("controller:application:all");
App.Dispatcher.trigger("controller:pages:all");
App.Dispatcher.trigger("controller:pages:index", "Hello some_username");
These events are triggered regardless of whether or not there is a JSKit controller created for any given Rails controller. In these cases, there is simply no listener for these events.
The App.createController
method creates an object from the provided definition and automatically wires it up to the events. The 3 events represent the scope of the JavaScript you wish to run.
If you create a JSKit controller named Application
, this controller's all
action will fire on every page. This allows you to execute "global" functions for every page in your application, indiscriminate of what page it's on.
App.createController("Application", {
actions: ["all"],
all: function() {
// this will execute on every page of your application
}
})
The second event is for when you need some specific code to run for all actions of a given controller. If a controller has an all
action. It will execute for every action of the controller, before the specific action code is executed.
App.createController("Pages", {
actions: ["all"],
all: function() {
// this will execute on every action of the PagesController
}
})
Finally you have the specific action event, which will execute code for the specific action
App.createController("Pages", {
actions: ["index"],
index: function() {
// this executes only on the Pages#index action
}
})
To pass data from your Rails controller to your JSKit controller, you have three available methods corresponding to the 3 events:
set_app_payload
set_controller_payload
set_action_payload
You can pass as many arguments as you want and they will automatically be converted to JSON and sent as arguments to their respective actions.
set_app_payload("From the ApplicationController", user)
set_controller_payload("From the PagesController", ["Hello", "World"])
set_action_payload("From the PagesController#index action")
Which will result in the following events:
App.Dispatcher.trigger("controller:application:all", "From the ApplicationController", { email: "[email protected]", uername: "someone"})
App.Dispatcher.trigger("controller:pages:all", "From the PagesController", ["Hello", "World"])
App.Dispatcher.trigger("controller:pages:index", "From the PagesController#index action")
App.createController("Pages", {
actions: [
"index",
{
create: "setupForm",
update: "setupForm",
edit: "setupForm",
new: "setupForm"
}
],
index: function() {
// do index stuff
},
setupForm: function() {
// do form stuff
}
})
Cacheing DOM elements
App.createController("Pages", {
elements: {
index: {
datePicker: ".datepicker",
searchField: ["#search", {
keyup: "handleSearchKeyup",
blur: "handleSearchBlur"
}],
completionList: ["#searchContainer", function(on) {
on("click", ".completion-option", this.handleCompletionOptionClick)
}]
}
},
index: function() {
this.$datePicker.datepicker()
},
handleSearchKeyup: function(evnt) {
// do some fancy autocompletion
var query = this.$seachField.val();
...
},
handleSearchBlur: function(evnt) {
// clean up for autocomplete
},
handleCompleteOptionClick: function(evnt) {
// handle selecting the option
}
})
- Focus on solving one problem
- Add as little as possible to make it valuable
- Don't write a "framework"
- Throw errors as early as possible with clear messages