Accessors Composition in ES6 Classes - javascript

Let's say I have a Thing class which I want to be both Hideable and Openable.
Using a similar approach to Douglas Crockford's object creation through composition, I have been able to "inherit" from multiple classes.
This approach does not work with accessors (getter/setters).
I need to use classes as it's a requirement. I'm also finding that I am duplicating functionality from class to class, but I don't want these to inherit from a base class.
Any ideas?
The progress I have made so far is in the below snippet:
class Openable {
constructor(isOpen = false) {
this._isOpen = isOpen;
}
get isOpen() {
return this._isOpen + ' is stupid.';
}
set isOpen(value) {
this._isOpen = value;
}
}
class Hideable {
constructor(isHidden = false) {
this._isHidden = isHidden;
}
get isHidden() {
return this._isHidden + ' is stupid.';
}
set isHidden(value) {
this._isHidden = value;
}
}
class Thing {
constructor(config) {
let { isOpen, isHidden } = config;
let openable = new Openable(isOpen);
this.isOpen = openable.isOpen;
let hideable = new Hideable(isHidden);
this.isHidden = openable.isHidden;
}
}
let thing = new Thing({
isOpen: true,
isHidden: false
});

Because isOpen and isHidden are accessors, you can't just grab a copy of them, you have to access them when you want them.
Still, you can create your own isOpen, isHidden which use the underlying ones:
let openable = new Openable(isOpen);
Object.defineProperty(this, "isOpen", {
get: () => openable.isOpen,
set: value => {
openable.isOpen = value;
}
});
let hideable = new Hideable(isHidden);
Object.defineProperty(this, "isHidden", {
get: () => hideable.isHidden,
set: value => {
hideable.isHidden = value;
}
});
Live example on Babel's REPL
Naturally, if you do this a lot, you'd want to have a worker function to set that up rather than retyping it all the time:
function wrapProperty(dest, src, name) {
Object.defineProperty(dest, name, {
get: () => src[name],
set: value => { src[name] = value; }
});
}
(or do it by grabbing the property descriptor and updating it)
then:
wrapProperty(this, openable, "isOpen");
wrapProperty(this, hideable, "isHidden");
I'd question the requirement that you must use class for Openable and Hideable. They look much more like mixins to me.

Besides that the OP's accessor approach via "pseudo private property" notation and prototypal getters/setters for Openable/Hideable classes already is questionable, traits would come closest to the also doubtable requirement of using classes as mixin surrogates just for the sake of meeting documentation requirements.
As long as JavaScript does not provide traits natively, one has to stick to either more advanced class based mixin patterns ore one remembers Angus Croll's Flight Mixins.
A mixin's function body one has to write is close enough to the constructor body of a class. Nevertheless function based mixins never will be instantiated but always have to be applied to an object/type via either call or apply.
A possible solution, featuring this kind of mixin approach, that already reliably fulfills the OP's requirements then might look like the next provided example code ...
let
Openable = (function openableMixinFactory () {
let
defineProperty = Object.defineProperty,
isBoolean = (type => (typeof type == 'boolean'));
return function openableMixinApplicator (isOpen = false) {
let
openableCompositeType = this,
getIsOpen = (() => isOpen),
setIsOpen = (value => ((isBoolean(value) && (isOpen = value)) || (void 0)));
defineProperty(openableCompositeType, 'isOpen', {
get: getIsOpen,
set: setIsOpen,
enumerable: true
});
return openableCompositeType;
};
}()),
Hideable = (function hideableMixinFactory () {
let
defineProperty = Object.defineProperty,
isBoolean = (type => (typeof type == 'boolean'));
return function hideableMixinApplicator (isHidden = false) {
let
hideableCompositeType = this,
//getIsHidden = (() => isHidden),
getIsHidden = (() => [isHidden, 'is stupid.'].join(' ')),
setIsHidden = (value => ((isBoolean(value) && (isHidden = value)) || (void 0)));
defineProperty(hideableCompositeType, 'isHidden', {
get: getIsHidden,
set: setIsHidden,
enumerable: true
});
return hideableCompositeType
};
}());
class Thing {
constructor(config) {
let
{isOpen, isHidden} = config;
Openable.call(this, isOpen);
Hideable.call(this, isHidden);
}
}
var
thing = new Thing({ isOpen: true/*, isHidden: false*/ });
console.log('thing : ', thing);
console.log('thing.isOpen : ', thing.isOpen);
console.log('thing.isHidden : ', thing.isHidden);
console.log('(thing.isOpen = "xyz") : ', (thing.isOpen = "abc"));
console.log('(thing.isHidden = "xyz") : ', (thing.isHidden = "xyz"));
console.log('thing.isOpen : ', thing.isOpen);
console.log('thing.isHidden : ', thing.isHidden);
console.log('(thing.isOpen = false) : ', (thing.isOpen = false));
console.log('(thing.isHidden = true) : ', (thing.isHidden = true));
console.log('thing.isOpen : ', thing.isOpen);
console.log('thing.isHidden : ', thing.isHidden);
.as-console-wrapper { max-height: 100%!important; top: 0; }
Other answers of mine at SO, that provide similar solutions to related questions, featuring the same approach are ...
How do I organize data by common traits?
Composition/Inheritance/Factory - Best Pattern For This Case
Multiple inheritance using classes
Class inheritance between projects

