src / mixin / index.js

Copyright (c) 2018 Florian Klampfer

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see


This component is written in RxJS and reading its code requires some basic understanding of how RxJS works. It may also serve as an example of how to use RxJS.

Other than RxJS, you should be familiar with ES6 Mixin, which is a clever way of using the ES6 class syntax to achieve inheritance-based mixins. The mixin in the main export of this file.


Including the patches for ES6+ functions, but there is a -lite version of the component that comes without these. import ‘core-js/fn/array/for-each’; import ‘core-js/fn/array/from’; import ‘core-js/fn/function/bind’; import ‘core-js/fn/object/assign’;

Importing the hy-compontent base libary, which helps with making multiple versions of the component (Vanilla JS, WebComponent, etc…).

import { componentMixin, COMPONENT_FEATURE_TESTS, Set } from "hy-component/src/component";
import { rxjsMixin } from "hy-component/src/rxjs";
import { array, bool, number, regex, string } from "hy-component/src/types";

import { Subject } from "rxjs/_esm5";

Partial polyfill of the URL class. Only provides the most basic funtionality of URL, but sufficient for this compoennt.

import { URL } from "../url";
import { isExternal } from "../common";

import { INIT, HINT, PUSH, POP } from "./constants";
import { setupObservablesMixin } from "./setup";

export { INIT, HINT, PUSH, POP };


A set of Modernizr tests that are required to run this component. These are the bare-minimum requirements, more ad-hoc features tests for optional behavior is part of the code below.

export const MIXIN_FEATURE_TESTS = new Set([

export { Set };

Patching the document fragment’s getElementById function, which is not implemented in all browsers, even some modern ones.

DocumentFragment.prototype.getElementById =
  DocumentFragment.prototype.getElementById ||
  function getElementById(id) {
    return this.querySelector(`#${id}`);

Push state mixin

export const pushStateMixin = C =>
  class extends setupObservablesMixin(rxjsMixin(componentMixin(C))) {

The name of the component (required by hy-component)

    static get componentName() {
      return "hy-push-state";


The default values (and types) of the configuration options (required by hy-component) See Options for usage information.

    static get types() {
      return {
        replaceIds: array,
        linkSelector: string,
        duration: number,
        hrefRegex: regex,
        scriptSelector: string,
        initialHref: string,
        prefetch: bool,

    static get defaults() {
      return {
        replaceIds: [],
        linkSelector: "a[href]:not(.no-push-state)",
        duration: 0,
        hrefRegex: null,
        scriptSelector: null,
        initialHref: window.location.href,
        prefetch: false,


We expose the same properties as window.location (in many ways this component can be thought of as a “replacement” for the global Location object). Currently they are read-only.

    get hash() {
      return this._url.hash;

    get host() {

    get hostname() {
      return this._url.hostname;

    get href() {
      return this._url.href;

    get origin() {
      return this._url.origin;

    get pathname() {
      return this._url.pathname;

    get port() {
      return this._url.port;

    get protocol() {
      return this._url.protocol;

    get search() {


    setupComponent(el, props) {
      super.setupComponent(el, props);

      this.saveScrollPosition = this.saveScrollPosition.bind(this);

      this.reload$ = new Subject();

This component has no shadow DOM, so we just return the element.

    setupShadowDOM(el) {
      return el;

Overriding the setup function.

    connectComponent() {
      if (process.env.DEBUG && !this.replaceIds && !
        console.warn("hy-push-state needs a 'replace-ids' or 'id' attribute.");

Setting up scroll restoration

      if ("scrollRestoration" in window.history) window.history.scrollRestoration = "manual";

Restore the last scroll position, if any.


Remember the current scroll position (for F5/reloads).

      window.addEventListener("beforeunload", this.saveScrollPosition);

Calling the setup observables function function.


TODO: meh…


Setting the initial history.state.

      const url = new URL(this.initialHref);
      this.updateHistoryState({ type: INIT, replace: true, url });

      const replaceEls = this.getReplaceElements(document);
      if (isExternal(this)) this.rewriteURLs(replaceEls);

After all this is done, we can fire the one-time init event…


…and our custom load event, which gets fired on every page change. We provide similar data as subsequent load events, however we can’t provide an anchor or event, since this load event wasn’t caused by a user interaction.

        type: INIT,
        title: this.getTitle(document),
        cacheNr: this.cacheNr,

    disconnectComponent() {
      window.removeEventListener("beforeunload", this.saveScrollPosition);


Public methods of this component. See Methods for more.

    assign(url) {
        type: PUSH,
        url: new URL(url, this.href),
        cacheNr: ++this.cacheNr, // eslint-disable-line no-plusplus

    reload() {
        type: PUSH,
        url: new URL(this.href),
        cacheNr: ++this.cacheNr, // eslint-disable-line no-plusplus
        replace: true,

    replace(url) {
        type: PUSH,
        url: new URL(url, this.href),
        cacheNr: ++this.cacheNr, // eslint-disable-line no-plusplus
        replace: true,

© 2018 Florian Klampfer

Powered by Hydejack v8.4.0