For pure educational and curiosity purposes, I am trying to create an element-wrapper object that allows me to tack-on my own properties and methods to an element. The behavior I'm trying to simulate is basically this:
// get a button element to wrap
const button = document.querySelector('button');
// some function that wraps new properties/methods around a given element
function wrap(element) {
this.customName = 'John';
this.customAge = 100;
this.printName = function() {
console.log(this.customName);
}
// ...
// ...somehow inherit element fields...
// ...
}
// wrap the button element
const customElement = new wrap(button);
// custom behavior:
console.log(customElement.customAge) // output => 100
customElement.printName() // output => 'John'
// legacy behavior
console.log(customElement.clientHeight) // output => client height
customElement.remove() // => should still call 'remove' on the element
So, here I should be able to add my own methods/properties but still access the original fields normally. Is this even possible?
I'm using a constructor function here as an example just to demonstrate the intended behavior, but I don't actually know if this would be relevant for the solution. I'm new to Javascript and I have done a ton of research on prototypes and classes, but I'm still confused on what approach I would take here.
Edit: As Brad pointed out in the comments, I also tried this implementation using classes:
class MyButton extends HTMLButtonElement {
constructor() {
super();
this.customName = 'John';
this.customAge = 100;
}
printName() {
console.log(this.customName);
}
}
const myBtn = new MyButton();
But this resulted in the error:
Uncaught TypeError: Illegal constructor
I haven't test this, but maybe something like this:
// get a button element to wrap
const button = document.querySelector('button');
// some function that wraps new properties/methods around a given element
function wrap(element) {
Object.defineProperties(element, {
customName: {value:"John"},
customAge: {value:100},
printName:{value: () => console.log(element.customName)}
})
return element
}
// wrap the button element
const customElement = wrap(button);
// custom behavior:
console.log(customElement.customAge) // output => 100
customElement.printName() // output => 'John'
// legacy behavior
console.log(customElement.clientHeight) // output => client height
customElement.remove() // => should still call 'remove' on the element
<button>Hello world!</button>
Another method could be used is wrap element into proxy()
This will allow return custom data if property doesn't exist and send notifications when properties changed:
const customElement = function (element, properties = {})
{
this.element = element;
this.customName = 'John';
this.customAge = 100;
this.printName = function() {
console.log(this.customName);
}
//override default properties
for(let i in properties)
{
if (i in element)
element[i] = properties[i];
else
this[i] = properties[i];
}
return new Proxy(this, {
get(target, prop)
{
if (prop in target.element) //is property exists in element?
{
if (target.element[prop] instanceof Function)
return target.element[prop].bind(target.element);
return target.element[prop];
}
else if (prop in target) //is property exists in our object?
return target[prop];
else
return "unknown property"; //unknown property
},
set(target, prop, value, thisProxy)
{
const oldValue = thisProxy[prop];
if (prop in target.element)
target.element[prop] = value;
else
target[prop] = value;
// send notification
target.element.dispatchEvent(new CustomEvent("propertyChanged", {
detail: {
prop,
oldValue,
value
}
}));
}
});
}
const button = new customElement(document.createElement("button"), {customName: "Not John"});
button.addEventListener("propertyChanged", e =>
{
console.log("property changed", e.detail);
});
button.printName();
console.log("age:", button.customAge);
console.log("height:", button.clientHeight);
console.log("blah:", button.blah);
button.blah = "ok";
console.log("blah:", button.blah);
Related
I'm using an entity-component system. I've defined some components as ES6 classes, and I can create instances of those components by calling their constructors with new. I'm trying to use these class instances as mixins. I've found that using Object.assign doesn't copy the methods of those instances onto my target object because the methods are bound to the object's prototype.
I've found a hacky solution as follows:
function getAllPropertyNames(obj) {
return Object
.getOwnPropertyNames(obj)
.concat(Object.getOwnPropertyNames(obj.__proto__))
.filter(name => (name !== "constructor"))
}
function assembleFromComponents(obj, ...cmp) {
for(let i of cmp) {
for(let j of getAllPropertyNames(i)) {
obj[j] = i[j]
}
}
}
This method is not ideal because it doesn't access the component's full prototype chain, although I don't think I'll need it anyway. However, upon inspection, getters and setters don't appear to work.
Is there a better way to use a class instance as a mixin?
I probably wouldn't define mixins using class syntax. I'd define them as objects:
const myMixin = {
doThis() {
// ...
},
doThat() {
// ...
},
// ...
};
One issue with class syntax is that super may not work as you expect, because even after being copied, the methods will still refer to their original home object. (Or that may be what you expect in which case you're fine.) More on this below.
But if you want to use class syntax, you can define an Object.assign-like function that applies all of the methods and other properties from the entire chain via Object.defineProperties and Object.getOwnPropertyDescriptors, which will copy getters and setters. Something like (off the cuff, not tested):
function assignAll(target, source, inherited = false) {
// Start from the prototype and work upward, so that overrides work
let chain;
if (inherited) {
// Find the first prototype after `Object.prototype`
chain = [];
let p = source;
do {
chain.unshift(p);
p = Object.getPrototypeOf(p);
} while (p && p !== Object.prototype);
} else {
chain = [source];
}
for (const obj of chain) {
// Get the descriptors from this object
const descriptors = Object.getOwnPropertyDescriptors(obj);
// We don't want to copy the constructor or __proto__ properties
delete descriptors.constructor;
delete descriptors.__proto__;
// Apply them to the target
Object.defineProperties(target, descriptors);
}
return target;
}
Using it:
assignAll(Example.prototype, Mixin.prototype);
Live Example:
function assignAll(target, source, inherited = false) {
// Start from the prototype and work upward, so that overrides work
let chain;
if (inherited) {
// Find the first prototype after `Object.prototype`
chain = [];
let p = source;
do {
chain.unshift(p);
p = Object.getPrototypeOf(p);
} while (p && p !== Object.prototype);
} else {
chain = [source];
}
for (const obj of chain) {
// Get the descriptors from this object
const descriptors = Object.getOwnPropertyDescriptors(obj);
// We don't want to copy the constructor or __proto__ properties
delete descriptors.constructor;
delete descriptors.__proto__;
// Apply them to the target
Object.defineProperties(target, descriptors);
}
return target;
}
class Example {
method() {
console.log("this is method");
}
}
const mixinFoos = new WeakMap();
class Mixin {
mixinMethod() {
console.log("mixin method");
}
get foo() {
let value = mixinFoos.get(this);
if (value !== undefined) {
value = String(value).toUpperCase();
}
return value;
}
set foo(value) {
return mixinFoos.set(this, value);
}
}
assignAll(Example.prototype, Mixin.prototype, true);
const e = new Example();
e.foo = "hi";
console.log(e.foo);
// HI
Here's an example where the mixin is a subclass and uses super, just to demonstrate what super means in that context:
function assignAll(target, source, inherited = false) {
// Start from the prototype and work upward, so that overrides work
let chain;
if (inherited) {
// Find the first prototype after `Object.prototype`
chain = [];
let p = source;
do {
chain.unshift(p);
p = Object.getPrototypeOf(p);
} while (p && p !== Object.prototype);
} else {
chain = [source];
}
for (const obj of chain) {
// Get the descriptors from this object
const descriptors = Object.getOwnPropertyDescriptors(obj);
// We don't want to copy the constructor or __proto__ properties
delete descriptors.constructor;
delete descriptors.__proto__;
// Apply them to the target
Object.defineProperties(target, descriptors);
}
return target;
}
class Example {
method() {
console.log("this is Example.method");
}
}
class MixinBase {
method() {
console.log("this is MixinBase.method");
}
}
class Mixin extends MixinBase {
method() {
super.method();
console.log("this is Mixin.method");
}
}
assignAll(Example.prototype, Mixin.prototype, true);
const e = new Example();
e.method();
// "this is MixinBase.method"
// "this is Mixin.method"
You've said you want to use class instances as mixins. The above works just fine doing that. Here's an example:
function assignAll(target, source, inherited = false) {
// Start from the prototype and work upward, so that overrides work
let chain;
if (inherited) {
// Find the first prototype after `Object.prototype`
chain = [];
let p = source;
do {
chain.unshift(p);
p = Object.getPrototypeOf(p);
} while (p && p !== Object.prototype);
} else {
chain = [source];
}
for (const obj of chain) {
// Get the descriptors from this object
const descriptors = Object.getOwnPropertyDescriptors(obj);
// We don't want to copy the constructor or __proto__ properties
delete descriptors.constructor;
delete descriptors.__proto__;
// Apply them to the target
Object.defineProperties(target, descriptors);
}
return target;
}
class Example {
method() {
console.log("this is Example.method");
}
}
class MixinBase {
method() {
console.log("this is MixinBase.method");
}
}
const mixinFoos = new WeakMap();
class Mixin extends MixinBase {
constructor(value) {
super();
this.value = value;
}
mixinMethod() {
console.log(`mixin method, value = ${this.value}`);
}
get foo() {
let value = mixinFoos.get(this);
if (value !== undefined) {
value = String(value).toUpperCase();
}
return value;
}
set foo(value) {
return mixinFoos.set(this, value);
}
method() {
super.method();
console.log("this is Mixin.method");
}
}
// Here I'm using it on `Example.prototype`, but it could be on an
// `Example` instance as well
assignAll(Example.prototype, new Mixin(42), true);
const e = new Example();
e.mixinMethod();
// "mixin method, value = 42"
e.method();
// "this is MixinBase.method"
// "this is Mixin.method"
e.foo = "hi";
console.log(e.foo);
// "HI"
But really, you can design it however you want to; assignAll is just an example, as are the runnable ones above. The key things here are:
Use Object.getOwnPropertyDescriptors to get property descriptors and Object.defineProperties (or their singular counterparts, getOwnPropertyDescriptor and defineProperty), so that accessor methods get transferred as accessors.
Work from the base prototype up to the instance, so that overriding at each level works correctly.
super will continue to work in its original inheritance chain, not in the new location the mixin has been copied to.
I wasn't sure how to word the title so I will go into more detail.
What I want to do is have some object called car.
The object car contains two objects called tire and engine.
What I want to happen is be able to say,
car.start();
Then I want the car object to be able to check both tire and engine to see if it contains a function with that name then call it.
So in summary
I want to be able to call an object and have that object called pass it onto whoever implemented that function call.
I looked at the proxy pattern but I don't see how I can dynamically have function calls passed on from an object to a nested object.
Any ideas would be appreciated. Thanks!
Example Code
function engine() {
return {
start: () => console.log('start')
}
}
function tire() {
return {
getWidth: () => console.log('get width')
}
}
function car() {
this.tire = new tire();
this.engine = new engine();
// Pass on function calls made to car over to tire or engine depending on who implemented that function call.
}
// This should print start.
car.start();
**PS. ** I know I can hard code the function calls and have it pass through but I am trying to do this dynamically so I don't have to declare every single function that can be called. So I want to do this dynamically.
Convert them to actual classes then copy properties from their prototypes:
class Engine {
start() {
console.log("start");
}
}
class Tire {
getWidth() {
console.log("get width");
}
}
class Car {
constructor() {
this.tire = new Tire();
this.engine = new Engine();
}
}
for (const key of Object.getOwnPropertyNames(Engine.prototype)) {
if (key !== "constructor") {
Car.prototype[key] = function(...args) { return this.engine[key](...args); };
}
}
for (const key of Object.getOwnPropertyNames(Tire.prototype)) {
if (key !== "constructor") {
Car.prototype[key] = function(...args) { return this.tire[key](...args); };
}
}
const car = new Car();
car.start();
car.getWidth();
Addressing this: "I looked at the proxy pattern but I don't see how I can dynamically have function calls passed on from an object to a nested object."
It is possible you missed checking for the 'undefined' method of the proxied class. In the code below note target[prop] === undefined - this check determines if the dynamic method exists on the target. If not, then look at the prop and call the method from the embedded object.
If you don't want to enumerate the methods from the embedded object, you can introspect them to find the object containing the method (not shown here).
This should demonstrate that a Proxy object can, indeed be used to accomplish your goal.
function Engine() {
this.start = () => {
console.log('started');
}
}
function Tire() {
this.getWidth = () => {
console.log('got width');
}
}
function Car() {
this.tire = new Tire();
this.engine = new Engine();
}
const carProxy = {
get: function(target, prop, receiver) {
if (target[prop] === undefined) {
if (prop === "getWidth") {
console.log('getting tire width');
return target.tire.getWidth;
} else if (prop === "start") {
console.log('starting');
return target.engine.start;
} else {
return () => {
console.log('Undefined function');
}
}
}
}
};
const target = new Car()
const car = new Proxy(target, carProxy);
car.start();
car.getWidth();
I'm trying to create a (class) instance that copies the behavior of another class, but using itself as the state context (this). Normal functions work fine (like the setValue function in the example below), but getters do not work. Here is a simple example:
const should = require('chai').should();
class One {
setValue(val) {
this._val = val;
}
get val() {
return this._val;
}
}
class Two {
constructor(one) {
this.one = one;
}
setValue(val) {
this.one.setValue.call(this, val);
}
get val() {
this.one.val.call(this);
}
}
let one = new One();
let two = new Two(one);
one.setValue(1);
two.setValue(2);
one.val.should.equal(1);
two.val.should.equal(2);
The above code explodes on the last line with the error:
TypeError: this.one.val.call is not a function
at Two.get val [as val] (C:\all\code\node-tests\invoke_getter.js:23:22)
at Object.<anonymous> (C:\all\code\node-tests\invoke_getter.js:32:5)
How can I make something like this work?
Recall that property getters for class ends in its prototype
So to access the method, you may want to get it from its prototype:
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this.one), 'val')
Then you can call the getter on your two instance:
class One {
setValue(val) {
this._val = val
}
get val() {
return this._val
}
}
class Two {
constructor(one) {
this.one = one
}
setValue(val) {
this.one.setValue.call(this, val)
}
get val () {
const desc = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this.one), 'val')
return desc.get.call(this)
}
}
const one = new One()
const two = new Two(one)
one.setValue(1)
console.log(two.val) // undefined (since _val does not exist on two yet)
two.setValue(2)
console.log(two.val) // 2
two._val = 3
console.log(two.val) // 3
I'm not Noob in JS and I know that technically there is no right way to inherit from multiple classes. so my question is very straightforward
Is there any idea how can I create a class or just an object that acts as it inherits from two builds in JS Native object. especially EventTarget and another object.
I try :
var map = new Map();
var eventtarget = new EventTarget();
mix = Object.create({...Map.prototype, ...EventTarget.prototype});
Object.assign(mix, et , map);
It seems not working because the methods inMap.prototype are not itrables
also using Object.assign({}, Map.prototype, ...EventTarget.prototype) as the same effect.
Another try:
class Base5 extends Map{
constructor(){
super();
var eventTarget = new EventTarget();
Object.assign(this,eventTarget);
}
}
Base5.prototype = Object.create(Base5.prototype)
Object.assign(Base5.prototype,EventTarget.prototype);
// that seem to work
const b5 = new Base5();
b5.set('foo','bar');
// but...
b4.addEventListener('fire', _=>_ )
// throw Uncaught TypeError: Illegal invocation at <anonymous>:1:4
This one worked but it not generic
const wm = new WeakMap();
class Base6 extends Map{
constructor(){
super();
wm.set(this, new EventTarget() )
}
addEventListener(){
wm.get(this).addEventListener(...arguments)
}
dispatchEvent(){
wm.get(this).dispatchEvent(...arguments)
}
removeEventListener(){
wm.get(this).removeEventListener(...arguments)
}
}
const b6 = new Base6();
b6.set('foo','bar'); // Map(1) {"foo" => "bar"}
b6.addEventListener('foo', e=>console.log(e) );
b6.dispatchEvent( new Event('foo') )
So anyone can come with a better approach?
Maybe Reflect.construct can help here somehow
You could create a function, that creates private instances of the base classes, and returns a proxy that dispatches property retrievals to one of those objects. The base classes could be passed to the constructor to keep it generic:
createMix(Map, EventTarget)
Several things will remain problematic. One blocking issue is that method calls often need this to be set to the base object for them to work. A work around may be to return a bound method, knowing that this in itself can have undesirable effects (e.g. the customer could not take the method and bind it themselves to something else -- if that made sense at all).
For sure this does not solve all potential issues, but it seems to work in very basic usage:
function createMix(...classes) {
const obj = {};
const instances = [obj, ...classes.map(cls => new cls)];
return new Proxy(obj, {
get(obj, prop) {
obj = instances.find(obj => prop in obj);
const val = Object(obj)[prop];
return typeof val === "function" ? val.bind(obj) : val;
},
has(obj, prop) { // Optional: if you care about the `in` operator
return instances.some(obj => prop in obj);
}
});
}
// Tiny test
const obj = createMix(Map, EventTarget);
obj.set('foo','bar');
console.log("Map contains: ", Object.fromEntries(obj));
obj.addEventListener('foo', e => console.log("Event object type: ", e.type) );
obj.dispatchEvent( new Event('foo') );
As this function returns a container object, it will not be instanceof any of the base classes passed to the function.
For now, and for other that seek solution to that problem I come with this soulution
const wm = new WeakMap();
function Emitter(Base) {
return class extends Base {
constructor() {
super(...arguments);
wm.set(this, new EventTarget())
}
addEventListener() {
wm.get(this).addEventListener(...arguments)
}
dispatchEvent() {
wm.get(this).dispatchEvent(...arguments)
}
removeEventListener() {
wm.get(this).removeEventListener(...arguments)
}
}
}
// how to use
const EmitterableMap = Emitter(Map);
#trincot inspired me to research a little bit more and come up with this solution, which improves some aspects of his solution:
Weak bind for inherited function.
Ability to use the Mixed Class as a prototype to extend another custom class.
If you like my answer, don't forget to thumb up his answer too.
Anyway, here is a factory function that extends multiple native classes. This is a start and it probably isn't perfect, so feel free to participate in the improvement of this snippet:
function CreateMixedClasess (...classes) {
return function Mixed () {
const instances = [this, ...classes.map(cls => new cls(...arguments))]
const weakbind = (proxy, fn, obj) => new Proxy(fn, {
apply (fn, thisArg, props) {
return fn.apply(proxy === thisArg ? obj : thisArg, props)
}
})
return new Proxy(this, {
get (_, prop, proxy) {
obj = instances.find(obj => prop in obj)
const val = Object(obj)[prop]
return typeof val === 'function' ? weakbind(proxy, val, obj) : val
},
has (_, prop) { // Optional: if you care about the `in` operator
return instances.some(obj => prop in obj)
}
})
}
}
// class Mixed extends CreateMixedClasess (Map, EventTarget) {}
const Mixed = CreateMixedClasess(Map, EventTarget)
const obj12 = new Mixed()
obj12.set('foo', 'bar')
console.log('Map contains: ', Object.fromEntries(obj12))
obj12.addEventListener('foo', e => console.log('Event object type: ', e.type))
obj12.dispatchEvent(new Event('foo'))
obj12.a = 3
obj12.myfn = function () { console.log(this.a) }
obj12.myfn() // 3
obj12.myfn.call({a: 4}) // 4
class MyAwsomeClass extends CreateMixedClasess (Map, EventTarget) {
constructor () {
super(...arguments)
this.a = 33
}
logA () {
console.log(this.a)
}
}
const myAwsomeInstance = new MyAwsomeClass(obj12)
myAwsomeInstance.has('foo') // true
I really never used a javascript function type or class before, I understand Java and Python, but not javascript. So, I build a class like this:
function FormStore (type) {
this.setup = () =>{
this.store = {};
this.ERR_LINE_PREFIX = '#err_';
this.NO_DISPLAY_CLASS = 'no-display';
this.settings = {
'myID':{'hide':false},
}
}
this.checkVal= () => {
var geoArr = ['id_xx','myID', (...)];
var id;
$.each( geoArr, function(val) {
id = geoArr[val];
console.log(this.store) //-> returns undefined, below line is error
if (!(this.store[id])) {
return false;
}
});
};
var FS = new FormStore();
FS.setup();
The store is filled by components on document.ready. There is a function that looks up if the aligned components (glyph, label, input) have some classes or values and for the specific component fills a dict: {label:false,glyph:false, input:false}. However, for some reason it doesn't matter. Even if I enter some values in to the store right away (in setup) or create them on the fly, in checkVal the store doesn't exist, it's undefined.
Please, anybody, what am I not understanding about javascript type and classes here? I am googling this a lot and trying to find good resources but, "javascipt variable class" (or type) just yields a lot of DOM manipulation.
edit
There is a context problem in checkVal, you are using a non-arrow (and not explicitly bound) callback function and trying to access this inside of it. Change that to an arrow function as well, and the parent context (this) will be preserved:
$.each( geoArr, (val) => {
id = geoArr[val];
console.log(this.store)
if (!(this.store[id])) {
return false;
}
});
And while you are at changing that section, it's not going to work. You will not get access to $.each's return value. You should rely on native array APIs for this task and use Array.every to determine if all geoArr items are in the store (assuming that's your goal):
// returns false if not all geoArr items are in the store
geoArr.every(id => this.store[id])
original
I don't see you calling checkVal() anywhere, but based on the error you are getting it is called prior to setup() (since setup initializes the store). You could solve that problem straight away by moving this.store = {} out of setup (right at the top), e.g.:
function FormStore(type) {
this.store = {};
...
Having said that, I would suggest either defining your methods on the prototype, or utilizing ES6 classes. Here is a simplified version of both:
ES5 class
function FormStore(type) {
// make sure user didn't forget new keyword
if (this === window) {
throw new Error('FormStore must be called with "new" keyword')
}
// initialize state, this is the constructor
this.type = type;
this.store = {};
// any other state the class manages
}
FormStore.prototype = {
setup: function() {
// do setup stuff
// "this" points to instance
console.log('setup', this.type)
},
checkVal: function() {
}
}
var formStore = new FormStore('foo')
console.log(formStore.store) // <-- not undefined
formStore.setup()
ES6 Class
class FormStore {
constructor(type) {
this.type = type;
this.store = {};
}
setup() {
console.log('setup', this.type)
}
checkVal() {
}
}
const formStore = new FormStore('bar')
console.log(formStore.store) // <-- not undefined
formStore.setup()
It has to do with scoping. Your $.each in checkVal has a normal function. Inside the function the scope if this is different. If you want to keep the original scope you could use a fat arrow function like you do when defining the methods.
this.checkVal= () => {
var geoArr = ['id_xx','myID', (...)];
var id;
$.each( geoArr, val => {
id = geoArr[val];
console.log(this.store) //-> returns undefined, below line is error
if (!(this.store[id])) {
return false;
}
});
}
When you run your original code and place a breakpoint on the line with console.log you can see in the inspector that this is set to the Window object and no longer points to your FormStore.
function FormStore () {
this.setup = function(){
this.store = {};
this.ERR_LINE_PREFIX = '#err_';
this.NO_DISPLAY_CLASS = 'no-display';
this.settings = {
'myID':{'hide':false},
}
}
this.checkVal= function(){
var geoArr = ['id_xx','myID'];
var id;
$.each( geoArr, function(val) {
id = geoArr[val];
console.log(this.store) //-> returns undefined, below line is error
if (!(this.store[id])) {
return false;
}
});
}
};
var FS = new FormStore();
FS.setup();
Works absolutely fine, the code you provided had a missing bracket and you were using some broken es6 syntax