Skip to content

Instantly share code, notes, and snippets.

@ulziibat-n
Forked from PJoy/guide.md
Created March 19, 2020 12:58
Show Gist options
  • Save ulziibat-n/a29d66d9e0fa7f58a24930a5b70b8099 to your computer and use it in GitHub Desktop.
Save ulziibat-n/a29d66d9e0fa7f58a24930a5b70b8099 to your computer and use it in GitHub Desktop.
SAGE BARBA

Using barba.js with Sage starter theme

Barba.js is a library using pushState and AJAX allowing websites to load pages asynchronously while still maintaining browser states and history.

Combining it with some JavaScript animations libraries can help craft some memorable browsing experiences.

alt text source

In this guide, we will try to find an optimal way to integrate Barba with our beloved Sage theme !

Note : This guide is using Sage v9.0.9 & Barba v2.9.7

Table of contents

Barba Hello, world !

First, let's try to accomplish a simple Hello, world ! which won't display the famous line but instead initialize Barba behaviour for our website.

1 - Install barba

$ yarn add @barba/core

2 - Define wrapper and container

alt text

wrapper and container are concepts specific to Barba.

The content included in the wrapper but not in the container will not change throughout browsing.

The content included in the container will be removed and loaded dynamically when links are clicked.

We will specify these blocs using data-* attributes in our main template file.

<!-- app.blade.php -->

<html {!! get_language_attributes() !!}>
  <!-- ... -->
  <body @php body_class() @endphp data-barba="wrapper">
    <!-- ... -->
    <div class="wrap container" role="document" data-barba="container">
        <!-- ... -->
    </div>
        <!-- ... -->
  </body>
</html>

Now Barba will know what parts to load dynamically once we have it initialised.

3 - Add Barba to our JS

For now we will put our Barba code in common.js so that it is initialised on every page. We will see later how we can optimise code structure.

// common.js

import barba from '@barba/core';

export default {
  init() {
    barba.init();
  },
  finalize() {
  },
};

Go try it, your code should be working now ! Loading a new page shouldn't prompt the browser to reload while still loading the content and changing the active url :

alt text

It's not working !

  • check that data-* attributes were effectively added to the HTML code
  • check that Barba is installed in your_theme/node_modules/@barba/core
  • check that Barba is initialised
  • try to take a break and come back 😁

Animate the transition

Now that Barba is up and running, let's try to implement a simple fade-out / fade-in effect for each container load (ie. new page load).

There are many libraries implementing animations, I personally really like anime.js by Julian Garnier but for the sake of consistency with Barba documentation we will use GSAP in this guide.

GSAP is a standard library for web animation but it's unfortunately not open-source.

Start by installing the library.

$ yarn add gsap

Then import it in your code.

// common.js

import barba from '@barba/core';
import gsap from 'gsap';

export default {
  init() {
    barba.init({
       /* ... */ 
    });
  },
  finalize() {
  },
};

First try

First, let's add a basic transition that we will chose to use for all pages (we will see later how to define different transitions for different pages).

We will specify the enter and leave functions, defining what will happen when the current container is removed (leave) and when the next container is loaded (enter).

We can access current container with

data.current.container

and next page's container with

data.next.container

We will implement a fade-out on the current container followed by a fade-in on the next container using GSAP functions .to() and .from(). Note that the second argument is the duration of the animation in seconds (GSAP .to() doc here).

// common.js
// ...

barba.init({
  transitions: [
    {
      name: 'basic',
      leave: function (data) {
        gsap.to(data.current.container, 1, {opacity: 0,});
      },
      enter: function (data) {
        gsap.from(data.next.container, 1, {opacity: 0,});
      },
    },
  ],
});

alt text

I tried it and the fade-in works properly but the first container is instantly deleted !

Indeed ! leave and enter are immediately executed so we can't see the leave animation.

Second try

Barba proposes many ways to implement synchronicity / asynchronicity, let's just pick one and not bother ! You are free to explore Barba docs to use the one you prefer.

Using this.async() here makes it so that Barba waits for GSAP callback to call the enter function.

// common.js
// ...

barba.init({
  transitions: [
    {
      name: 'basic',
      leave: function (data) {
        gsap.to(data.current.container, 1, {opacity: 0, onComplete: this.async(),});
      },
      enter: function (data) {
        gsap.from(data.current.container, 1, {opacity: 0, onComplete: this.async(),});
      },
    },
  ],
});

alt text

I'm getting close but it still doesn't work ! It seems like the first container stays on top of the other for a while before being deleted...

Yes, the first container is only removed after the whole transition is complete, it will stay on the page until then, unless we find a way to remove it !

Last try

You can access the parentNode of the container then remove it like so :

// common.js
// ...

barba.init({
      transitions: [
        {
          name: 'basic',
          leave: function (data) {
            gsap.to(data.current.container, 1, {opacity: 0, onComplete: this.async(),});
          },
          enter: function (data) {
            // Remove the old container
            data.current.container.parentNode.removeChild(data.current.container);
            gsap.from(data.current.container, 1, {opacity: 0, onComplete: this.async(),});
          },
        },
      ],
});

Note that this problem wouldn't have happened if we didn't use this.async() in enter function but this way is cleaner and leaves more room for future implementation of complex transitions.

alt text

Perfect !

Namespaces

If you plan to use Barba on your website, I suppose you're planning on doing fancier things than fading in/out for transitions and more importantly, you will want to have different transitions for different pages.

Luckily, Barba can help you with that !

1 - Adding namespaces in templates

namespace is a simple Barba concept allowing you to define groups of pages for which Barba will have the same behaviour.

For now let's just define the namespace according to the current post_name for the sake of the example. (In the future you might want to replace it by a specific function taking into account post types, categories, page names, etc.)

<!-- app.blade.php -->

<html {!! get_language_attributes() !!}>
  <!-- ... -->
  <body @php body_class() @endphp data-barba="wrapper">
    <!-- ... -->
    <div class="wrap container" role="document" data-barba="container" data-barba-namespace="{{$post->post_name}}">
        <!-- ... -->
    </div>
        <!-- ... -->
  </body>
</html>

2 - Using namespaces in JS

In your JS code, you will now be able to write specific Barba transitions for your own cases :

  • when you're going to a specific page : to
  • when you're coming from a specific page : from
  • a combination of both cases : to + from

Note : this has nothing to with GSAP .to() and .from() functions !

Here is a simple implementation :

// common.js
// ...

barba.init({
      transitions: [
        {
          name: 'basic',
          /* ... */
        } , {
          name: 'to-some-page',
          to: {
            namespace: ['some-page'],
          },
          leave: function (data) {
            /* ... */
          },
          enter: function (data) {
            alert('this is some page');
            /* ... */
          },
        },
      ],
});

I called my transition to-some-page here but the name of the transition doesn't matter, only the namespaces are taken into account. Also note that namespace takes an array as an argument so you can specify multiple namespaces at once.

Load page-specific JavaScript

Here is where things begin to get a little bit more complicated.

From this point we were able to :

  • setup Barba on our site
  • setup animations
  • setup multiple transitions for different namespaces

But we saw that Barba only loads content contained inside the containers, so what about JS code for each page which is located after the global <footer> and therefore not loaded nor executed after each transition ?

We will see what we can do about that but first, let's take a look at Sage JS routing system

Sage JS routing system

How it works

Sage implements a simple routing system for JavaScript files which does basically 2 things :

  • Select what route will be called according to WordPress <body> classes
  • Execute route events in a specific order, which is :
    • common init
    • page-specific init
    • page-specific finalize
    • common finalize

This system also has the benefit of allowing us to split our JS code between different files, each one corresponding to our different pages.

For more info about this I recommend you just open main.js and router.js since it is pretty much straightforward and the code is well explained.

How to create a route

You might need this at some point so I will just explain it here :

  • create a new file for your route : routes/somePage.js
  • fill the content :
// somepage.js

export default {
  init() {
    // JavaScript to be fired on the page
  },
};
  • add the route to main.js
// main.js
// ...

import somePage from './routes/somePage';
/* ... */
const routes = new Router({
  /* ... */
  somePage,
});

Keep in mind that your <body> will need to have the class somePage (or some-page) if you want the code for this route to be loaded and executed. Also you will probably encounter some issues with <body> classes not updating once Barba is all set up, I explain how to fix this here.

Anyway, back to our problem, how are we going to connect this system to Barba ?

Using routes for Barba

Basically our problem now with Barba is that our JS code doesn't load when we change pages. What we want to have is the following :

When we enter a page without Barba (via direct access or if Barba is disabled), load :

  • common JS code for the whole page (container included)
  • page-specific JS code

When we enter a page with Barba, load :

  • common JS code for the container only
  • page-specific JS code

We are assuming here that page-specific JS only affects content inside the container

The first thing we'll do is split our common.js code in two parts :

  • one for the "global" JS outside the containers (header, menus, footer, ...) : init()
  • one for JS inside the containers that must be loaded for each page : containerInit()

containerInit() will be called in the initial init() function but also on Barba enter trigger. Rest assured, both calls won't ever happen simultaneously.

// common.js

import barba from '@barba/core';

export default {
  containerInit() {
    // common code for all containers
    /* ... */
  },
  init() {
    // common code outside containers (header, menu, footer, etc.)
    /* ... */
    
    // container init
    this.containerInit();

    barba.init({
      transitions: [
        {
          name: 'basic',
          leave: function (data) {
            /* ... */
          },
          enter: function (data) {
            // Load common code for all containers
            this.containerInit();

            /* ... */
          },
        },
      ],
    });  },
  finalize() {
  },
};

This will load the container-specific JS code common to all pages while still keeping the default behaviour if we access the page directly (via url) or if Barba is disabled.

If we want to add page-specific code, we can just import it and load it in the enter function for this page.

// common.js

import barba from '@barba/core';
import somePage from "./somePage";