Related

Have the niceties of extended prototype without actually extending the prototype

What I'm trying to do is have an interface where you can wrap an element into an instance which has an extended prototype, so something like.
const wrappedButton = new WrappedElem(document.getElementById('some-button'))
// we have access to our custom added method:
console.log(`I am the ${wrappedButton.siblingIndex()}th button`)
// but we still have access to the original methods
wrappedButton.addEventListener('click', function(e){
console.log('I clicked on', e.target)
// right here we won't have access to siblingIndex, because the prototype of the element itself was not changed.
})
I can extend the original prototype like this
HTMLElement.prototype.siblingIndex = function() {
if(this.parentNode == null) {
return -1
}
return Array.prototype.indexOf.call(this.parentNode.children, this)
}
But extending the prototype is bad practice, and bad performance.
So it possible to do something like this?
By using a Proxy we can add a new method without protoype changes :
const siblingIndex = () => {}
const handler = {
get(target, property) {
if(property === 'siblingIndex') {
return siblingIndex;
}
return target[property];
}
}
const proxyButton = new Proxy(button, handler);
// we can also call the siblingIndex function
proxyButton.siblingIndex();
// we can access the properties of the underlying object
proxyButton.tagName;
e.target however will not return the proxy but the original object,
but you can just use proxyButton instead of e.target
if you want you can also override the addEventListener method to return the proxied version instead when the callback is called
This is what seems to work alright. Big thanks to Lk77.
var wrappedElMethods = {
siblingIndex() {
if(this.parentNode == null) {
return -1
}
return Array.prototype.indexOf.call(this.parentNode.children, this)
}
}
function wrappedEl(el) {
return proxyInherit(el, wrappedElMethods)
}
function proxyInherit(item, overwrites) {
const handler = {
get(target, property) {
let value
const overwriteVal = overwrites[property]
if(overwriteVal != undefined) {
value = overwriteVal
} else {
value = target[property]
}
if(value instanceof Function) {
return value.bind(item)
}
return value
},
set(target, property, value) {
target[property] = value
}
}
return new Proxy(item, handler)
}
// Usage:
var button = wrappedEl(e.target)
button.onclick = function() {
console.log(button.siblingIndex())
}

How do I manage context when exposing object methods in JS modules?

