JavaScript Engine

Contributing an additional JavaScript library to Padrino is actually quite straightforward. For this guide, let's look at how the jquery component is integrated into Padrino; a new library follows the same steps.

Generators

Every file in padrino-gen/lib/padrino-gen/generators/components/scripts/ is a choice for --script, named after the file. There's no list of components to edit: adding scripts/mylib.rb is enough for padrino g project --script=mylib and, in an existing project, padrino g component --script=mylib. Both read the file and then call its setup_script method.

Here is the integration of jQuery into the generator, in padrino-gen/generators/components/scripts/jquery.rb:

# padrino-gen/lib/padrino-gen/generators/components/scripts/jquery.rb

def setup_script
  begin
    get('https://raw.githubusercontent.com/padrino/padrino-static/master/js/jquery.js',  destination_root('/public/javascripts/jquery.js'))
    get('https://raw.githubusercontent.com/padrino/padrino-static/master/ujs/jquery.js', destination_root('/public/javascripts/jquery-ujs.js'))
  rescue StandardError
    copy_file('templates/static/js/jquery.js',  destination_root('/public/javascripts/jquery.js'))
    copy_file('templates/static/ujs/jquery.js', destination_root('/public/javascripts/jquery-ujs.js'))
  end
  create_file(destination_root('/public/javascripts/application.js'), '// Put your application scripts here')
end

This downloads the latest version of the library and its unobtrusive JavaScript adapter into the public/javascripts folder of the project, and constructs the application.js file. Without a network connection, it copies the versions that ship with the gem instead.

The library itself doesn't live in padrino-gen: it lives in padrino-static, in js/ for the library and ujs/ for the adapter. padrino-gen includes padrino-static as a git submodule at padrino-gen/lib/padrino-gen/generators/templates/static, which is where the templates/static/... paths above point. So the first step for a new library is a pull request to padrino-static that adds, for example, js/mylib.js.

padrino g component also runs setup_script in existing projects, where people have edited their files. For a file like application.js, pass skip: true to create_file so that a re-run keeps it. The jquery component doesn't, so a re-run asks whether to overwrite application.js.

Tests

Let's also add a test to ensure the new JavaScript component generates as expected in padrino-gen/test/test_project_generator.rb:

# padrino-gen/test/test_project_generator.rb
describe 'the generator for script component' do
  it 'should properly generate for jquery' do
    out, = capture_io { generate(:project, 'sample_project', "--root=#{@apptmp}", '--script=jquery') }
    assert_match(/applying.*?jquery.*?script/, out)
    assert_file_exists("#{@apptmp}/sample_project/public/javascripts/jquery.js")
    assert_file_exists("#{@apptmp}/sample_project/public/javascripts/jquery-ujs.js")
    assert_file_exists("#{@apptmp}/sample_project/public/javascripts/application.js")
  end
  ...
end

padrino-gen/test/test_component_generator.rb has tests for padrino g component, for example for running it again on a project that already uses jquery.

The tests don't download anything: stub_static_files in padrino-gen/test/helper.rb answers the padrino-static URLs from a list of files, and any other request fails the test. Add js/mylib.js and ujs/mylib.js to that list. Run the tests with:

$ cd padrino-gen
$ bundle exec ruby -Ilib -Itest -I../padrino/test test/test_project_generator.rb -n '/script component/'

README

Finally, let's update the README for padrino-gen to reflect the new component in padrino-gen/README.rdoc:

# padrino-gen/README.rdoc
script::     none  (default), jquery, prototype, mootools, dojo, vite

Unobtrusive JavaScript Adapter

Although optional, you can also provide a unobtrusive JavaScript (UJS) adapter which provides 'remote' and 'method' support to a project using a particular JavaScript framework. For more information about UJS, check out the UJS Helpers guide.

To support UJS in a given JavaScript framework, simply create a new file such as ujs/mylib.js in your padrino-static fork and then follow the UJS adapter template used by the jQuery implementation. It listens on document with jQuery's .on, so links and forms added to the page later are handled too:

// ujs/jquery.js

/* Remote Form Support
 * form_for @user, '/user', :remote => true
**/

$(function(){
    $(document).on('submit', 'form', function(e) {
      // ...
    });

    /* Confirmation Support
     * link_to 'sign out', '/logout', :confirm => 'Log out?'
    **/
    $(document).on('click', 'a[data-confirm]', function(e) {
      // ...
    });

    /* Link Remote Support
     * link_to 'add item', '/create', :remote => true
    **/
    $(document).on('click', 'a[data-remote=true]', function(e) {
      // ...
    });

    /* Link Method Support
     * link_to 'delete item', '/destroy', :method => :delete
    **/
    $(document).on('click', 'a[data-method]:not([data-remote])', function(e) {
      // ...
    });

    /* JSAdapter */
    var JSAdapter = {
      // Sends an xhr request to the specified url with given verb and params
      // JSAdapter.sendRequest(element, { verb: 'put', url : '...', params: {} });
      sendRequest: function(element, options) {
        // ...
      },
      // Triggers a particular method verb to be triggered in a form posting to the url
      // JSAdapter.sendMethod(element);
      sendMethod: function(element) {
        // ...
      }
    };

    // Same-origin requests that change state are sent along with the CSRF token.
    $.ajaxPrefilter(function(options, originalOptions, xhr) {
      // ...
    });
});

Generally the only changes need to be made in the JSAdapter JavaScript module specifically to implement the sendRequest and sendMethod functions that are used by all the events to power the UJS functionality. Keep the CSRF handling: sendMethod adds the token from the page's csrf-param and csrf-token meta tags to the form it submits, and the adapter sends the X-CSRF-Token header only on same-origin requests that change data.

Once that unobtrusive adapter has been implemented, copy it in setup_script next to the library, as the jquery component does with jquery-ujs.js:

# padrino-gen/lib/padrino-gen/generators/components/scripts/mylib.rb
def setup_script
  begin
    get('https://raw.githubusercontent.com/padrino/padrino-static/master/js/mylib.js',  destination_root('/public/javascripts/mylib.js'))
    get('https://raw.githubusercontent.com/padrino/padrino-static/master/ujs/mylib.js', destination_root('/public/javascripts/mylib-ujs.js'))
  rescue StandardError
    copy_file('templates/static/js/mylib.js',  destination_root('/public/javascripts/mylib.js'))
    copy_file('templates/static/ujs/mylib.js', destination_root('/public/javascripts/mylib-ujs.js'))
  end
  create_file(destination_root('/public/javascripts/application.js'), '// Put your application scripts here', skip: true)
end

and check for the adapter in the test, as the jquery test does with jquery-ujs.js.

Contribute to Padrino

This completes the full integration of a JavaScript library into Padrino. Once all of this has been finished in your GitHub forks, send us pull requests (one to padrino-static for the library and adapter, one to padrino-framework for the component, its tests, the README, a line in CHANGES.rdoc and a bump of the templates/static submodule to the padrino-static commit that adds the library) and, assuming you followed these instructions properly and the library actually works when generated, we will include the component into the next Padrino version crediting you for the contribution!

The commit that added the vite script component in 0.17.0 (from @osbre's #2334) is a recent example: the component file, its project and component generator tests, the README and the CHANGES.rdoc line.

Type to search the guides.