I'm confused about how the states of members of classes in Javascript work.
I'm trying to create a dialog class which has the ability to hide the dialog when a user clicks the cancel button. I've simplified the implementation to show the problem I'm facing.
class FooClass {
bar
createBar() {
this.bar = document.createElement("ons-dialog");
this.bar.innerHTML = "this is bar content";
document.querySelector("#home").appendChild(this.bar);
this.bar.addEventListener(
'click',
this.hideBar);
this.bar.show();
}
hideBar() {
if(this.bar) {
console.log("bar exists: " + this.bar);
this.bar.hide();
} else {
console.log("bar is null!")
}
}
}
foo = new FooClass();
foo.createBar();
When I click the dialog that comes up when the page completes loading, nothing happens and the output in the console is always:
bar is null!
Why does the class lose this.bar after the page completes loading? Furthermore, how can I assign an event listener to some button which closes the dialog within this class?
Here is a codepen.io link with implementation:
https://codepen.io/moonlightcheese/pen/NWpxxMj?editors=1111
In Javascript classes, methods are not automatically bound to the instance. E.g. when using a method from a class as an event handler, like you are doing, this will point to the element you attach the listener to.
To fix that, you have two options:
In the constructor of your class, bind the method to the instance:
constructor() {
this.hideBar = this.hideBar.bind(this);
}
class FooClass {
bar;
constructor() {
this.hideBar = this.hideBar.bind(this);
}
createBar() {
this.bar = document.createElement("ons-dialog");
this.bar.innerHTML = "this is bar content";
document.querySelector("#home").appendChild(this.bar);
this.bar.addEventListener(
'click',
this.hideBar);
this.bar.hidden = false;
}
hideBar() {
if (this.bar) {
console.log("bar exists: " + this.bar.outerHTML);
this.bar.hidden = true;
} else {
console.log("bar is null!")
}
}
}
foo = new FooClass();
foo.createBar();
<div id="home"></div>
Use a class property and assign an arrow function:
hideBar = () => { /* your function code */ }
class FooClass {
bar;
createBar() {
this.bar = document.createElement("ons-dialog");
this.bar.innerHTML = "this is bar content";
document.querySelector("#home").appendChild(this.bar);
this.bar.addEventListener(
'click',
this.hideBar);
this.bar.hidden = false;
}
hideBar = () => {
if (this.bar) {
console.log("bar exists: " + this.bar.outerHTML);
this.bar.hidden = true;
} else {
console.log("bar is null!")
}
}
}
foo = new FooClass();
foo.createBar();
<div id="home"></div>
Please note that due to the missing custom element definition, the custom element ons-dialog does not have a show and hide method (it's simply an HTMLUnknownElement here in the snippet), which is why I replaced that functionality in the snippets using the standard DOM hidden API.
Related
I'm using this example for the accordion and trying to play with it to have just One Section Open at a time
I know how to create this using iQuery, but here I'm puzzled. Should I also do something like forEach or !this or to specify current?
class Accordion {
constructor(domNode) {
this.rootEl = domNode;
this.buttonEl = this.rootEl.querySelector('button[aria-expanded]');
const controlsId = this.buttonEl.getAttribute('aria-controls');
this.contentEl = document.getElementById(controlsId);
this.open = this.buttonEl.getAttribute('aria-expanded') === 'true';
// add event listeners
this.buttonEl.addEventListener('click', this.onButtonClick.bind(this));
}
onButtonClick() {
this.toggle(!this.open);
}
toggle(open) {
// don't do anything if the open state doesn't change
if (open === this.open) {
return;
}
// update the internal state
this.open = open;
// handle DOM updates
this.buttonEl.setAttribute('aria-expanded', `${open}`);
if (open) {
this.contentEl.removeAttribute('hidden');
} else {
this.contentEl.setAttribute('hidden', '');
}
}
// Add public open and close methods for convenience
open() {
this.toggle(true);
}
close() {
this.toggle(false);
}
}
// init accordions
const accordions = document.querySelectorAll('.accordion h3');
accordions.forEach((accordionEl) => {
new Accordion(accordionEl);
});
I have a button. When this button is clicked I do two things
Open up a search menu
Attach an event listener to the document body to listen for close events.
However, I cannot seem to be able to remove the eventlistener from the document on the close function. That is, the second time I try to open the menu, it immediately calls the close function
My question is...
How do I remove the document event listener?
And how do I make it so that if the user clicks the search menu, it does not trigger the document click event
openDesktopSearchMenu() {
this.$desktopSearchMenu.style.height = '330px';
document.addEventListener('click', this.closeDesktopSearchMenu.bind(this), true);
}
closeDesktopSearchMenu() {
this.$desktopSearchMenu.style.height = '0px';
document.removeEventListener('click', this.closeDesktopSearchMenu.bind(this), true);
}
Update July 24
Nick's answer definitely put me in the right direction. However, the document was always being called first due to the capture parameter. So if the user clicks inside the search menu, it's automatically closed.
Removing the capture parameter causes the close function to be invoked immediately after it opens.
The way around this that worked for me is to wrap the listener inside a timeout when I add it. And then naturally I had to call stopPropagation() on search menu click
searchMenuClick = (e) => {
e.stopPropagation();
}
/** open the desktop search menu */
openDesktopSearchMenu = () => {
this.$desktopSearchMenu.style.height = '330px';
this.$navBar.classList.add('search');
setTimeout(() => {
document.addEventListener('click', this.closeDesktopSearchMenu, { capture: false });
});
}
closeDesktopSearchMenu = () => {
this.$desktopSearchMenu.style.height = '0px';
setTimeout(() => {
this.$navBar.classList.remove('search');
}, 300);
document.removeEventListener('click', this.closeDesktopSearchMenu, { capture: false });
}
The .bind() method returns a new function, so the function which you're adding as the callback to addEventListener is a different reference to the one you're trying to remove. As a result, the event listener doesn't get removed.
You could consider binding in your constructor like so:
constructor() {
...
this.closeDesktopSearchMenu = this.closeDesktopSearchMenu.bind(this);
...
}
And then use your method like so (without the bind, as that's now done in the constructor):
openDesktopSearchMenu() {
this.$desktopSearchMenu.style.height = '330px';
document.addEventListener('click', this.closeDesktopSearchMenu, true);
}
closeDesktopSearchMenu() {
this.$desktopSearchMenu.style.height = '0px';
document.removeEventListener('click', this.closeDesktopSearchMen, true);
}
See example snippet below:
class Test {
constructor() {
this.prop = "bar";
this.foo = this.foo.bind(this);
}
foo() {
console.log('Foo', this.prop);
}
a() {
document.addEventListener('click', this.foo, true);
}
b() {
document.removeEventListener('click', this.foo, true);
}
}
const test = new Test();
console.log("Event listener added");
test.a();
setTimeout(() => {
console.log("Event listener removed");
test.b();
}, 3000);
class Elemento
{
constructor (numerito)
{
this.numero = document.getElementById(numerito).innerText
this.boton = document.getElementById(numerito)
}
escribir()
{
console.log(this.numero)
}
}
numeroUno = new Elemento("1")
numeroUno.boton.addEventListener("click", numeroUno.escribir)
I'm trying to show in console numerito value when button is clicked but instead it shows "undefined".
I strongly suspect this is a this binding issue - when the event handler numeroUno.escribir is called by the browser after the user clicks the button, it has "lost the context" of the numeroUno object.
One solution this is to use the bind method to fix the this reference of the method, no matter how it is called:
class Elemento
{
constructor (numerito)
{
this.numero = document.getElementById(numerito).innerText
this.boton = document.getElementById(numerito)
this.escribir = this.escribir.bind(this) // add this line
}
escribir()
{
console.log(this.numero)
}
}
You are not utilising the value that you pass to the constructor, try this:
class Elemento
{
constructor (numerito)
{
this.numero = numerito // See here, use the numerito passed to the constructor function
this.boton = document.getElementById(numerito)
}
escribir()
{
console.log(this.numero)
}
}
numeroUno = new Elemento("1")
numeroUno.boton.addEventListener("click", numeroUno.escribir)
The problem can be fixed by explicitly binding the function in the class to this.
Binding syntax is:
function_name = this.function_name.bind(this)
Here is the working solution:
<html>
<head>
<title>demo</title>
</head>
<body>
<div>
<button id="1">Numerito</button>
</div>
<script>
class Elemento {
constructor (numerito) {
this.numero = document.getElementById(numerito).innerText
this.boton = document.getElementById(numerito)
}
escribir() {
console.log("numero = " + this.numero)
}
// JavaScript expects an explicit binding of each function in the class
//
// Binding syntax is:
//
// function_name = this.function_name.bind(this)
escribir = this.escribir.bind(this)
}
numeroUno = new Elemento("1")
numeroUno.boton.addEventListener("click", numeroUno.escribir)
</script>
</body>
</html>
Imagine having a class that generates content on the page. Part of the content should have event listener attached in html such as onclick=function().
How can I make sure to call the function from within the class that constructed the html?
class Container {
constructor(hook) {
this.hook = "#" + hook;
this.addDiv = this.addDiv.bind(this);
this.fireMe = this.fireMe.bind(this);
this.init = this.init.bind(this);
this.init();
}
addDiv() {
const div = `<div onclick="fireMe()">FIRE ME</div>`;
document.querySelector(this.hook).innerHTML = div;
}
fireMe() {
console.log("hello!");
}
init() {
this.addDiv();
}
}
let div = new Container("app");
now getting error that fireMe is undefined (which is right because it is not available in global scope).
I know I can add event listener by rendering the div first and than adding the event listener, but is there a way of adding event listener from within <div> tag to actually reach Container.fireMe() method?
You have to create the element -> something like this
class Container {
constructor (hook) {
this.hook = '#' + hook;
this.addDiv = this.addDiv.bind(this);
this.fireMe = this.fireMe.bind(this);
this.init = this.init.bind(this);
this.init();
}
addDiv () {
const div = document.createElement('div');
div.textContent = 'FIRE ME';
div.addEventListener('click', this.fireMe );
document.querySelector(this.hook).innerHTML = div;
}
fireMe () {
console.log('hello!');
}
init () {
this.addDiv();
}
}
const div = new Container('app');
Never use inline event handlers as there are many reasons to avoid this 20+ year old technique that just will not die.
Instead, use modern, standards-based code with .addEventListener(). If you do this along with making the new HTML using the DOM API, you'll be able to more easily accomplish your goal:
addDiv() {
const div = document.createElement("div");
div.textConent = "FIRE ME";
div.addEventListener("click", this.fireMe);
document.querySelector(this.hook).innerHTML = div;
}
You should create elements use document.createElement() rather than using string
class Container {
constructor(hook) {
this.hook = "#" + hook;
this.addDiv = this.addDiv.bind(this);
this.fireMe = this.fireMe.bind(this);
this.init = this.init.bind(this);
this.init();
}
addDiv(){
const div = document.createElement('div');
div.innerHTML = "Fire Me";
div.addEventListener("click",this.fireMe);
document.querySelector(this.hook).appendChild(div);
}
fireMe() {
console.log("hello!");
}
init() {
this.addDiv();
}
}
let div = new Container("app");
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
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.