I Need to refactor an IIFE in ES6. In ES6 let and const have a block scope, so I really need an IIFE in ES6? This is the ES5 version of the code:
var oojs = (function(oojs) {
var createToolbarItems = function(itemElements) {
var items = [];
[].forEach.call(itemElements,
function(el, index, array) {
var item = {
toggleActiveState: function() {
this.activated = !this.activated;
}
};
Object.defineProperties(item, {
el: {
value: el
},
enabled: {
get: function() {
return !this.el.classList.contains('disabled');
},
set: function(value) {
if (value) {
this.el.classList.remove('disabled');
} else {
this.el.classList.add('disabled');
}
}
},
activated: {
get: function() {
return this.el.classList.contains('active');
},
set: function(value) {
if (value) {
this.el.classList.add('active');
} else {
this.el.classList.remove('active');
}
}
}
});
items.push(item);
});
return items;
};
oojs.createToolbar = function(elementId) {
var element = document.getElementById(elementId);
var items = element.querySelectorAll('.toolbar-item');
return {
items: createToolbarItems(items)
}
};
return oojs;
}(oojs || {}));
What is the best way to translate this code in ES6? I tried many solution but I miss something, and I get an error: oojs is not defined.
Maybe I can use a Class instead? As you can see from the code I'm writing a Toolbar API in a OOP way (I think...)
Thanks for any help
EDIT: Thanks to georg, I try to refactor my code using classes. This is the new ES6 version:
class Toolbar {
constructor(elementId) {
this.elementId = elementId;
}
get items() {
const element = document.getElementById(this.elementId);
return element.querySelectorAll(".toolbar-item");
}
createToolbarItems() {
return [...this.items].map(el => new ToolbarItem(el));
}
}
class ToolbarItem {
constructor(el) {
this.el = el;
}
get enabled() {
return !this.el.classList.contains('disabled');
}
set enabled(value) {
if (value) {
this.el.classList.remove('disabled');
} else {
this.el.classList.add('disabled');
}
}
get activated() {
return this.el.classList.contains('active');
}
set activated(value) {
if (value) {
this.el.classList.add('active');
} else {
this.el.classList.remove('active');
}
}
toggleActiveState() {
this.activated = !this.activated;
}
}
// const toolbar = new Toolbar('myToolbar');
// const toolbarItems = toolbar.createToolbarItems();
EDIT: please check if is the right way to write this code, I'm pretty new to ES6
Thanks again
You can start by factoring out the toolbar item code (var item and below):
class ToolbarItem
{
constructor(element) {
....
}
}
Now, decide if you want to keep enabled and activated as properties or refactor them to explicit methods like isEnabled and setEnabled. In the former case it would be,
class ToolbarItem {
get enabled() {
...
}
set enabled(value) {
...
}
}
while ordinary methods can be defined like this:
class ToolbarItem {
isEnabled() {
...
}
setEnabled(value) {
...
}
}
Once you get this sorted out, replace your item initialization code with items.push(new ToolbarItem(el)) and test.
Hope this helps you getting started, good luck!
Related
I have a tooltip control I've written that works very nicely in Vue 3, but I need a mechanism to fire off to all other instances to tell them to close. There are delays on close, so I'm occasionally getting two tooltips to show up at the same time.
This method, which was a crutch I've used in the past, is not allowed by the compiler / build tools. I can full well understand why, but I don't know the right way:
tooltipManager: function() {
if (!window.TooltipManager) {
function tooltipManager() {
let _data = {
tooltipIndex: 0,
callbacks: {}
};
return {
register: function (callback) {
let id = "tooltip_" + _data.tooltipIndex;
_data.tooltipIndex++;
_data.callbacks[id] = callback;
return id;
},
closeOpenPopups: function (id) {
Object.keys(_data.callbacks).forEach(key => {
if (id !== key) {
_data.callbacks[key]();
}
});
},
destroy: function (id) {
delete _data.callbacks[id];
}
};
}
window.TooltipManager = tooltipManager();
}
return window.TooltipManager()
},
The first thing I tried but didn't work was a service which I imported:
export default class TooltipManager {
constructor() {
if(! TooltipManager.instance){
this._data = {
tooltipIndex: 0,
callbacks: {}
};
}
}
register (callback) {
let id = "tooltip_" + this._data.tooltipIndex;
this._data.tooltipIndex++;
this._data.callbacks[id] = callback;
return id;
}
closeOpenPopups(id) {
Object.keys(this._data.callbacks).forEach(key => {
if (id !== key) {
this._data.callbacks[key]();
}
});
}
destroy(id) {
delete this._data.callbacks[id];
}
}
Ok, I was close with the first service. It should be written this way, and I'm going to leave my console.logs in that confirmed that it is indeed a singleton even though it is running on different tooltips.
class TooltipManager {
constructor() {
if(! TooltipManager.instance){
this._data = {
tooltipIndex: 0,
callbacks: {}
};
console.log("got new instance");
} else {
console.log("got old instance");
}
}
register (callback) {
let id = "tooltip_" + this._data.tooltipIndex;
this._data.tooltipIndex++;
this._data.callbacks[id] = callback;
console.log("registered key: " + id);
return id;
}
closeOpenPopups(id) {
Object.keys(this._data.callbacks).forEach(key => {
if (id !== key) {
console.log("closed: " + key);
this._data.callbacks[key]();
}
});
}
destroy(id) {
delete this._data.callbacks[id];
}
}
export default new TooltipManager();
I got the following from console.logs:
got new instance
TooltipManager.js:19 registered key: tooltip_0
TooltipManager.js:19 registered key: tooltip_1
TooltipManager.js:19 registered key: tooltip_2
TooltipManager.js:19 registered key: tooltip_3
TooltipManager.js:26 closed: tooltip_0
TooltipManager.js:26 closed: tooltip_1
TooltipManager.js:26 closed: tooltip_2
TooltipManager.js:26 closed: tooltip_3
And indeed it solved the problem of ghost tooltips when one pops up before the other closes with a delay to prevent bounce.
In the tooltip tool I wrote, which I will later post here as an example of how easy Vue3 Teleport makes something like this to write. I want to test it a little longer before I show it off.
I just need to:
mounted() {
this.data.tooltipId = TooltipManager.register(this.forceHide);
And, which also shows some state data I use to keep track of this:
methods: {
forceHide: function() {
if (this.data.isDisplayed) {
this.data.style = '{top: -1000px, left: -1000px}';
}
this.data.hideRequested = false;
this.data.showRequested = false;
this.data.isDisplayed = false;
},
Now the next thing maybe using Vuex for this, but I may leave this in as an alternative method so it's not dependent on it.
I created this script, however the cache cleaning warning appears in the debug console which is not defined. How can I solve it?
I uploaded the code here https://codepen.io/stiac/pen/ExPjgwe
class NotificationBanner {
constructor(el) {
this.storageKey = 'notifications'
this.el = el
this.id = this.el.dataset.id
this.el.querySelector(".closebutton").onclick = () => this.close()
this.showUnlessDismissed()
}
show() {
this.el.hidden = false
}
close() {
this.el.remove()
this.updateLocalStorage()
}
showUnlessDismissed() {
if(this.getLocalStorage().includes(this.id)) {
this.close()
}
else {
this.show()
}
}
updateLocalStorage() {
const dismissedNotifications = this.getLocalStorage()
if(!dismissedNotifications.includes(this.id)) {
dismissedNotifications.push(this.id)
localStorage.setItem(this.storageKey, JSON.stringify(dismissedNotifications))
}
}
getLocalStorage() {
return JSON.parse(localStorage.getItem(this.storageKey)) || []
}
}
class NotificationBanners {
constructor() {
const notifications = [...document.querySelectorAll(".notification-banner")];
notifications.forEach(function(notification) {
return new NotificationBanner(notification);
})
}
}
new NotificationBanners()
clearcache.onclick = e => localStorage.setItem('notifications', JSON.stringify([]))
It is a script to hide a message. I wish I could set a deadline to make it appear after a few days.
I'm messing around with decorators for a bit, having an Angular background i'm trying to wrap my head around the HostListener decorator.
This is how far i got:
class Demo {
counter = 0;
#Listen("mousemove") onMouseMove(e?) {
console.log(this);
this.counter++;
}
}
export function Listen(name) {
return (target, key, descriptor) => {
window.addEventListener(name, oldValue.bind(target));
return descriptor;
};
}
new Demo();
This is more or less the implementation only problem is passing the target/this reference as target is not initialised.
Solved, i'm using Vue so this might not be everyones answer, basically what i'm doing is calling the function just once in Vue you can add a mixin and inside this mixin the beforeMount hook will be called, thus allowing me here to call it once.
Decorator updated code:
export function Listen(name) {
return function (target, key, descriptor) {
if (!target.subscriptions) target.subscriptions = [];
add(target, "Listen", key);
if (process.client) {
const oldValue = descriptor.value;
descriptor.value = function() {
target.subscriptions.push(
target.$eventManager.add(window, name, oldValue.bind(this))
);
};
return descriptor;
}
return descriptor;
};
}
const add = (target, name, functionName) => {
if(!target.decorators) target.decorators = {};
if(!target.decorators[name]) target.decorators[name] = [];
target.decorators[name].push(functionName);
};
Vue mixin:
Vue.mixin({
beforeMount: function() {
if(this.decorators && this.decorators.Listen) {
this.decorators.Listen.forEach(key => this[key]());
}
},
destroyed: function () {
this.$subscriptionManager.remove(this.subscriptions);
}
});
I've managed to get a fairly complex setup (though that's a question for Code Review) for my mixins that looks like this:
TooManyCaps.js
module.exports = {
labelCopyCaps: () => {
if (this.release.tracks.length > 1) {
if (_this._notEnoughLowercase(this.release.title)) {
this._recordError(release, 'LABELCOPYCAPS');
} else {
this.release.tracks.some( (track) => {
if (this._lowerCaseCount(track.label_copy)) {
this._recordError(release, 'LABELCOPYCAPS');
return true;
}
});
}
}
},
_notEnoughLowercase: (str) => {
if ((str.match(/[a-zA-Z]/g)||[]).length > 3
&& str.length - str.replace(/[a-z]/g, '').length) {
return true;
}
return false;
}
};
I then have an Object that would use this as a mixin:
Rule.js
class Rule {
constructor(release) {
this.release = release;
this.errors = [];
}
_recordError(error, options) {
this.errors.push({
release_id: this.release.id,
rule: error,
options: options,
});
}
}
module.exports = Rule;
i then have an index page that joins them together
index.js
const TooManyCaps = require('./TooManyCaps');
const Rule = require('./Rule');
Object.assign(Rule.prototype, [TooManyCaps]);
module.exports = Rule;
And then my main start of the program that does some instantiating of things:
'use strict';
const RuleValidator = require('./job/validation/RuleValidatorMixin');
const Rule = require('./job/validation/rulesmixins/rules/index');
// some logic that's a loop
arr.forEach((value) => {
new RuleValidator(new Rule(value)).validate();
}
and within validate() I have:
validate() {
console.log('VALIDATE');
this.rule.labelCopyCaps();
// console.log(this.rule);
}
But then when I run this, I get:
this.rule.labelCopyCaps is not a function
So where have i gone wrong?
Object.assign does not take an array:
Object.assign(Rule.prototype, [TooManyCaps]);
// ^ ^
should be just
Object.assign(Rule.prototype, TooManyCaps);
I'm developing win8(metro style) application with Html5-js-jquery.
I have this code segment;
GetBoutiqueDetail: function (boutiqueId, options) {
if (IsUserLogin()) {
//different job A
} else {
ShowLoginPanel(undefined);
}
},
GetProductDetail: function (boutiqueId, productId, options) {
if (IsUserLogin()) {
//different job B
} else {
ShowLoginPanel(undefined);
}
},
AddBasket: function (productId, productVariantId, quantity, options) {
if (IsUserLogin()) {
//different job C
} else {
ShowLoginPanel(undefined);
}
},....
.And ~20 functions should check if user login or not.
I should call functions like similar to "Library.GetBoutiqueDetail();"
So my question is simple, how can I refactor that code to remove these if-else sections ?
Try something like this:
checkLogin: function( action, actionArgs ) {
if( IsLogin ) {
return action.apply(this, actionArgs );
}
ShowLoginPanel();
},
GetBoutiqueDetail: function (boutiqueId, options) {
//different job A
},
GetProductDetail: function (boutiqueId, productId, options) {
//different job B
},
AddBasket: function (productId, productVariantId, quantity, options) {
//different job C
}
How about an object map for this:
var objMap = {
"GetBoutiqueDetail":fnJobA,
"GetProductDetail":fnJobB,
"AddBasket":fnJobC}
....
}
if (loggedIn) {
objMap[task]();
}
else {
doLogin();
}
You could always wrap the common code into a higher-scope function, and have it invoked from the library functions - e.g:
//Higher scope:
function CheckForLogin(executionFunction)
{
if(IsLogin) {
executionFunction();
} else {
ShowLoginPanel(undefined);
}
};
GetBoutiqueDetail: function (boutiqueId, options) {
CheckForLogin(//different job A)
}
Passing in different job 'N' as an anonymous function to CheckForLogin
In Javascript you can return from a function to end it, so f.ex:
GetProductDetail: function (boutiqueId, productId, options) {
if (!IsLogin) return ShowLoginPanel();
// different job...
}
You will still have some repetitive code though. Another option is to define a higher level function. Something like:
var loginOrAction = function() {
if (!IsLogin) return ShowLoginPanel();
var args = [].slice.call(arguments);
Library[args.shift()].apply(Library, args);
}
loginOrAction('GetBoutiqueDetail', boutiqueId, options);
use the ternary operator
(IsLogin) ? jobA() : ShowLoginPanel(undefined)