Skip to content

Session 3 walkthrough

Tero Parviainen edited this page Apr 17, 2012 · 7 revisions

JavaScript is the lingua franca of the web. It is the most widely supported programming language in the world (it's in every web browser!), and has enabled the web to grow from a static collection of pages to the application platform it is today.

No webapp is complete these days without having at least some rich functionalities added with JavaScript. Today we're going to look at JavaScript and how it integrates to the Rails development workflow.

JavaScript Primer

JavaScript is a quirky little language. Its syntax and name might give the impression that it has something to do with Java, but it really doesn't. It's a completely different kind of beast, and in a moment we'll see how.

JavaScript is included in every (modern) web browser, and this means all of us also probably have at least a couple of JavaScript environments on our machines. Some of these environments also provide a JavaScript console, where we can type in JavaScript expressions and have them evaluated. We are going to use them today, so please make sure you have one of these installed:

  • Firefox with the Firebug extension. To launch Firebug after installation, click on the little bug icon that appears in your Firefox browser, in the top or bottom right hand corner depending on your Firefox version. The JavaScript console will be in the Console tab.
  • Google Chrome. Chrome has excellent build-in JavaScript development tools, which you'll find from Tools (the wrench icon) -> Tools -> Developer Tools. The JavaScript console will be in the Console tab.

Basic Expressions

In the JavaScript console, we can execute some basic JavaScript expressions:

"a string"
2 + 4
Math.PI * 2.0
"a string".toUpperCase()

The first JavaScript quirk we'll talk about today is that JavaScript is a loosely typed language, which basically means that for example Numbers can become Strings if used in a certain kind of expression:

1 + 2 + "3"  // "33"

One of the places this can trip you up is when comparing equality. JavaScript has the == operator, as you would expect:

3 == 3          // true
3 == 4          // false
"abc" == "abc"  // true

But this operator also does something called type coercion, where it may change the type of its arguments, which can result in unexpected results:

3 == "3"        // true

This is why it is almost in every case preferrable to use the "triple equals", or "strict equality" operator, which does not do type coercion:

3 === "3"       // false
3 === 3         // true

There is also a negated version:

3 !== 4         // true

document, console

In any JavaScript environment, there are a few global objects that can be used for getting information about the browser environment, and for executing different functionalities:

document

The document object represents the HTML document that's open in the browser. It provides access to all the different HTML elements of the document, and you can find them by their id, for example:

document.getElementById("query"); 

We will mostly do HTML manipulation through jQuery, and not use the document object directly.

console

The console object can be used as a development tool. It is present in most modern browsers, and the most useful thing we can do with it is insert logging statements to our code:

console.log("Hello")

These messages will show up in the Firebug or Chrome console. One thing to note is that you need to be careful with console statements, older browsers may not have the console object, and if you leave logging statements in, there will be errors. So only use them temporarily.

Functions

In JavaScript we can define a function using the function keyword:

function sayHello() {
  return "hello";
}

sayHello();

Functions can take arguments, as you would expect:

function sayHelloTo(name) {
  return "hello " + name + "!";
}

sayHelloTo("Joe");

What's different in JavaScript, compared to many other languages is that JavaScript has something called higher-order functions. That's a fancy way of saying that in JavaScript a function is just a variable. This can be seen more clearly by using another way of defining a function:

sayHello = function() {
  return "Hello!";
}

sayHello();

So, a function, just like a String or a Number, is really just an object, which you can put into a variable. This means you can also give functions to other functions as arguments:

function sayHelloTo(nameFunction) {
  return "hello " + nameFunction() + "!";
}

sayHelloTo(function() {
  return "Joe";
});

What this is most often used with is callbacks, you can set up some function to be called later when something happens, for example when the user clicks a button. For example, there is a function called setTimeout, which takes as arguments a function and a number, and it executes the function after some amount of time has passed. The time is defined as milliseconds:

setTimeout(function() {
  console.log("Executed!")
}, 2000);

Variables and The Global Scope

One of the most confusing aspects of JavaScript is how it treats variables. You can create a variable by assigning some value to a name:

myVar = "123";
myVar;

But actually, when you define a variable like this, it will be a global variable, which means it will be present in all your code. For example, if you define such a variable in a function:

function sayHello() {
  message = "Hello";
  return message;
}

The "message" variable will be define outside of the function once it has been called!

sayHello();
message;     // "Hello"

Variables declared like this are actually defined as members of the "window" object:

window.message;  // "Hello"

The window object in JavaScript is special - it is called the global object. When you're referring to a variable, if JavaScript cannot find it in your local scope, it will look for members of the window object and use them instead.

The way to correctly use variables is to always define them using the var keyword:

function sayHello() {
  var myMessage = "Hello";
  return myMessage;
}

This way their scope will be local to the function they were defined in:

sayHello();
myMessage;     // Reference error

jQuery Primer

Most of the JavaScript that we have in a typical application is about interacting with the HTML page in different ways - changing its contents, responding to events, and so on. Although web browsers provide all the APIs needed to do this, unfortunately there are differences in browsers for how to do things, and the APIs are also often not very user friendly.

jQuery is a JavaScript library that attempts to solve this problem: It provides a clean API for interacting with the HTML page, in a way that's consistent across browsers. jQuery is used all over the place, the last statistic I saw was that it's used on about 25 000 000 pages. Rails also ships with it by default.

Let's look at the basics, then. In jQuery, pretty much everything starts with a function just called $. This is a kind of HTML manipulation multi-tool, which can do a lot of different things based on what kind of arguments we give it.

Selectors

The most common use case for $ is element selection. We want to grab some HTML elements from the page in order to do something with them. The most simple selector is probably the id selector, which does the same thing as document.getElementById(), which we saw before:

$('#query')

So the id is prefixed with the hash sign. jQuery uses CSS syntax for element selection - so when selecting elements, you use the same kind of selector rules you would use in CSS files when you apply styling to elements.

We can also grab elements by their class:

$('.navbar')

Or just by their tag name:

$('input')

Or by some arbitrary attribute they might have:

$('input[type=hidden]')

There are also something called metaclasses, which are prefixed by colon. For example, to grab checkboxes or radio buttons that are checked, there's a :checked selector

$(':checked')

To combine rules to find elements that match all of them, just add them back to back:

$('input[type=radio]:checked')

You can also select elements that mach any of some group of selectors, by separating them with a comman, so for example to select all links and inputs:

$('a, input')

Manipulating elements

After we have selected some elements, we can manipulate them in various ways. We're going to look at just a couple of them, but there are many more which you can find in the jQuery documentation.

For example, we can replace the contents of some element with a new piece of HTML, by calling the html function:

$('a').html("<em>Link text</em>")

We can look at some element's attributes, by calling the attr function:

$('a.brand').attr('href')

We can also set those same attributes, by calling the attr function with two arguments:

$('a.brand').attr('href', 'http://www.eficode.com')

For form fields, we can get their value using the val function:

$('#query').val()

Or set it by calling the val function with an argument:

$('#query').val('Steppenwolf')

Events

One of the most useful things we can do with elements is to somehow respond to user events, like clicking. jQuery provides functions for attaching all kinds of event handlers, for example - click events:

$(document).on('click', '.navbar', function() {
  console.log('navbar clicked');
})

We attach an event handler on the document object, tell it what event to listen to, and which elements we are interested in by giving a selector. So, the previous statement says: Call this function when anything that matches the '.navbar' selector is clicked.

If you have used jQuery before, this style of attaching event handler may be a bit different from what you've seen before. By attaching event handlers to document, instead of attaching them to the elements directly, we make it a lot easier to work with Ajax and with pages whose content may change over time. We think this is the style that should be preferred.

In event handler, the element that originated the click can be found in the "this" variable:

$(document).on('click', '.navbar', function() {
  console.log(this);
})

If we want to do some jQuery processing with that element, we need to wrap it in the jQuery function, for example to change its style:

$(document).on('click', '.navbar', function() {
  $(this).html("Clicked!");
})

Adding some Spice to the Search Form

Let's try these things out by doing a small improvement to our search form: The search input field currently has a placeholder text saying "Search by title". But we really have three different fields we can search by, which we can choose by those three radio buttons. It would be nice if that placeholder text reflected the radio we have chosen.

First, let's refactor the search field a little bit: We can use an HTML5 search input instead of a normal text field, because that supports a placeholder attribute, which has the benefit that it goes away once you start typing some actual search value to the field.

Find this line in app/views/books/index.html:

<%= text_field_tag :query, params[:query] || 'Search by title' %>

and change it to:

<%= search_field_tag :query, params[:query], placeholder: 'Search by title' %>

We will add the JavaScript to the books JavaScript file, because this is about searching books and that's the most natural place for it. Rename app/assets/javascripts/books.js.coffee to books.js (we're not using CoffeeScript on this course), and remove the comments from it so that the file is empty.

