Configuring and personalizing

This documentation is a work in progress. It describes prerelease software, and is subject to change.

This page will take you through the steps you need to do to modify the app and add your own content.

Table of Contents

Folder structure

Your app will be initialized with a bunch of folders and files, that looks like this:

my-app
├── images
│   ├── favicon.ico
│   └── manifest
│       ├── icon-48x48.png
│       └── ...
├── src
│   ├── store.js
│   ├── actions
│   │   └── ...
│   ├── reducers
│   │   └── ...
│   ├── components
│   │   └── ...
├── test
│   ├── unit
│   │   └── ...
│   └── integration
│       └── ...
├── index.html
├── README.md
├── package.json
├── polymer.json
├── manifest.json
├── service-worker.js
├── sw-precache-config.js
├── wct.conf.json
├── .travis.yml

You can add more app-specific folders if you want, to keep your code organized – for example, you might want to create a src/views/ folder and move all the top-level views in there.

Naming and code conventions

This section covers some background about the naming and coding conventions you will find in this template:

import { LitElement, html } from '@polymer/lit-element';
class SampleElement extends LitElement {
  // The properties that your element exposes.
  static get properties() { return {
    publicProperty: { type: Number },
    _privateProperty: { type: String }    /* note the leading underscore */
  }};

  constructor() {
    super();
    // Set up the property defaults here
    this.publicProperty = 0;
    this._privateProperty = '';
  }

  render() {
    // Note the use of the object spread to explicitely
    // call out which properties you're using for rendering.
    const {publicProperty, _privateProperty} = this;

    // Anything code that is related to rendering should be done in here.

    return html`
      <!-- your element's template goes here -->
    `;
  });

  firstUpdated() {
    // Any code that relies on render having been called once goes here.
    // (for example setting up listeners, etc)
  }
  ...
}
window.customElements.define('sample-element', SampleElement);

Customizing the app

Here are some changes you might want to make to your app to personalize it.

Changing the name of your app

By default, your app is called my-app. If you want to change this (which you obviously will), you’ll want to make changes in a bunch of places:

Adding a new page

There are 4 places where the active page is used at any time:

To add a new page, you need to add a new entry in each of these places. Note that if you only want to add an external link or button in the toolbar, then you can skip adding anything to the <main> element.

Create a new page

First, let’s create a new element, that will represent the new view for the page. The easiest way to do this is to copy the <my-view404> element, since that’s a good and basic starting point:

import { html } from '@polymer/lit-element/lit-element.js';
import { PageViewElement } from './page-view-element.js';
import { SharedStyles } from './shared-styles.js';

class MyView4 extends PageViewElement {
  render() {
    return html`
      ${SharedStyles}
      <section>
        <h2>Oops! You hit a 404</h2>
        <p>The page you're looking for doesn't seem to exist. Head back
           <a href="/">home</a> and try again?
        </p>
      </section>
    `
  }
}
window.customElements.define('my-view4', MyView4);

(🔎This page extends PageViewElement rather than LitElement as an optimization; for more details on that, check out the conditional rendering section).

Adding the page to the application

Great! Now we that we have our new element, we need to add it to the application!

First, add it to each of the list of nav links. In the toolbar (the wide-screen view) add:

<nav class="toolbar-list">
  ...
  <a ?selected="${_page === 'view4'}" href="/view4">New View!</a>
</nav>

Similarly, we can add it to the list of nav links in the drawer:

<nav class="drawer-list">
  ...
  <a ?selected="${_page === 'view4'}" href="$/view4">New View!</a>
</nav>

And in the main content itself:

<main class="main-content">
  ...
  <my-view4 class="page" ?active="${_page === 'view4'}"></my-view4>
</main>

Note that in all of these code snippets, the selected attribute is used to highlight the active page, and the active attribute is also used to ensure that only the active page is actually rendered.

Finally, we need to lazy load this page. Without this, the links will appear, but they won’t be able to navigate to your new page, since my-view4 will be undefined (we haven’t imported its source code anywhere). In the loadPage action creator, add a new case statement:

switch(page) {
  ...
  case 'view4':
    import('../components/my-view4.js');
    break;
}

Don’t worry if you don’t know what an action creator is yet. You can find a complete explanation of how it fits into the state management story in the Redux page.

That’s it! Now, if you refresh the page, you should be able to see the new link and page. Don’t forget to re-build your application before you deploy to production (or test that build), since this new page needs to be added to the output.

Adding the page to the push manifest