Okay, I realize this can be considered subjective, but I'm trying to better understand how to consider scope when writing modules that only expose what's needed publicly. I have a string utility that I've written as an object literal below:
const substrings = {
query: {},
text: "",
results: [],
exists: function (index) {
const exists = index >= 0
return exists
},
check: function () {
const q = this.query
const start = q.openIndex
const stop = q.closeIndex
if (q.hasOpen && !q.hasClose) {
console.log("Missing closing delimiter.")
}
if (!q.hasOpen && q.hasClose) {
console.log("Missing opening delimiter.")
}
if (q.hasOpen && q.hasClose && start > stop) {
console.log("Closing delimiter found before opening.")
}
if (!q.hasOpen && !q.hasClose && this.results.length == 0) {
console.log("No results found.")
}
const order = start < stop
const check = q.hasOpen && q.hasClose && order
return check
},
update: function () {
const q = this.query
const text = this.text
q.before = this.text.indexOf(q.open)
q.start = q.before + q.open.length
this.text = text.slice(q.start, text.length)
q.stop = this.text.indexOf(q.close)
q.after = q.stop + q.close.length
q.openIndex = q.before
q.closeIndex = q.before + q.stop
q.hasOpen = this.exists(q.openIndex)
q.hasClose = this.exists(q.stop)
const newPosition = q.start + q.after
q.position = q.position + newPosition
this.query = q
},
substrings: function () {
const q = this.query
const current = this.text.slice(0, q.stop)
const fullLength = this.text.length
this.text = this.text.slice(q.after, fullLength)
this.results.push(current)
this.update()
if (this.check()) {
this.substrings()
}
},
init: function (open, close, text) {
this.results = []
this.query = {
open,
close,
position: 0,
}
this.text = text
this.update()
},
getSubstrings: function (open, close, text) {
this.init(open, close, text)
if (this.check()) {
this.substrings()
return this.results
}
},
getSubstring: function (open, close, text) {
this.init(open, close, text)
if (this.check()) {
return this.text.slice(0, this.query.stop)
}
}
}
I want to use it as a Node module and expose the getSubstring and getSubstrings methods, but if I were to do:
module.exports = {
all: substrings.getSubstrings,
one: substrings.getSubstring
}
I would get an error due to the usage of this. I realize that if I replace this with the object var name substrings to reference it directly, it works. I could also refactor it to be one big function or smaller functions and just export the 2 I need.
I am trying to go about learning things the right way and am struggling with how I should be thinking about context. I understand how this changes here, but I feel like I'm not fully wrapping my head around how I should consider context when structuring my code.
Is there a more elegant solution to expose methods with code like this that wasn't written to separate private and public methods?
A simple solution would be to bind the exported functions to the proper calling context inside the exports object:
module.exports = {
all: substrings.getSubstrings.bind(substrings),
one: substrings.getSubstring.bind(substrings)
}
Personally, I prefer using the revealing module pattern over object literals for situations like this. With the revealing module pattern, create an IIFE that returns the desired functions, referring to local variables instead of properties on this. For example:
const { getSubstrings, getSubstring } = (() => {
let query = {}
let text = ''
let results = []
function exists(index) {
return index >= 0
}
function check() {
const q = query;
// ...
}
...
function getSubstrings(open, close, text) {
}
...
return { getSubstrings, getSubstring };
})();
module.exports = {
all: getSubstrings,
one: getSubstring
}
This is somewhat opinion-based, but code can be easier to read when there aren't any this references to worry about.

Defining chained string transform functions in Javascript

Chaining Methods, also known as Cascading, refers to repeatedly calling one method after another on an object, in one continuous line of code.
Writing code like this:
str.replace("k", "R").toUpperCase().substr(0,4);
is not just pleasurable and convenient but also succinct and intelligible. It allows us to read code like a sentence, flowing gracefully across the page. It also frees us from the monotonous, blocky structures we usually construct.
This blog talks about how to do it generally, however considering extending the String prototype will be good enough for my case, I don't know whether it is an overkill or not.
Basically I don't know enough Javascript to make the judgement call of,
whether go with the above general way,
or just extend the String prototype, knowing that it is generally a bad practice
or maybe using function wrapper (or maybe it is totally irrelevant)
I also know the following is another option, but it's kind of rigid, I.e., translated into my case, it means the string transform can only be
replace("k", "R") then followed by toUpperCase() then followed by substr(0,4).
// https://medium.com/javascript-scene/javascript-factory-functions-with-es6-4d224591a8b1
console.log('Begin');
const withConstructor = constructor => o => {
const proto = Object.assign({},
Object.getPrototypeOf(o),
{ constructor }
);
return Object.assign(Object.create(proto), o);
};
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
// or `import pipe from 'lodash/fp/flow';`
// Set up some functional mixins
const withFlying = o => {
let isFlying = false;
return {
...o,
fly () {
isFlying = true;
return this;
},
land () {
isFlying = false;
return this;
},
isFlying: () => isFlying
}
};
const withBattery = ({ capacity }) => o => {
let percentCharged = 100;
return {
...o,
draw (percent) {
const remaining = percentCharged - percent;
percentCharged = remaining > 0 ? remaining : 0;
return this;
},
getCharge: () => percentCharged,
get capacity () {
return capacity
}
};
};
const createDrone = ({ capacity = '3000mAh' }) => pipe(
withFlying,
withBattery({ capacity }),
withConstructor(createDrone)
)({});
const myDrone = createDrone({ capacity: '5500mAh' });
console.log(`
can fly: ${ myDrone.fly().isFlying() === true }
can land: ${ myDrone.land().isFlying() === false }
battery capacity: ${ myDrone.capacity }
battery status: ${ myDrone.draw(50).getCharge() }%
battery drained: ${ myDrone.draw(75).getCharge() }%
`);
console.log(`
constructor linked: ${ myDrone.constructor === createDrone }
`);
console.log('End');
To make it simple, I need to define my own functions like
str.transform1("k", "R").transform2().transform3(0,4);
that do just as
str.replace("k", "R").toUpperCase().substr(0,4);
In reality each function will be a set of complicated replace() function call, but let's make it simple for this question.
What best should I do?

Javascript es6 class handling error

