Hugo
Hugo

Reputation: 163

JavaScript ES6 import/export and class extends

I builded a custom element which is a hamburger button and now I'm working on a side nav. In this side nav I want to use my hamburger button so I try to export my HCHamburger class which correspond to my button and import it in my SideNav class. The idea is to animate my button position when the side nav is opened. I try to extends my SideNav class with HCHamburger but I got the following error : Uncaught TypeError: Failed to construct 'HTMLElement': Please use the 'new' operator, this DOM object constructor cannot be called as a function.

My HCHambuger class looks like this :

'use strict';

export default class HCHamburger extends HTMLElement {

    get menuButton() {
        if (!this._menuButton) {
            this._menuButton = this.querySelector('.hamburger-menu');
        }

        return this._menuButton;
    }

    get bar() {
        if (!this._bar) {
            this._bar = this.querySelector('.bar');
        }

        return this._bar;
    }

    attachedCallback() {
        this.menuButton.addEventListener('click', _ => {
            const sideNavContainerEl = document.querySelector('.js-side-nav-container');
            this.bar.classList.toggle("animate");
            if (sideNavContainerEl.getAttribute('nav-opened') == 'false') {
                this.openMenuButton(sideNavContainerEl);
            } else {
                this.closeMenuButton(sideNavContainerEl);
            }
        });
    }

    sayHello() {
        console.log('TOTO');
    }

    openMenuButton(sideNavContainerEl) {
        this.style.transform = `translateX(${sideNavContainerEl.offsetWidth}px)`;
    }

    closeMenuButton(sideNavContainerEl) {
        this.style.transform = `translateX(0px)`;
    }
}

document.registerElement('hc-hamburger', HCHamburger);

And my SideNav class like this :

'use strict';

import Detabinator from './detabinator.js';
import HCHamburger from './hamburger.js';

class SideNav extends HCHamburger {
  constructor () {
    super();
    this.toggleMenuEl = document.querySelector('.js-menu');
    this.showButtonEl = document.querySelector('.js-menu-show');
    this.hideButtonEl = document.querySelector('.js-menu-hide');
    this.sideNavEl = document.querySelector('.js-side-nav');
    this.sideNavContainerEl = document.querySelector('.js-side-nav-container');
    // Control whether the container's children can be focused
    // Set initial state to inert since the drawer is offscreen
    this.detabinator = new Detabinator(this.sideNavContainerEl);
    this.detabinator.inert = true;

    this.toggleSideNav = this.toggleSideNav.bind(this);
    this.showSideNav = this.showSideNav.bind(this);
    this.hideSideNav = this.hideSideNav.bind(this);
    this.blockClicks = this.blockClicks.bind(this);
    this.onTouchStart = this.onTouchStart.bind(this);
    this.onTouchMove = this.onTouchMove.bind(this);
    this.onTouchEnd = this.onTouchEnd.bind(this);
    this.onTransitionEnd = this.onTransitionEnd.bind(this);
    this.update = this.update.bind(this);

    this.startX = 0;
    this.currentX = 0;
    this.touchingSideNav = false;

    this.supportsPassive = undefined;
    this.addEventListeners();
  }

  // apply passive event listening if it's supported
  applyPassive () {
    if (this.supportsPassive !== undefined) {
      return this.supportsPassive ? {passive: true} : false;
    }
    // feature detect
    let isSupported = false;
    try {
      document.addEventListener('test', null, {get passive () {
        isSupported = true;
      }});
    } catch (e) { }
    this.supportsPassive = isSupported;
    return this.applyPassive();
  }

  addEventListeners () {
    this.toggleMenuEl.addEventListener('click', this.toggleSideNav);
    this.sideNavEl.addEventListener('click', this.hideSideNav);
    this.sideNavContainerEl.addEventListener('click', this.blockClicks);

    this.sideNavEl.addEventListener('touchstart', this.onTouchStart, this.applyPassive());
    this.sideNavEl.addEventListener('touchmove', this.onTouchMove, this.applyPassive());
    this.sideNavEl.addEventListener('touchend', this.onTouchEnd);
  }

  onTouchStart (evt) {
    if (!this.sideNavEl.classList.contains('side-nav--visible'))
      return;

    this.startX = evt.touches[0].pageX;
    this.currentX = this.startX;

    this.touchingSideNav = true;
    requestAnimationFrame(this.update);
  }

  onTouchMove (evt) {
    if (!this.touchingSideNav)
      return;

    this.currentX = evt.touches[0].pageX;
    const translateX = Math.min(0, this.currentX - this.startX);

    if (translateX < 0) {
      evt.preventDefault();
    }
  }

  onTouchEnd (evt) {
    if (!this.touchingSideNav)
      return;

    this.touchingSideNav = false;

    const translateX = Math.min(0, this.currentX - this.startX);
    this.sideNavContainerEl.style.transform = '';

    if (translateX < 0) {
      this.hideSideNav();
    }
  }

  update () {
    if (!this.touchingSideNav)
      return;

    requestAnimationFrame(this.update);

    const translateX = Math.min(0, this.currentX - this.startX);
    this.sideNavContainerEl.style.transform = `translateX(${translateX}px)`;
  }

  blockClicks (evt) {
    evt.stopPropagation();
  }

  onTransitionEnd (evt) {
    this.sideNavEl.classList.remove('side-nav--animatable');
    this.sideNavEl.removeEventListener('transitionend', this.onTransitionEnd);
  }

  showSideNav () {
    this.sideNavEl.classList.add('side-nav--animatable');
    this.sideNavEl.classList.add('side-nav--visible');
    this.detabinator.inert = false;
    this.sideNavEl.addEventListener('transitionend', this.onTransitionEnd);
  }

  hideSideNav () {
    this.sideNavEl.classList.add('side-nav--animatable');
    this.sideNavEl.classList.remove('side-nav--visible');
    this.detabinator.inert = true;
    this.sideNavEl.addEventListener('transitionend', this.onTransitionEnd);
  }

  toggleSideNav () {
    if (this.sideNavContainerEl.getAttribute('nav-opened') == 'true') {
      this.hideSideNav();
      this.sideNavContainerEl.setAttribute('nav-opened', 'false');
    } else {
      this.showSideNav();
      this.sideNavContainerEl.setAttribute('nav-opened', 'true');
    }
  }
}

new SideNav();

I'm using webpack to build my JS code and maybe it's the reason of my issue... I tried different method to import/export but nothing worked.

I thought to just export the method that I needed but it didn't work neither.

Thank's

Upvotes: 5

Views: 6546

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074138

Fundamentally, there's just a mis-match between the DOM's API and JavaScript's inheritance (at present). You can't do the extends HTMLElement thing on current browsers. You may be able to at some point when the custom elements specification settles down and is widely-implemented in its final form, but not right now.

If you transpile, you'll get the error you have in your question, because the transpiled code tries to do something along these lines:

function MyElement() {
  HTMLElement.call(this);
}
var e = new MyElement();

If you don't transpile (requiring ES2015+ support on the browser), you'll likely get a different error:

TypeError: Illegal constructor

class MyElement extends HTMLElement {
}
let e = new MyElement();

You have a couple of options that don't involve inheriting from HTMLElement: Wrapping and prototype augmentation

Wrapping

You have a function that wraps elements. It might create wrappers for individual elements, or sets of elements like jQuery; here's a very simple set example:

// Constructor function creating the wrapper; this one is set-based
// like jQuery, but unlike jQuery requires that you call it via `new`
// (just to keep the example simple).
function Nifty(selectorOrElementOrArray) {
  if (!selectorOrElementOrArray) {
    this.elements = [];
  } else {
    if (typeof selectorOrElementOrArray === "string") {
      this.elements = Array.prototype.slice.call(
        document.querySelectorAll(selectorOrElementOrArray)
      );
    } else if (Array.isArray(selectorOrElementOrArray)) {
      this.elements = selectorOrElementOrArray.slice();
    } else {
      this.elements = [selectorOrElementOrArray];
    }
  }
}
Nifty.prototype.addClass = function addClass(cls) {
  this.elements.forEach(function(element) {
    element.classList.add(cls);
  });
};

// Usage
new Nifty(".foo").addClass("test");
new Nifty(".bar").addClass("test2");
.test {
  color: green;
}
.test2 {
  background-color: yellow;
}
<div id="test">
  <span class="bar">bar1</span>
  <span class="foo">foo1</span>
  <span class="bar">bar2</span>
  <span class="foo">foo2</span>
  <span class="bar">bar3</span>
</div>

Prototype Augmentation

You can augment HTMLElement.prototype. There are vocal contingents both for and against doing so, the "against" primarily point to the possibility of conflicts if multiple scripts try to add the same properties to it (or if the W3C or WHAT-WG add new properties/methods to it), which is obviously a very real possibility. But if you keep your property names fairly unlikely to be used by others, you can minimize that possibility:

// Add xyzSelect and xyzAddClass to the HTMLElement prototype
Object.defineProperties(HTMLElement.prototype, {
  "xyzSelect": {
    value: function xyzSelect(selector) {
      return Array.prototype.slice.call(this.querySelectorAll(selector));
    }
  },
  "xyzAddClass": {
    value: function xyzAddClass(cls) {
      return this.classList.add(cls);
    }
  }
});

// Usage
var test = document.getElementById("test");
test.xyzSelect(".foo").forEach(function(e) { e.xyzAddClass("test"); });
test.xyzSelect(".bar").forEach(function(e) { e.xyzAddClass("test2"); });
.test {
  color: green;
}
.test2 {
  background-color: yellow;
}
<div id="test">
  <span class="bar">bar1</span>
  <span class="foo">foo1</span>
  <span class="bar">bar2</span>
  <span class="foo">foo2</span>
  <span class="bar">bar3</span>
</div>

Prototype augmentation works in modern browsers, and also IE8. It didn't work in IE7 and earlier.

Upvotes: 4

Related Questions