We should wrap our code to the jQuery load function, so that the code is executed only after the page has loaded. Let's just first add a log statement in to check that our code is executing:

In app/assets/javascripts/books.js:

$(function() {
  console.log('books.js initializing!');
});

We should see the message in the browser's JavaScript console when the page loads.

We need to respond to the 'click' event in those three radio buttons, and change the search field text accordingly. To make that easier, let's add a CSS class for each of those.

In app/helpers/books_helper.rb, find the line:

output << (radio_button_tag :by, field, checked)

and add a class attribute to it:

output << (radio_button_tag :by, field, checked, class: 'query-by')

Exercise 1 answer

Let's first attach a click handler to those radio buttons, first with just a logging statement to check that they work.

In app/assets/javascripts/books.js

$(document).on('click', '.query-by', function() {
  console.log('clicked!');
});

Next, let's try to grab their value with the val() function and see what it contains:

$(document).on('click', '.query-by', function() {
  var value = $(this).val();
  console.log('clicked ' + value);
});

Then, let's just grab the search field from the page, and set its placeholder attribute using the attr function:

$(document).on('click', '.query-by', function() {
  var value = $(this).val();
  var searchField = $('#query');
  searchField.attr('placeholder', 'Search by '+value);
});

jQuery in Rails

Rails not only ships with jQuery, but it also provides an integration layer to it, to make many things easier. If we look at our HTML source, we'll find a script tag there for a file called jquery_ujs.js. This file includes some jQuery code, which interacts with some of the Rails-generated HTML elements to enable some interesting interactions.

A good example is the "method" attribute we have in some of our links. For example, in our _reservation_links.html.erb partial, we have a line like this:

<%= link_to "Free", free_book_reservation_path(book, reservation), method: :put, class: 'btn' %>

Links in HTML always perform a GET request - there is no standard way to do anything else. But this link actually uses the PUT method. How Rails makes this work is through the jQuery integration code. If we look at the HTML that's generated by this line:

<a href="/books/2/reservations/2/free" class="btn" data-method="put" rel="nofollow">Free</a>

We see this data-method attribute. What the jquery_ujs.js file does is that it looks for links on the page that have this data-method attribute, and attaches a click listener to them. When they are clicked, it constructs a little form with the correct method, and then submits that form, instead of executing the default link action. We can see this code in jquery_ujs.js file line 160, in the handleMethod() function. This means we can construct such links without writing a lot of JavaScript handler code ourselves.

There are several things like this in jquery_ujs.js, many of them related to Ajax as we'll see in a moment. A simple one is the "confirm" attribute. If we invoke a link helper with the confirm attribute, it will produce a data-confirm attribute in the HTML link, which will cause jquery_ujs.js to add a confirmation dialog to that link. Let's add this to our book deletion link.

In app/views/books/show.html.erb

<%= link_to "Delete", book_path, method: :delete, class: 'btn btn-danger', confirm: 'Are you sure?' %>

You can study the jquery_ujs.js file. It's quite readable, and you'll learn a lot about jQuery!

Ajax with The Asset Pipeline (slides 4-5)

The asset pipeline is a great way to organize, preprocess, obfuscate, and compress JavaScript code. It's really a huge improvement compared to working with static files.

However, we're not quite using it to full extent yet. The main problem is the way we're currently doing Ajax.

If you recall, we're returning a JavaScript view from an Ajax action, which is then executed on the page. We currently have one of these, in app/views/reservations/create.js.erb.

One of the problems here is that our JavaScript code is scattered all over the place. In a large application, there can be all of these little JavaScript snippets in many different places, making things quite difficult to maintain.

There are also practical issues here. One of them can be clearly seen, if we introduce a JavaScript bug to a view like this.

In app/views/reservations/create.js.erb

$('#reservation-form').hidde();

When we click the "Reserve" button, what happens? Nothing! There was a JavaScript error but we can't see it anywhere. It does not show up in the console. This can make things really difficult to debug.

The asset pipeline and the jquery_ujs.js integration provide an alternative way of doing Ajax. What we can do is have all of our JavaScript code in the JavaScript assets - compiled, preprocessed beforehand - and then just move data in the Ajax requests, which will then be processed by the JavaScript code.

The key to this is that when we specify a link as being remote, the Rails jQuery adapter will fire a bunch of events on that link when its Ajax call is being executed. We can see how this works by adding some log statements.

First, let's add a class to the reserve link so we can grab it in jQuery. In app/views/books/_reservation_links.html.erb:

<%= link_to "Reserve", book_reservations_path(book), class: 'btn reserve-link', remote: true, method: :post %>

Then, let's add the callback functions in app/assets/javascripts/reservations.js (first rename the file):

$(function() {

  $(document).on('ajax:beforeSend', '.reserve-link', function() {
    console.log('beforeSend');
  });

  $(document).on('ajax:success', '.reserve-link', function() {
    console.log('success');
  });

  $(document).on('ajax:complete', '.reserve-link', function() {
    console.log('complete');
  });

  $(document).on('ajax:error', '.reserve-link', function() {
    console.log('error');
  });

});
  • The ajax:beforeSend event is fired before the Ajax request is sent
  • The ajax:success event is fired after the response is received, if it was successful
  • The ajax:error event is fired after the response is received, if it resulted in an error
  • The ajax:complete event is fired after the response is received, whether it succeeded or not.

When we click the link now, we should be seeing the beforeSend, error, and complete events. The reason we get the error is because we broke our create.js.erb code before. We can actually see the error by refactoring our error listener to take some arguments and log the error:

document.on('ajax:error', '.reserve-link', function(evt, request, status, error) {
  console.log(error);
}

The arguments are the error event object, the Ajax request, the HTTP status code of the response, and the actual error, respectively.

So, what we actually want to do is to execute the operations we currently have in create.js.erb in the event handler in reservations.js, and just return some JSON data from the server. Let's do the JSON bit first.

We need to indicate that the data format we actually want is JSON. We can do that with the type attribute in the link, which actually generates a data-type attribute in the HTML. In app/views/books/_reservation_links.html.erb:

<%= link_to "Reserve", book_reservations_path(book), class: 'btn reserve-link', remote: true, data: {type: :json}, method: :post %>

Then, let's change our action implementation, so that it is able to respond to a JSON request. In reservations#create, replace the format.js line with:

format.json { render json: @reservation.to_json(include: :user) }

The include part is about also including the associated user object in the JSON, so we can grab its email.

We can also now remove the create.js.erb file - it is no longer used.

Let's change our success callback to log what the response actually was, in app/assets/javascripts/reservations.js:

document.on('ajax:success', '.reserve-link', function(evt, data, status, xhr) {
  console.log(data);
})

It's logging the reservation as a JavaScript object. Exactly what we need! Let's finish the implementation by adding the appropriate operations in the success handlers, and by removing the event handlers we're not actually using. In app/assets/javascripts/reservations.js:

$(document).on('ajax:success', '.reserve-link', function(evt, data, status, xhr) {
  $('#reservation-form').hide();
  $('#reservation-status').html('<span class="label important">Reserved by '+data.user.email+'</span>');
  $('#reservation-links').html('<a href="/books/'+data.book_id+'/reservations/'+data.id+'/free" class="btn" data-method="put">Free</a>');
});

You might notice we are now constructing the HTML by hand, instead of using Rails partial templates. On one hand, we are not mixing our Ruby code with our JavaScript code, giving us a nice separation of concerns. We also have full control of the Ajax lifecycle, and can actually see any errors there might be.

On the other hand, constructing HTML in JavaScript strings gets quite cumbersome very quickly. Indeed, for many people the next step when doing this style of Ajax is to incorporate a JavaScript templating framework like Handlebars to get some more power into client side HTML generation.

Testing JavaScript

As today's final main topic, let's talk a little bit about testing JavaScript. We already have pretty good tests for our Ruby code, but not really anything for our JavaScript code. It's not a huge problem yet since we don't have much, but typically modern web projects end up having lots and lots of JavaScript to drive all the dynamic behavior - and where there's lots of code, tests are also needed.

What makes JavaScript testing challenging is that so much of it is really interacting with the HTML page, and needs it to be there to be able to run. So it's really difficult to run JavaScript code in isolation. We should really run JavaScript tests in a web browser.

This has been an unsolved - or at least just partially solved - problem for a long time, but fortunately lately tools have started to crop up that solve this problem. A tool we're going to use today is called Jasmine. It's a JavaScript testing framework, which we can run in the browser, and also can integrate to the Rails asset pipeline very nicely.

In Jasmine, the tests are actually called "specs" (they're written in the behavior-driven style, see the resources for more information). So that's what we're going to refer to them by for the rest of the session.

We need to add one gem called "jasminerice" to our Gemfile. This is a Rails Jasmine integration library, which also includes the Jasmine framework itself. We're going to need this lib in both development and test environments, so let's add a section for gems that are in both groups:

group :development, :test do
  gem 'jasminerice'
end

Run bundle install and restart the Rails server after you've done this.

We can run our Jasmine specs by opening the address http://localhost:3000/jasmine in a web browser. If we open it now, we'll see that each and every one of our 0 specs is currently passing.

Jasmine specs should be put in directory spec/javascripts in the Rails project, so create that directory first. Next, let's add the entry point to our specs, in a file called spec/javascripts/spec.js

//= require jquery
//= require jquery_ujs
//= require_tree ./

This should look familiar - jasminerice actually works with the asset pipeline, so we can use it to include any JavaScript libraries and files we need! Here we are including jQuery, the jQuery rails integration, and then every file under the spec/javascripts directory.

Now, let's add our first spec. Create a file called spec/javascripts/books_spec.js, with the following contents:

describe("books.js", function() {

  it("should work", function() {
    expect("abc").toEqual("abc");
  });

});

Here we see the Jasmine API in action:

  • Specs are grouped in "describe" functions (which is similar to a context in Shoulda)
  • Each spec goes in an "it" function (which is similar to a should in Shoulda)

We're not really testing anything interesting yet, just that two equal strings are actually equal, but we should already see one spec passing if we reload the jasmine page in the web browser.

If we look at the actual contents of books.js, it's really all about interacting with the HTML page. So we need such a HTML page also in the specs. In Jasminerice, there's a facility called "fixtures" for exactly this. We can craft an HTML page with the kinds of elements we need for our specs, and then check our code against that in the spec.

Create the directory spec/javascripts/fixtures, and add the file spec/javascripts/fixtures/book_search.html with these contents:

<form id="search-form">
  <input id="query" type="search" placeholder="Initial">

  <input id="query-by-title" class="query-by" type="radio" value="title" checked>
  <input id="query-by-isbn" class="query-by" type="radio" value="isbn">

</form>

<div id="book-list">
</div>

Notice how we're not fully reproducing the HTML contents of the search form here - we're only interested about the parts that matter from the script's point of view.

Next let's add a before function to our specs, which will load this fixture (inside the describe function):

beforeEach(function() {
  loadFixtures("book_search");
});

Now we can update our spec to check something about the fixture:

it("should work", function() {
  var queryField = $('#query');

  expect(queryField.attr('placeholder')).toEqual("Initial");
});

We can actually execute jQuery against the fixture!

We don't really want to test the fixture, though, we want to test our JavaScript code. So let's first pull that in by requiring it via the asset pipeline in books_spec.js:

//= require books

The first interesting fact about books.js that we want to test is that when a query radio is clicked, it sets the placeholder text of the query field to its value. So let's add a spec for that:

it("should set the search field placeholder to match the value of a clicked radio", function() {
  $('#query-by-isbn').click();

  expect($('#query').attr('placeholder')).toEqual("Search by isbn");
});

We can also add tests for Ajax search behavior - we have not implemented this yet so the tests will fail, but once you have done the homework exercise they should start passing.

So the first thing to check is that the script should submit the search form when a radio is clicked, if there is some value in the search field. We can do this by attaching a Jasmine spy as an event listener for the form's submit event, and then checking that it was called:

it("should submit the search form when a radio is clicked, if the search field is not empty", function() {
  var submitSpy = jasmine.createSpy().andReturn(false);
  $('#search-form').on('submit', submitSpy);

  $('#query').val('my search term');
  $('#query-by-isbn').click();

  expect(submitSpy).toHaveBeenCalled();
});

The "spy" here is really something called a "mock". It is a JavaScript function, which always returns false, which we specified with the andReturn call. We can also ask it if it has been called, so with this approach we are actually able to check that a form was submitted by "spying" on its submit event.

We can also add another test case for the case when the search term is empty and the form should not be submitted:

it("should not submit the search form when a radio is clicked, if the search field is empty", function() {
  var submitSpy = jasmine.createSpy().andReturn(false);
  $('#search-form').on('submit', submitSpy);

  $('#query').val('');
  $('#query-by-isbn').click();

  expect(submitSpy).not.toHaveBeenCalled();
});

Finally, we can check the behavior that replaces the contents of the book list when the form receives a successful Ajax response. For this, we can just artificially trigger the event:

it("should replace the contents of the book list when the search form gets a response", function() {
  var newHtml = "<h2>New contents</h2>";

  $('#search-form').trigger('ajax:success', newHtml);

  expect($('#book-list').html()).toEqual(newHtml);
});

Solution for Homework Exercise: Ajax Search

First we need to make some modifications to the search form. We want it to be remote, we're going to request the html data type, and we'll also add an id so we can refer to the form via jQuery. In app/views/books/index.html.erb

form_tag search_books_path, method: :get, id: 'search-form', remote: true, data: {type: :html} do %>

Next, we're going to wrap the book listing on the index page to a div, so we can later replace its contents via Ajax. In app/views/books/index.html.erb:

<div id="book-list">
  <% if @books.size > 0 %>
     ...
  <% end %>
</div>

Let's also extract the contents of book-list to a new partial called _list.html.erb, and then just render that in the div:

<div id="book-list">
  <%= render 'list' %>
</div>

In the search action, we want to just render the partial if the request is an Ajax request, and otherwise just render the index action as before. In books#search:

if request.xhr?
  render partial: 'list'
else
  render action: :index
end

We also need an Ajax handler for the form, which knows what to do when the Ajax response arrives. In app/assets/books.js, let's first grab the search form, and then add an ajax:success listener to it:

$('#search-form').on('ajax:success', function(evt, data, status, xhr) {
  $('#book-list').html(data);
});

Finally, to re-execute the search automatically when one of the radio buttons are clicked, let's modify the click handlers to submit the form if needed:

if (searchField.val() !== '') {
  $('#search-form').submit();
}

Bonus material: pjax

here's one more Ajax technique that we're going to talk about today, and that's a really powerful and easy one, which mostly has to do with the performance and loading speed of our application.

When we look at the HTML source of our application, it's obviously divided into two sections: head and body. Head includes things like the page title, and all the inclusion tags for JavaScript and CSS. The body includes the actual contents of the page.

What's noticeable is that the head section doesn't really change on every page - it's always the same. Even though that's true, we're still loading the whole head section for each page, which causes the browser to also reload all the JavaScript and CSS files, which takes time. It isn't really necessary, and pjax fixes exactly that.

What happens with pjax is that normal link clicks are transformed into Ajax requests, so that instead of a full page load, just a subsection of the page is loaded. It also changes the page address in the browser's location bar with JavaScript, with the HTML5 pushState API. In browsers that don't support the pushState API, pjax just disables itself and we get a full pageload instead.

There's an official Rails pjax integration gem, which makes all of this really easy. So let's see how that works.

First, let's add the gem to Gemfile. We'll use the latest version from Github, since that has some new features we're going to need:

gem 'pjax_rails', :git => 'git://github.com/rails/pjax_rails.git'

Remember to run bundle install and to restart the rails server once you've done this.

Then let's include the pjax files into our assets, by requiring them from app/assets/javascripts/application.js

//= require pjax

With pjax, we need to wrap the part of the page we want to replace with a special div. What the Rails integration gem does is it automatically renders all pjax requests without the page layout. So, with these things in mind, what we need to do is, in our layout, wrap the part that includes the page contents into the pjax div. In app/views/layouts/application.html.erb

<div data-pjax-container>
  <%= yield %>
</div>

Now we have blazingly fast link navigation!

But there's a couple of things we still need to do.

Firstly, our navigation links and our flash messages are currently rendered in the layout. Those are not things that stay the same on every page, so we need to somehow get them into the content replaced by pjax.

We basically need to add a separate layout for pjax, which will render the navbar and the flash, but not the head section. But let's first extract the navbar and the flash into partials, which can be included to both of our layouts.

Create the directory app/views/application. This is a good place for partials that are not specific to any controller, but global to the application. Create two partials in here: _navbar.html.erb, and _flash.html.erb. Then extract the navbar and the flash rendering from application.html.erb to these files. Finally, render the partial in application.html.erb, inside the data-pjax-container div:

<div data-pjax-container>
  <%= render partial: 'navbar' %>
  <%= render partial: 'flash', locals: {flash: flash} %>
  <%= yield %>
</div>

To add the pjax layout, in ApplicationController, let's override the pjax layout like so:

protected 

def pjax_layout
  'pjax'
end

Then create a new layout file app/views/layouts/pjax.html.erb, and add the contents:

<%= render partial: 'navbar' %>
<%= render partial: 'flash', locals: {flash: flash} %>
<%= yield %>

And now everything works as it should!

Resources

Something went wrong with that request. Please try again.