I have created a JS class. Here is following code:
export default class Service {
constructor(
serviceId,
serviceName,
serviceDescription,
serviceImageName,
categoryId,
servicePrice,
currencyCode,
acceptPayment,
serviceDuration,
multipleBookingPerSlot,
mode,
tzSupport,
minOptionCount
) {
try{
this.id = serviceId;
this.title = serviceName;
this.subTitle = serviceDescription;
this.imageUrl = serviceImageName;
this.categoryId = categoryId;
this.price = servicePrice;
this.currencyCode = currencyCode;
this.acceptPayment = acceptPayment;
this.meetingDuration = serviceDuration;
this.multipleBookingPerSlot = multipleBookingPerSlot;
this.serviceName = serviceName;
this.mode = mode;
this.tzSupport = tzSupport;
this.session = minOptionCount
} catch(e){
if(e instanceof ReferenceError){
console.error("Service data missing.")
}
}
}
}
My goal is whenever new object of Service creates like new Service('1') if any of key is missing code should throw error and stop execution. How can i achieve this?
You won't get a ReferenceError if the caller doesn't supply enough arguments, you'll just see undefined in the parameters.
You have 13 parameters (which is far, far too many). You could do the brute-force thing:
if (arguments.length < 13) {
throw new Error("Missing arguments");
}
Instead, though, I suggest using the builder pattern or an options object instead of 13 discrete parameters. More than three parameters is fairly hard to manage.
For instance, with an options object:
export default class Service {
constructor(
options
) {
["id", "title", "subTitle", "imageUrl", "categoryId", "price", "currencyCode",
"acceptPayment", "meetingDuration", "multipleBookingPerSlot", "serviceName",
"mode", "tzSupport", "session"].forEach(name => {
if (!options.hasOwnProperty(name)) {
throw new Error(name + " is a required option");
}
});
Object.assign(this, options);
}
}
Usage:
let s = new Service({id: 1, title: "foo", /*...etc...*/});
That way, the caller isn't lost in a sea of parameters.
However, if it's important to validate the parameter values are present, isn't it important to validate their values, too? Nothing's to stop me from calling new Service with 13 completely-invalid arguments (undefined repeated 13 times, for instance).
So I would probably use an options object (because it's much easier for the caller) combined with parameter destructuring, and then individual validation, e.g.:
export default class Service {
constructor({ // <== Notice the {
id,
name,
decription,
imageUrl,
categoryId,
price,
currencyCode,
acceptPayment,
meetingDuration,
multipleBookingPerSlot,
mode,
tzSupport,
minOptionCount
}) { // <== And the }
this.id = validate.positiveNumber(id);
this.title = validate.nonBlank(name);
this.subTitle = validate.nonBlank(description);
this.imageUrl = validate.URL(imageUrl);
this.categoryId = validate.positiveNumber(categoryId);
this.price = validate.price(price);
this.currencyCode = validate.currencyCode(currencyCode);
this.acceptPayment = validate.boolean(acceptPayment);
this.meetingDuration = validate.duration(meetingDuration);
this.multipleBookingPerSlot = validate.boolean(multipleBookingPerSlot);
this.serviceName = this.title; // Already validated
this.mode = validate.mode(mode);
this.tzSupport = validate.tzSupport(tzSupport);
this.session = validate.whateverThisIs(minOptionCount);
}
}
...where validate is a set of reusable validations. Usage is the same as above:
let s = new Service({id: 1, title: "foo", /*...etc...*/});
As i already commented assigning undefined to an objects property is completely valid. The solution might be to check for the values of the arguments Arraylike against undefined:
constructor(a,b,c){
if(arguments.length!=3){//check length
return;
}
for(var a=0;a<arguments.length;a++){
if(arguments[a]===undefined){//check against undefined
return;
}
}
//your code
}
http://jsbin.com/vugepakama/edit?console

Vanilla JavaScript: Is there a way to toggle multiple CSS-classes in one statement?

I use these JavaScript-code to change classes in my script:
var toggleDirection = function() {
group.classList.toggle('left-to-right');
group.classList.toggle('right-to-left');
}
In my example there a only two classes to change but could be also multiple classes ...
So therefore: Does anyone know a way to write the example less redundant?
No it is not possible using Element.classList API directly. Looking at API you can read:
toggle ( String [, force] ) When only one argument is present: Toggle
class value; i.e., if class exists then remove it, if not, then add
it. When a second argument is present: If the second argument is true,
add specified class value, and if it is false, remove it.
Reference here.
You could potentially write your own "utility" function (in vanilla JS) which does what you want, below a very simple demonstrative example which work on top of the classList API:
var superToggle = function(element, class0, class1) {
element.classList.toggle(class0);
element.classList.toggle(class1);
}
And you call it in this way:
superToggle(group,'left-to-right', 'right-to-left');
For anyone looking for a short answer, you can do this on one line using the rest parameter introduced in ES6/ES2015:
const toggleCSSclasses = (el, ...cls) => cls.map(cl => el.classList.toggle(cl))
This is pretty close to #attacomsian's answer, but taking advantage of the fact that the rest parameter will return an array - no matter how many arguments is being passed to the function. Which means we can skip the part where we detect whether we're working with a string or an array.
const toggleCSSclasses = (el, ...cls) => cls.map(cl => el.classList.toggle(cl));
const one = document.querySelector(".one");
one.addEventListener("click", () => {
toggleCSSclasses(one, "class1");
});
const two = document.querySelector(".two");
two.addEventListener("click", () => {
toggleCSSclasses(two, "class1", "class2");
});
.class1 { text-decoration: underline }
.class2 { background: steelblue }
<p class="one">Click to toggle one class</p>
<p class="two">Click to toggle two classes</p>
just use the map array.
like
['left-to-right', 'right-to-left'].map(v=> group.classList.toggle(v) )
Here is ES6 version of solution
const classToggle = (el, ...args) => args.map(e => el.classList.toggle(e))
const classToggle = (el, ...args) => {
args.map(e => el.classList.toggle(e))
}
.a {
color: red
}
.b {
background: black
}
.c {
border-color: yellow
}
<button onclick="classToggle(this,'a', 'c','b')" class="a b c ">Click me</button>
And here's old JS code:
var classToggle = function classToggle(el) {
for (
var _len = arguments.length,
args = new Array(_len > 1 ? _len - 1 : 0),
_key = 1;
_key < _len;
_key++
) {
args[_key - 1] = arguments[_key];
}
args.map(function (e) {
return el.classList.toggle(e);
});
};
Answer from year 2020 here!
Found this article helpful from 4/2021
Can use comma separated list of classes like this:
const button = document.getElementById('button')
button.classList.add('btn', 'btn-primary', 'btn-primary--footer')
button.classList.remove('btn', 'btn-primary', 'btn-primary--footer')
or even spread syntax from a list of classes:
const button = document.getElementById('button')
const classes = ['btn', 'btn-primary', 'btn-primary--footer']
button.classList.add(...classes)
button.classList.remove(...classes)
You can extend the DOMTokenList object with the following multiToggle
if (window["DOMTokenList"]) //check if DOMTokenList is an existing object.
{
//multitoggle
DOMTokenList.prototype.multiToggle = function()
{
if (arguments.length > 0) // there needs to be at least one object
{
for (argument in arguments) //loop all arguments
{
var argument = arguments[argument];
//All primitives are allowed as input (Symbol not included). If not a primitive, raise error.
if (Object.prototype.toString.call(argument) !== "[object Undefined]" && Object.prototype.toString.call(argument) !== "[object Null]" && Object.prototype.toString.call(argument) !== "[object String]" && Object.prototype.toString.call(argument) !== "[object Number]" && Object.prototype.toString.call(argument) !== "[object Boolean]")
{
throw new SyntaxError;
}
else
{
if (this.contains(argument)) //check if classList contains the argument.
{
this.remove(argument); //if so remove
}
else
{
this.add(argument); //if not add
}
}
}
}
else
{
throw new Error("The argument is not optional");
}
return undefined; //return undefined as with add and remove.
}
}
multiToggle does not have the force ability of the original toggle. It just turns class names on and off for as many arguments as supplied.
Warning, expanding fixed Objects can cause troubles in the future . When an object gets deprecated or changed your functionality could break, requiring to more maintenance.
There is no direct way but you can create a helper function:
const toggleClass = (el, cls) => {
if (Array.isArray(cls)) {
cls.map((cl) => {
el.classList.toggle(cl);
});
} else {
el.classList.toggle(cls);
}
};
Now just call toggleClass() like below:
// single class
toggleClass(document.querySelector('body'), 'left-to-right');
//multiple classes
toggleClass(document.querySelector('body'), ['left-to-right', 'right-to-left']);
If I need to toggle multiple classes I just create an array and then iterate through it.
var classes = [
"red",
"blue",
"green",
"purple"
]
for (var i = 0; i < classes.length; i++){
p.classList.toggle(classes[i])
}
Assuming that myElement is a valid DOM Element, this works:
['right-to-left', 'left-to-right'].forEach(function(className){
this.classList.toggle(className);
}, myElement);
This Worked for me
let superToggle = (element, class0, class1) => {
element.classList.toggle(class0);
element.classList.toggle(class1);
};
const toggleClasses = (e, classes) => {
classes.forEach((className) => {
e.classList.toggle(className)
});
}
const classes = [
'hidden',
'bg-white',
]
toggleClasses(element, classes)
The following should work; granted that these class-names are defined in your CSS and some elements on the current page have these classNames:
var toggleDirection = function()
{
var ltr, rtl, lst, cls;
ltr = 'left-to-right';
rtl = 'right-to-left';
lst = [].slice.call(document.getElementsByClassName(ltr));
lst = ((lst.length > 0) ? lst : [].slice.call(document.getElementsByClassName(rtl)));
lst.forEach
(
function(node)
{
cls = node.getAttribute('class');
if (cls.indexOf(ltr) > -1)
{ cls.split(ltr).join(rtl); }
else
{ cls.split(rtl).join(ltr); }
node.setAttribute('class', cls);
}
);
}

Categories

Resources