export default {
  containerInit() {
    /* ... */
  },
  init() {
    this.containerInit();

    barba.init({
      transitions: [
        {
          name: 'basic',
            /* ... */
        } , {
          name: 'to-some-page',
          to: {
            container: ['some-page'],
          },
          leave: function (data) {
            /* ... */
          },
          enter: function (data) {
            // Load this page JS
            somePage.init();

            // Load common code for all containers
            this.containerInit();

            /* ... */
          },
        },
      ],
    });  },
  finalize() {
  },
};

This will work ! But... This code doesn't feel very clean does it ?

We have to include all our pages in common.js, removing the semantic separation between routes. Also it seems like common.js will be full of Barba settings, moving all this code to an external file would certainly be better...

So let's do it !

Organise the code

First we want to have a single file with all our Barba code, we will import all our routes there so that we can call their init() functions when needed.

// barba.js

import barba from '@barba/core';
import common from "./routes/common";
import somePage from "./routes/somePage";
// import ...

export default {
  init() {
    barba.init({
      transitions: [
        /* ... */
        {
          name: 'to-some-page',
          to: {
            namespace: ['some-page'],
          },
          leave: function (data) {
            /* ... */
          },
          enter: function (data) {
            // load JS
            common.containerInit();
            somePage.init();

            // barba behavior
            /* ... */
          },
        },
        /* ... */
      ],
    });
  },
};

Then we have to add our Barba code somewhere in the execution pipeline so it's taken into account. I chose to do it in main.js after the other events since it won't do anything before we actually leave the page.

// main.js

// import external dependencies
import 'jquery';
    
// Import everything from autoload
import './autoload/**/*'
    
// import local dependencies
import Router from './util/Router';
import common from './routes/common';
import home from './routes/home';
import aboutUs from './routes/about';
import somePage from './routes/somePage';
    
/** Populate Router instance with DOM routes */
const routes = new Router({
  // All pages
  common,
  // Home page
  home,
  // About Us page, note the change from about-us to aboutUs.
  aboutUs,
  // The new page we created
  somePage,
});
    
// Init barba && Load Events
jQuery(document).ready(() => {
  routes.loadEvents();
  myBarba.init();
});

And there we have it ! We can now use Barba in a clean fashion, knowing that every bit of code needed will be executed according to the user actions.

Final notes

Even if your code is now functional, there are still some problems that might upset you.

Issue n°1 : admin bar

One issue you'll encounter using Barba with WordPress is that the asynchronous loading also happens when clicking on links from the admin bar. Firstly the admin pages will fail to load and secondly, trying to edit posts this way will end up in creating draft copies of your post in WordPress back-office...

According to Barba creator the best thing to do to solve this issue is simply to disable Barba when logged in.

To do so, we'll have to add these 3 bits of code into our project.

First let's enable us to use AJAX by giving JS acces to the AJAX URL (more info about using AJAX in WordPress here).

// app/setup.php
// ...

add_action('wp_enqueue_scripts', function () {
    wp_enqueue_script('sage/main.js', asset_path('scripts/main.js'), ['jquery'], null, true);
    $ajax_params = array(
        'ajax_url' => admin_url('admin-ajax.php'),
        'ajax_nonce' => wp_create_nonce('my_nonce'),
    );

    wp_localize_script('sage/main.js', 'ajax_object', $ajax_params);
}, 100);

Then add a function in functions.php, which will be called via AJAX, to check if user is logged in.

// functions.php
// ...

function ajax_check_user_logged_in() {
    echo is_user_logged_in();
    die();
}
add_action('wp_ajax_is_user_logged_in', 'ajax_check_user_logged_in');
add_action('wp_ajax_nopriv_is_user_logged_in', 'ajax_check_user_logged_in');

Finally access this function in JS and disable Barba if needed.

// barba.js
// ...

$.post(ajax_object.ajax_url, {action: 'is_user_logged_in'}, function (isLogged) {
  if (!isLogged) barba.init(/* ... */);
});

Your users will now be able to use the admin bar properly.

Issue n°2 : <body> classes

Since Barba only loads content in the container, <body> classes won't change when entering a specific page. This can cause some issues if we're actually using these classes in CSS (and jQuery) selectors.

People on roots discourse actually found a smart workaround for this issue. The idea is to simply have an HTML element inside your container where the classes will be loaded.

<!-- app.blade.php -->
<!-- ... -->

<div id="body-classes" @php(body_class())>

You can then append the classes to your <body> element at the end of each page load.

// barba.js
// ...

barba.init({
  transitions: [
    {
      name: 'to-some-page',
      to: {
        namespace: ['some-page'],
      },
      leave: function (data) {
        /* ... */
      },
      enter: function (data) {
        gsap.from(data.current.container, 1, {
          opacity: 0, 
          onComplete: () => {
            this.async();

            $('body').attr('class', $('#body-classes').attr('class'));
          }
        });
      },
    },
  ],
});

Wrapping it up

I think this pretty much does it, I didn't encounter other real issues while using the lib.

Now you can just let your imagination run wild and create all kind of transitions with pure CSS or animation libraries !

alt text

Barba has much more functionalities such as caching, prefetching , and routing. You will find all of these in the official documentation.

If some parts aren't clear enough or if you find some more optimizations, don't hesitate to tell me and I will include your remarks in the guide.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment