Skip to content

Instantly share code, notes, and snippets.

@daytonn
Last active December 1, 2017 14:44
Show Gist options
  • Save daytonn/38e08e52ef50584e2e94fa0dc1d0d31a to your computer and use it in GitHub Desktop.
Save daytonn/38e08e52ef50584e2e94fa0dc1d0d31a to your computer and use it in GitHub Desktop.
JSKit Presentation

JSKit

The problems:

  1. you want to add some javscript to a specific page
  2. you need to load that specific script after all the libraries are loaded
  3. you want to keep your application.js file at the bottom of the body
  4. you want to keep those page specific scripts from becoming one giant ball of immediately executed code
  5. you want to be able to test this code
  6. you need to pass some data to the page specific script

Ad-hoc solution:

# 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

JSKit Setup

# 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")

Bells and Whistles

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
	}
})

Guiding Principals

  1. Focus on solving one problem
  2. Add as little as possible to make it valuable
  3. Don't write a "framework"
  4. Throw errors as early as possible with clear messages
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment