node package manager

nowtv-global-nav

NowTV Global Nav #2

Very basic rebuild of the global nav using Preact and redux to keep code organised and testable.

Some core ideas:

  • Declarative rendering of pieces.
  • No more handlebars.
  • Functional testing of rendered/mounted dom in node.
  • Highly configurable.
  • No more confusing contradictory state.
  • Managing events cleanly.
  • No more managing instances of the GlobalNav, instead keeping the instances abstracted away and exposing static methods on the singleton class object.

Usage

For the client, initialise it with all optional settings. And then mount it on a node.

GlobalNav.init({
  initialUiState: {
    theme: 'entertainment',
    section: 'messages'
  },
  baseUrls: {
    help: '//custom-help-base-url.com'
  }
});
GlobalNav.mount($('global-nav-container'));

For Rendering to a String use the GlobalNavServer with it's render() method to get a HTML string of content.

// nothing on npm for this yet! just psuedo 
import GlobalNavServer from 'GlobalNav/src/GlobalNavServer';
 
GlobalNavServer.init({ config });
const markup = GlobalNavServer.render();

Settings

You can pass the following settings to the .init() method.

// default settings:
{
  "config": null,
  "initialUiState": {
    "theme": "default",
    "search": "visible",
    "open": null,
    "topNav": {
      "position": "fixed",
    },
    "sideNav": {
      "burger": "visible",
      "content": null
    }
  },
  "baseUrls": {}
}
  • config: A JSON object of the config if you already have it.
  • theme: "entertainment", "sports" or something.
  • search: Defaults to "visible". Change it to "hidden".
  • open: Used mostly internally but you can still use it to start with a section open such as "help" for whatever reason.
  • topNav.position: "fixed" by default or "static".
  • sideNav.burger: "visible" by default. Change to "hidden" or "mobile-only".
  • sideNav.content: A HTML string of content for the sideNav.
  • baseUrls: An object of baseUrls to use instead of those from the config. This list gets merged over the top.

You can pass a config into the init method as well, if you have it.

API

  • GlobalNav.init(settings, ResourceProvider, LinkPropsProvider) - initialise with settings and providers.
  • GlobalNav.mount(node) - mount inside a node.
  • GlobalNav.unmount() - unmount from said node.
  • GlobalNav.updateUiState(state) - update the internal ui state.
  • GlobalNav.setConfig(config) - update the config.
  • GlobalNav.fetchConfig() - returns a promise and updates the config by fetching using the ResourceProvider.

The Config

You can render the nav without a config. To get the config, either before or after rendering, call the GlobalNav.fetchConfig(). This will return a promise of the config request being fetched and also update the nav without you needing to do anything.

For the server render, the GlobalNavServer should have a config before you render, so either fetch it or pass it in as an init(setting).

Customising the config request.

The default config request is made using the isomorphic fetch library.

When you call .init() There is an optional second argument. A config provider. This should be a class that provides an instance method called fetchConfig() that is expected to return a promise. You can implement the request however you like. It should resolve the config so that the nav can update.

Also:

  • fetchWatchlist
  • fetchContinueWatching

E.g. to use a different request library:

class AxiosResourceProvider {
  // maybe encapsulate some logic for a dynamic config url. e.g.   
  getUrl() {
    if (process.env.NODE_ENV == 'production') {
      return "http:\/\/cdn.url/config.json";
    } else {
      return "http:\/\/test/config.json";
    }
  }
 
  // you should make this method and ensure it returns a promise that resolves a config object. 
  // e.g. 
  fetchConfig() {
    return axios.get(this.getUrl()).then(response => response.data);
  }
 
  fetchWatchlist() {
    
  }
  fetchContinueWatching() {
 
  }
}
GlobalNav.init(undefined, AxiosResourceProvider);
GlobalNav.mount($('#nav-container'));
GlobalNav.fetchConfig();

Building Links

Similarly to the ResourceProvider, you can make your own LinkPropsProvider. This might be useful if you want to implement pushState transitions to certain links on the nav!

class MyLinkPropsProvider {
  getProps(link) {
    if (['my-section', 'my-other-section'].includes(link.name)) {
      return {
        onClick(e) {
          e.preventDefault();
          history.push(link.uri);
        }
      }
    }
    // return undefined or null or an empty object for default behaviour 
  }
}
GlobalNav.init(undefined, undefined, MyLinkPropsProvider);

Example Server

There's a funky example server we threw in quickly using create react app. To get it going you'll need to cd ./example && npm install first. Once installed, cd back into the root again cd .. and npm run example.

Then you can play around with some of the options to see how things work and whatnot.

Testing

This navigation library was made using Preact to keep things small and convenient.

But that means enzyme doesn't work for writin tests. However, you can just use cheerio instead, which is simply put, jQuery for jsdom. chai-cheerio has already been setup.

There are currently only 2 types of tests running:

Unit tests

Testing the components and helpers and so on, using cheerio or preact-jsx.

Functional Tests

Repeatedly mounting/rendering the whole GlobalNav with various settings and asserting on sub sections inside node again with jsdom.

Browser Tests

These don't exist yet.

The idea being to use the example server to drive behaviour of the Global nav through the controls on the page and assert that things work in different browsers.

TODO:

  • Make the example server hot reload the GlobalNav implementation and not just itself.
  • Write up all the unit tests