Building Advertising Websites : The Router

The projects I work on have very peculiar transitions and navigation flow, that’s why a good router is extremely important, the majority of the router libraries out there were not built with transitions in mind, they just map the URL with a function, and if we are talking about frameworks, they don’t offer any flexibility for transitions and rendering.

I need a router that was designed with transitions in mind, that has sub-router support and doesn’t handle rendering for me, that’s when Ways comes into play.

You can use Ways as a traditional router library that just maps an URL with a function, but that’s not how I use it. What I am going to cover here is a feature called Flow Mode.

Ways has 2 types of flow:

ways.flow('destroy+run'); // destroy first, run after
ways.flow('run+destroy'); // run first, destroy after

What that means is that if you go to another route, the current route is going to be “destroyed” before or after the next one shows up.

After setting the Flow Mode, we can start defining our routes:

ways("/", intro, outro)
function intro(req, done) {}
function outro(req, done) {}

ways has 3 required parameters: route url pattern, runner method and the destroyer method. URL pattern is pretty straightforward, it works the same way as other router libraries (use :param to define parameters). Runner method is going to be executed when the router is activated (and destroyer before the route is changed, they are analogous to intro and outro methods, or show and hide.). Both take two parameters: req and done. req contains data about the route (URL pattern, parameters and more) and done is a callback function that needs to be called to tell Ways that you are done with your current action.

Sub-Router Support

Aside from the 3 required parameters, Ways has an optional 4th parameter which defines a dependency router. What does that mean?

It means that you can create a router that requires another one to be completed first, lets look at an example:

ways("/", intro, outro)
ways("/home", intro, outro, "/")

If you tell ways to go to /home , it will first execute the / router Runner method (in this case, intro), and after the done is called, it will execute the /home Runner method, e.g.:

ways.go("/home")
// go to "/", runs "intro", wait for "done" to be called
// go to "/home", runs "intro", wait for "done" to be called.

And it doesn’t stop there, you can have as many nested routes as you want:

ways("/", intro, outro)
ways("/home", intro, outro, "/")
ways("/home/gallery", intro, outro, "/home")
ways("/home/gallery/:id", intro, outro, "/home/gallery")
ways.go("/home/gallery/1")
// go to "/", runs "intro", wait for "done" to be called
// go to "/home", runs "intro", wait for "done" to be called
// go to "/home/gallery", runs "intro", wait for "done" to be called
// go to "/home/gallery/:id", runs "intro", wait for "done" to be called

Now, what happens if I I am at /home/gallery/1 and I go to a router that isn’t in the same dependency chain? e.g:

ways("/", intro, outro)
ways("/home", intro, outro, "/")
ways("/home/gallery", intro, outro, "/home")
ways("/about", intro, outro, "/")
ways.go("/home/gallery/1") //Start at "/home/gallery/:id"
ways.go("/about") // Go to "/about"

It will start running the destroyer method (in this case, outro) until it reaches the same level of dependency as /about :

1. // Execute the destroyer method of "/home/gallery/:id" and wait for "done" to be called.
2. // Execute the destroyer method of "/home/gallery" and wait for "done" to be called.
3. // Execute the destroyer method of "/home" and wait for "done" to be called.
4. // Execute the RUNNER method of "/about" and wait for "done" to be called.

As both /home and /about has the same dependency, Ways doesn’t execute the Runner nor the Destroyer method of the / route.

This is extremely useful when building interfaces, as you can create a dependency graph for your routes.

Using Views

As for now, we are only using single functions as Runner and Destroyer, but in more realistic cases (at least in my projects), I usually map concrete view’s methods with the route, e.g.:

import layout from "layout"
import home from "home"
import gallery from "gallery"
import about from "about"
ways("/", layout.intro.bind(layout), layout.outro.bind(layout))
ways("/home", home.intro.bind(home), home.outro.bind(home), "/")
ways("/home/gallery", gallery.intro.bind(gallery), gallery.outro.bind(gallery), "/home")
ways("/about", about.intro.bind(about), about.outro.bind(about), "/")
To maintain the view’s scope when calling its methods, I need to pass the method using bind

This way each view can implement their own Runner (intro) and Destroyer (outro) method.

On a side note, as there won’t be more than 1 instance of each PageView, I create them as single instances so they can be accessed from anywhere in the application. (I know what you’re thinking, Singletons are bad, but I think is safe to use them in this case)

Creating a single instance view — note the new when exporting the class
class View {
    constructor() {
}
    intro(req, done) {
}
    outro(req, done) {
}
}
export default new View;

But you don’t have to do it this way, you can perfectly import the classes inside the router and instantiate them there, that way you can have access to all the views just by requiring the router module.

Rendering

Ways doesn’t get involved in rendering at all. Outputting things to the screen is up to the developer, which I think is a good thing. I need to have the flexibility to render the elements whenever and wherever I want, let’s look at a basic view’s implementation of intro and outro :

class Layout {
    intro(req, done) {
        this.el = document.createElement("<div></div>");                                                       
        document.body.appendChild(this.el); 

done();

}
}

I have total control of the rendering, I can choose whether I want to create an element, or use an existing in the DOM, or don’t create anything at all, the only requirement is to call done() to tell Ways that the intro is completed.

Taking the code above as an example, we can render Home as an overlay over the screen, it doesn’t need to be rendered inside the Layout DOM element. That’s the beauty of Ways, it doesn’t enforce View implementation, it only execute router methods at the right order and time.

Transitions

Transitions are pretty straightforward, I usually implement them inside intro , like this:

class Layout {
    intro(req, done) {
        this.el = document.createElement("<div></div>");
        document.body.appendChild(this.el); 

TweenMax.to(this.el, {opacity:0});
        TweenMax.to(this.el, {opacity:1, onComplete:done});
    }
}

If we run ways.go("/home") , it will execute Layout’s intro method, and wait until the animation is complete to move forward to Home’s intro method.

Note the TweenMax onComplete parameter

The same goes for the outro method, with the exception that I usually remove the DOM element and I clear all the variables to free space in the memory:

class Home {
    outro(req, done) {
        TweenMax.to(this.el, {opacity:0, onComplete:()=>{

this.el.parentNode.removeChild(this.el);

this.el = undefined;
            done();        
});
    }
}

Preloading

If my views needs to preload assets before rendering, I normally run preload before rendering the page:

class Home {
    intro(req, done){
        this.preload(()=> {
this.render()
            done();
})
    }
}

And If I need to show a loader while the assets are loading, one of the ways to achieve this is to create a Preloader component inside the Layout so I can access it through Home:

import layout from "scripts/views/layout"
class Home {
    intro(req, done){
        this.preload(()=> {
            layout.hidePreloader();
this.render()
            done();
})
    }
    preload(done) {

layout.showPreloader();
        // Load assets and call done when its finished
done();
    }
}

There are many ways to achieve this, the important thing is that is up to you to organise your code.

Push State

By default, Ways doesn’t change the URL of the browser, that’s because it doesn’t have the Browser as a dependency, you can use Ways in your server if you want. To activate Push State, all you have to do is run ways.use(ways.addressbar).

Conclusion

Ways is the backbone of my projects, I don’t use it only as a router, but as a way of organising the navigation flow by creating a dependency graph between my views.

In the next section I’ll cover models and how I work with data on my projects.

Wan’t to start from the beginning? Head over to the index.