In a project with a soft page transition I have a hard time killing all redundant listeners. On a page-change the removeEventListeners function is called. However I'm not able to remove the listeners on the window.
I know the variable scoping in the first code example is wrong. And I've been trying to fix it in the second example. Unsuccessfully. Any ideas?
export default class Slider extends Core {
init() {
const swiper = new Swiper('.mySlider', {
slidesOffsetBefore: this.sliderOffset(),
});
window.addEventListener('resize', () => {
swiper.params.slidesOffsetBefore = this.sliderOffset();
swiper.update();
});
}
removeEventListeners() {
// this doesn't work since there is no function reference
window.removeEventListener('resize');
}
sliderOffset() {
// does stuff and...
return value
}
}
This doesn't seem to work either...
export default class Slider extends Core {
init() {
this.swiper = new Swiper('.mySlider', {
slidesOffsetBefore: this.sliderOffset(),
});
window.addEventListener('resize', this.updateSwiper);
}
updateSwiper() {
this.swiper.params.slidesOffsetBefore = this.sliderOffset();
this.swiper.update();
}
removeEventListeners() {
window.removeEventListener('resize', this.updateSwiper);
}
sliderOffset() {
// does stuff and...
return value
}
}
Related
I added a handler for the 'cuechange' event to a Text Track" This works fine. But I can not find a way to remove this handler. I tried each of instructions below to remove the handler, but it still gets called.
onHiliteSpeech() {
const textTrack = this.videojsComponent.getTextTrack();
const handleCueChange = () => {
...
console.log(in event handler);
}
};
if (this.bevents) {
textTrack.addEventListener('cuechange', handleCueChange);
} else {
// none of the below instructions remove the handler.
textTrack.removeEventListener('cuechange', handleCueChange);
// textTrack.removeAllListeners();
// textTrack.removeAllListeners('cuechange');
// textTrack.eventListeners = null;
}
}
In my videojsComponent:
getTextTrack(): TextTrack {
return this.player.textTracks()[0];
}
After some trial and error, I found the problem. The function "handleCueChange" should not be a nested function within onHiliteSpeech.
I moved handleCueChange outside of onHiliteSpeech. (This also involved some work to allow handleCueChange to access some OnHiliteSpeech properties.) The new working code became:
const handleCueChange = () => {
...
console.log(in event handler);
}
};
onHiliteSpeech() {
textTrack.addEventListener('cuechange', this.handleCueChange);
...
textTrack.removeEventListener('cuechange', this.handleCueChange);
}
Here is my Javascript module:
const Calculator = (function() {
return {
listen: function (formId) {
this.formId = formId;
this.calculatorForm = document.querySelector(`#form_${this.formId}`);
if (this.calculatorForm) {
this.addEventListeners();
}
},
addEventListeners: function() {
const self = this;
this.calculatorForm.addEventListener('submit', function(event) {
console.log('calculatorForm submit', self);
self.calculatorSubmission(event);
}, false);
},
calculatorSubmission: function(event) {
event.preventDefault();
console.log('Form submitted', this.calculatorForm);
}
};
})();
export default Calculator;
I build all the Javascript using Webpack so I can load modules like this:
import Calculator from './modules/calculator';
The page in question where the Javascript is loaded has tabbed content. Each tab contains a different form, all using the Calculator module so when I switch between tabs, I call:
Calculator.listen('form-id');
The issue I have is when I switch between tabs a few times. Say I view tab 3, 5 times and then fill out and submit form in tab 3. The form is submitted 5 times because of the addEventListener called each time I view tab 3. Make sense?
I'm struggling to fix it - probably because I've been looking at it for hours now and my head is now mash.
Is the problem my module setup?
What best approach to overcoming my issue?
Thanks
I've updated my Calculator to be a class and this seems to work as expected now.
Any improvements welcome!
class Calculator {
constructor() {
if (!Calculator.instance) {
Calculator.instance = this;
}
this.calculatorSubmissionHandler = function(event) {
Calculator.instance.calculatorSubmission(event);
};
return Calculator.instance;
}
listen(formId) {
this.formId = formId;
this.calculatorForm = document.querySelector(`#${this.formId}`);
if (this.calculatorForm) {
this.addEventListeners();
}
}
addEventListeners() {
this.calculatorForm.addEventListener('submit', this.calculatorSubmissionHandler, false);
}
calculatorSubmission(event) {
event.preventDefault();
console.log('Form submitted', Calculator.instance.formId);
}
}
I am trying to destroy and re-load my Flickity slideshow while using Swup for page transitions, and I am not having much luck. This is my js file:
import Swup from 'swup';
var Flickity = require('flickity');
function init() {
if (document.querySelector('.testimonials-slideshow')) {
var flkty = new Flickity('.testimonials-slideshow', {
wrapAround: true,
pageDots: false,
autoPlay: true,
arrowShape: 'M68.374,83.866L31.902,50L68.374,16.134L64.814,12.3L24.214,50L64.814,87.7L68.374,83.866Z'
});
}
}
function unload() {
flkty.destroy();
}
init();
const swup = new Swup();
swup.on('contentReplaced', init);
swup.on('willReplaceContent', unload);
But when I try this I get the error flkty is not defined. Can anyone give me any pointers on this?
Variable scoping
As mentioned by CBroe, your var is undefined because of where you define it. It is defined in a function, but should be defined at the "top level".
import Swup from 'swup';
var Flickity = require('flickity');
// Added a "global" definition here:
var flkty;
function init() {
if (document.querySelector('.testimonials-slideshow')) {
// Removed var:
flkty = new Flickity('.testimonials-slideshow', {
wrapAround: true,
pageDots: false,
autoPlay: true,
arrowShape: 'M68.374,83.866L31.902,50L68.374,16.134L64.814,12.3L24.214,50L64.814,87.7L68.374,83.866Z'
});
}
}
function unload() {
flkty.destroy();
}
init();
const swup = new Swup();
swup.on('contentReplaced', init);
swup.on('willReplaceContent', unload);
Furthermore, if you are using any kind of module bundler, sometimes it can still get lost, so you could consider doing something like:
window.flkty = new Flickity('.testimonials-slideshow', ...
And always reference it in that way, i.e.
window.flkty.destroy();
Only destroying instances that exist
That's it for your variable definition. The next potential error is that you only init flkty when the query selector matches:
if (document.querySelector('.testimonials-slideshow')) {
But you destroy it every willReplaceContent, so really you could do with a check on "is it inited, this page load?". In this instance, you can do a check like so:
// Init the var as false:
var flkty = false
function init() {
if (document.querySelector('.testimonials-slideshow')) {
flkty = new Flickity('.testimonials-slideshow', ...);
}
}
function unload() {
if(flkty){
flkty.destroy();
// Make sure the flkty var is set to false at the end:
flkty = false;
}
}
Neatening up your code
This can all get a bit out of hand, so what we started doing was creating modules. Here is a skeleton of a carousel module we use:
// modules/Carousel.js
import Swiper from "swiper";
export default {
carouselEl: null,
carouselSwiper: null,
setup() {
this.carouselEl = document.getElementById("header-carousel");
if (!this.carouselEl) {
// Just stop if there is no carousel on this page
return;
}
this.carouselSwiper = new Swiper(this.carouselEl, { ... });
this.carouselSwiper.on("slideChange", () => { ... });
},
destroy() {
// If we already have one:
if (this.carouselSwiper) {
this.carouselSwiper.destroy();
}
// Make sure we are reset, ready for next time:
this.carouselSwiper = null;
},
};
Then, in our main.js we do something like you have:
import Carousel from "./modules/Carousel.js";
function init(){
Carousel.setup();
// Add more here as the project grows...
}
function unload(){
Carousel.unload();
}
swup = new Swup();
swup.on("contentReplaced", init);
swup.on("willReplaceContent", unload);
init();
All of the modules have setup and unload functions that won't break if the elements don't exist, so we can call all of them on each page load and unload.
I love swup but also have personal experience in the nightmare of initing and destroying things so let me know if you need any further help.
My code is supposedly made to show two sprites of buttons (nothing terribly complex (or is it?), but nothing is appearing, not even the blue screen that is supposed to show with only creating the game variable and initiating it. All my code has been made from following the official Excalibur documentation, so what is happening?
The code:
var game = new ex.Engine({
width: 1024,
height: 768
});
function loadAssets()
{
var loader = new ex.Loader();
var resources = {
txGameTitle: new.ex.Texture("icons/GUI/final/"),
txStartButton: new.ex.Texture("icons/GUI/final/MenuPlayButton.png"),
txLoadButton: new.ex.Texture("icons/GUI/final/MenuLoadButton.png"),
txOptionsButton: new.ex.Texture("icons/GUI/final/"),
txExitButton: new.ex.Texture("icons/GUI/final/"),
txMenuBackground: new.ex.Texture("icons/GUI/final/"),
txMenuMusic: new.ex.Sound("icons/GUI/final/")
};
for (var loadable in resources)
{
if (resources.hasOwnProperty(loadable))
{
loader.addResource(resources[loadable]);
}
}
}
function startUp()
{
var StartButton = new ex.Actor.extend({
onInitialize: function (engine)
{
this.addDrawing(txStartButton.asSprite());
}
});
var LoadButton = new ex.Actor.extend({
onInitialize: function (engine)
{
this.addDrawing(txLoadButton.asSprite());
}
});
}
function init()
{
loadAssets();
startUp();
}
init();
game.start(loader).then(function () {
console.log("Game started!");
});
sorry for the bad formatting.
I think that could be because of a code error. I noticed that right at the end of the file
game.start(loader)
the loader variable is referenced but seems like it is not defined. There is the same variable which created inside loadAssets function, but it is a local one. Probably in order to use it, you need to define it above.
var loader;
function loadAssets() {
loader = ...
}
...other code
game.start(loader).then(...
Another variant is to define loader outside the loadAssets function.
var loader = new ex.Loader();
function loadAssets() {
var resources = {...
}
...other code
game.start(loader).then(...
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.