To take advantage of HTTP/2 server push, you need to specify what scripts are needed for the new page. Add a new entry to push-manifest.json:

{
  "/view4": {
    "src/components/my-app.js": {
      "type": "script",
      "weight": 1
    },
    "src/components/my-view4.js": {
      "type": "script",
      "weight": 1
    },
    "node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js": {
      "type": "script",
      "weight": 1
    }
  },

  /* other entries */
}

Using icons

You can inline an <svg> directly where you need it in the page, but if there’s any reusable icons you’d like to define once and use in several places, my-icons.js is a good spot for that. To add a new icon, you can just add a new line to that file:

export const closeIcon = html`<svg>...</svg>`

Then, you can import it and use it as a template literal in an element’s render() method:

import { closeIcon } from './my-icons.js';
render() {
  return html`
    <button title="close">${closeIcon}</button>
  `;
}

Sharing styles

Similarly, shared styles are also just exported template literals. If you take a look at shared-styles.js, it exports a <style> node template, that is then inlined in an element’s render() method:

import { SharedStyles } from './shared-styles.js';
render() {
  return html`
    ${SharedStyles}
    <div>...</div>
  `;
}

Fonts

The app doesn’t use any web fonts for the content copy, but does use a Google font for the app title. Be careful not too load too many fonts, however: aside from increasing the download size of your first page, web fonts also slow down the performance of an app, and cause flashes of unstyled content.

But I don’t want to use Redux

The pwa-starter-kit is supposed to be the well-lit path to building a fairly complex PWA, but it should in no way feel restrictive. If you know what you’re doing, and don’t want to use Redux to manage your application’s state, that’s totally fine! We’ve created a separate template, template-no-redux, that has the same UI and PWA elements as the main template, but does not have Redux.

Instead, it uses a unidirectional data flow approach: some elements are in charge of maintaining the state for their section of the application, and they pass that data down to children elements. In response, when the children elements need to update the state, they fire an event.

Advanced topics

Responsive layout

By default, the pwa-starter-kit comes with a responsive layout. At 460px, the application switches from a wide, desktop view to a small, mobile one. You can change this value if you want the mobile layout to apply at a different size.

For a different kind of responsive layout, the template-responsive-drawer-layout template displays a persistent app-drawer, inline with the content on wide screens (and uses the same small-screen drawer as the main template).

Changing the wide screen styles

The wide screen styles are controlled in CSS by a media-query. In that block you can add any selectors that would only apply when the window viewport’s width is at least 460px; you can change this pixel value if you want to change the size at which these styles get applied (or, can add a separate style if you want to have several breakpoints).

Changing narrow screen styles

The rest of the styles in my-app are outside of the media-query, and thus are either general styles (if they’re not overwritten by the media-query styles), or narrow-screen specific, like this one (in this example, the <nav class="toolbar-list"> is hidden in the narrow screen view, and visible in the wide screen view).

Responsive styles in JavaScript

If you want to run specific JavaScript code when the size changes from a wide to narrow screen (for example, to make the drawer persistent, etc), you can use the installMediaQueryWatcher helper from pwa-helpers. When you set it up, you can specify the callback that is ran whenever the media query matches.

Conditionally rendering views

Which view is visible at a given time is controlled through an active attribute, that is set if the name of the page matches the location, and is then used for styling:

<style>
  .main-content .page {
    display: none;
  }
  .main-content .page[active] {
    display: block;
  }
</style>
<main class="main-content">
  <my-view1 class="page" ?active="${page === 'view1'}"></my-view1>
  <my-view2 class="page" ?active="${page === 'view2'}"></my-view2>
  ...
</main>

However, just because a particular view isn’t visible doesn’t mean it’s “inactive” – its JavaScript can still run. In particular, if your application is using Redux, and the view is connected (like my-view2 for example), then it will get notified any time the Redux store changes, which could trigger render() to be called. Most of the time this is probably not what you want – a hidden view shouldn’t be updating itself until it’s actually visible on screen. Apart from being inefficient (you’re doing work that nobody is looking at), you could run into really weird side effects: if a view’s render() function also updates the title of the application, for example, the title may end up being set incorrectly by one of these inactive views, just because it was the last view to set it.

To get around that, the views inherit from a PageViewElement base class, rather than LitElement directly. This base class checks whether the active attribute is set on the host (the same attribute we use for styling), and calls render() only if it is set.

If this isn’t the behaviour you want, and you want hidden pages to update behind the scenes, then all you have to do is change the view’s base class back to LitElement (i.e. changing this line). Just look out for those side effects!

Routing

The app uses a very basic router, that listens to changes to the window.location. You install the router by passing it a callback, which is a function that will be called any time the location changes:

installRouter((location) => this._locationChanged(location));

Then, whenever a link is clicked (or the user navigates back to a page), this._locationChanged is called with the new location. You can check the Redux page to see how this location is stored in the Redux store.

Sometimes you might need to update this location (and the Redux store) imperatively – for example if you have custom code for link hijacking, or you’re managing page navigations in a custom way. In that case, you can manually update the browser’s history state yourself, and then call the this._locationChanged method manually (thus simulating an action from the router):

// This function would get called whenever you want
// to manually manage the location.

onArticleLinkClick(page) {
  const newLocation = `/article/${page}`
  window.history.pushState({}, '', newLocation);
  this._locationChanged(newLocation);
};

SEO

We’ve added a starting point for adding rich social graph content to each pages, both using the Open Graph protocol (used on Facebook, Slack etc) and Twitter cards.

This is done in two places:

A different approach is to update this metadata differently, depending on what page you are. For example, the Books doesn’t update the metadata in the main top-level element, but on specific sub-pages. It uses the image thumbnail of a book only on the detail pages, and adds the search query on the explore page.

If you want to test how your site is viewed by Googlebot, Sam Li has a great article on gotchas to look out for – in particular, the testing section covers a couple tools you can use, such as Fetch as Google and Mobile-Friendly Test.

Fetching data

If you want to fetch data from an API or a different server, we recommend dispatching an action creator from a component, and making that fetch asynchronously in a Redux action. For example, the Flash Cards sample app dispatches a loadAll action creator when the main element boots up; it is that action creator that then does the actual fetch of the file and sends it back to the main component by adding the data to the state in a reducer.

A similar approach is taken in the Hacker News app where an element dispatches an action creator, and it’s that action creator that actually fetches the data from the HN API.

Responding to network state changes

You might want to change your UI as a response to the network state changing (i.e. going from offline to online).

Using the installOfflineWatcher helper from pwa-helpers, we’ve added a callback that will be called any time we go online or offline. In particular, we’ve added a snackbar that gets shown; you can configure its contents and style in snack-bar.js. Note that the snackbar is shown as a result of a Redux action creator being dispatched, and its duration can be configured there.

Rather than just using it as an FYI, you can use the offline status to display conditional UI in your application. For example, the Books sample app displays an offline view rather than the details view when the application is offline.

State management

There are many different ways in which you can manage your application’s state, and choosing the right one depends a lot on the size of your team and application. For simple applications, a uni-directional data flow pattern might be enough (the top level, <my-app> element could be in charge of being the source of state truth, and it could pass it down to each of the elements, as needed); if that’s what you’re looking for, check out the template-no-redux branch.

Another popular approach is Redux, which keeps the state in a store outside of the app, and passes immutable copies to each element. To see how that is set up, check out the Redux and state management section for an explainer, and more details.

Theming

This section is useful both if you want to change the default colours of the app, or if you want to let your users be able to switch between different themes.

Changing the default colours

For ease of theming, we’ve defined all of the colours the application uses as CSS custom properties, in the <my-app> element. Custom properties are variables that can be reused throughout your CSS. For example, to change the application header to be white text on a lavender background, then you need to update the following properties:

--app-header-background-color: lavender;
--app-header-text-color: black;

And similarly for the other UI elements in the application.

Switching themes

Re-theming the entire app basically involves updating the custom properties that are used throughout the app. If you just want to personalize the default template with your own theme, all you have to do is change the values of the app’s custom properties.

If you want to be able to switch between two different themes in the app (for example between a “light” and “dark” theme), you could just add a class (for example, dark-theme) to the my-app element for the new theme, and style that separately. This would end up looking similar to this:

:host {
  /* This is the default, light theme */
  --app-primary-color: red;
  --app-secondary-color: black;
  --app-text-color: var(--app-secondary-color);
  --app-header-background-color: white;
  --app-header-text-color: var(--app-text-color);
  ...
}

:host.dark-theme {
  /* This is the dark theme */
  --app-primary-color: yellow;
  --app-secondary-color: white;
  --app-text-color: var(--app-secondary-color);
  --app-header-background-color: black;
  --app-header-text-color: var(--app-text-color);
  ...
}

You control when this class is added; this could be when a “use dark theme” button is clicked, or based on a hash parameter in the location, or the time of day, etc.

Next steps

Now that you’re done configuring your application, check out the next steps: