JavaScript - Dynamically loading a class and creating new instance with specific properties - javascript

I have a Node.js v11.11.0 app. In this app, my files are structured like the following:
./src
/animals/
animal.js
tiger.js
koala.js
index.js
As shown above, I have three classes defined in the animals directory. Over time, I intend to add additional animals with more complex logic. At this time, my classes are defined like so:
animal.js
'use strict';
class Animal {
constructor(properties) {
properties = properties || {};
this.kind = 'Unknown';
}
eat(foods) {
for (let i=0; i<foods.length; i++) {
console.log(`Eating ${foods[i]}`);
}
}
}
module.exports = Animal;
tiger.js
'use strict';
const Animal = require('./animal');
class Tiger extends Animal {
constructor(properties) {
super(properties);
this.kind = 'Tiger';
}
eat(foods) {
for (let i=0; i<foods.length; i++) {
if (foods[i].kind === 'meat') {
console.log(`Eating ${foods[i]}`);
}
}
}
}
module.exports = Tiger;
koala.js
'use strict';
const Animal = require('./animal');
class Koala extends Animal {
constructor(properties) {
super(properties);
this.kind = 'Koala';
}
eat(foods) {
for (let i=0; i<foods.length; i++) {
if (foods[i].kind === 'plant') {
console.log(`Eating ${foods[i]}`);
}
}
}
}
module.exports = Koala;
I am trying to dynamically create an instance of one of these classes based on what the user enters. For example, I'm doing this:
index.js
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
})
readline.question('What animal file do you want to load?', (fname) => {
let properties = {
name: 'My Pet'
};
let Animal = require(fname);
let pet = Object.create(Animal.prototype, properties);
});
If I run this and enter ./animals/tiger.js, I get an error. The error says: Property description must be an object. I don't understand this. Both Tiger and Koala extend Animal. At the same time, as my list will be dynamic, I need the free form option of typing a file path and dynamically loading the class. For that reason, I can't use the approach shown here, which specifically lists the Class names.
How do I dynamically load a class and create an instance of that class with specific properties?

The second argument to Object.create is the same argument you would pass to Object.defineProperties, your properties is therefore invalid. Instead use Object.assign:
Object.assign(Object.create(Animal.prototype), properties)
But why don't you just call the constructor?
new Animal(properties)
Secondly this:
properties = properties || {};
is probably intended to be:
this.properties = properties || {};
And I wouldn't recommend to dynamically require, especially not from a user entered value, instead create a lookup object:
const AnimalByName = {
Bear: require("./Bear"),
//...
};
const instance = new AnimalByName[name](properties);

Related

call a methods of a class without using NEW keyword inside other class node js

I want to access Main class methods to another Person class without creating a new instance Is it possible??
Can we access it without creating an instance of a class
let myInstance = new Person();
class Main {
constructor(args) {
this.hooks = [];
}
add_hooks(name, func) {
if (!this.hooks[name]) this.hooks[name] = [];
this.hooks[name].push(func);
}
call_hooks(name, ...params) {
if (this.hooks[name]) this.hooks[name].forEach((func) => func(...params));
}
}
other class Person how to access without using new keyword
const Main = require("./main.js");
class Person {
exec() {
const action = Main();
action.add_hook("jump", console.log.bind(console, "this will log "));
}
}
There is no big magic to it. Since the OP just wants to reuse prototypal Main methods, one is going to explicitly delegate the method/s of interest which was/were provided/accessed before via Main.prototype ...
class Main {
constructor(args) {
this.hooks = {};
}
add_hooks(name, func) {
if (!this.hooks[name]) {
this.hooks[name] = [];
}
this.hooks[name].push(func);
}
call_hooks(name, ...params) {
if (this.hooks[name]) {
this.hooks[name].forEach(func => func(...params));
}
}
}
// const Main = require("./main.js");
class Person {
// // ... either add `hooks` as public property at instantiation time ...
// hooks = {};
exec() {
const ref = Main.prototype;
ref.add_hooks.call(this, "jump", console.log.bind(console, "this will log"));
}
}
// ... or add `hooks` via additional glue code ...
function createPersonWithHooksAndExecute() {
const type = new Person();
type.hooks = {};
type.exec();
return type;
}
const someone = createPersonWithHooksAndExecute();
console.log({ someone });
// this will log
Main.prototype.call_hooks.call(someone, "jump");
.as-console-wrapper { min-height: 100%!important; top: 0; }
If you're not planning on instantiating the object, and you don't care about having multiple instances with each having their own state, you don't need a class.
Just create individual functions, or export an object.
const hooks = [];
export function add_hooks(name, func) {
if (!hooks[name]) hooks[name] = [];
hooks[name].push(func);
}
export function call_hooks(name, ...params) {
if (!hooks[name]) return;
for (const func of this.hooks[name]) {
func(...params);
}
}
It's possible too to do this with static methods, and that would be the likely answer if you write Java where everything has to be a class, but I wouldn't recommended it in Javascript.

How to return object directly with module exports?

Why cat class cant set property, whereas module exports is new object that i pass.
TypeError: Cannot set property '#sound' of undefined
animal class
class Animal
{
#sound;
// i make it return object this object class.
setSound(sound)
{
this.#sound = sound;
return this;
}
getSound()
{
return this.#sound;
}
}
// i make it create object and return that object.
module.exports = new Animal;
cat class
const {setSound} = require('./animal');
const cat = setSound('Meow');
console.log(cat.getSound());
In Javascript, this simply refers to whatever comes before the .. For example:
function t() {
return this;
}
a = {t: t}
a.t()
// returns a
If you call t directly you get undefined:
t()
// undefined
So in your case you are calling the method directly without a ., so this will be undefined.
You can fix it in two ways, one is fixing your import:
const cat = require('./animal');
cat.setSound('Meow') // This works because there is a period
Or using bind:
class Animal {
constructor() {
this.setSound = this.setSound.bind(this);
this.getSound = this.getSound.bind(this);
}
// Other methods
}
This basically "freezes" the value of this and prevents it from changing if the method is ever disconnected from the instance.
You export an instance of class so you can not just import the method from it. Do something like:
const CAT = require('./animal');
const cat = CAT.setSound('Meow');
console.log(cat.getSound());

Replace customElements.define with custom logic

Is it possible to modify customElements.define and use a custom function instead?
This is what I have tried so far, but obviously this does not work because window.customElements is read only.
const future = ["hello-world"];
let customElementsRegistry = window.customElements;
const registry = {};
registry.define = function(name, constructor, options) {
if (future.includes(name)) {
customElementsRegistry.define(name, constructor, options);
}
}
window.customElements = registry;
window.customElements.define("hello-world", HelloWorld);
Is this possible or is my only option to create my own registry and just use that one?
The property is still configurable so you can redefine it. Though this doesn't look like a great idea for production app.
const future = ["hello-world"];
let customElementsRegistry = window.customElements;
const registry = {};
registry.define = function(name, constructor, options) {
console.log('hijacked')
if (future.includes(name)) {
customElementsRegistry.define(name, constructor, options);
}
}
Object.defineProperty(window, "customElements", {
get() {
return registry
}
})
window.customElements.define("hello-world", class HelloWorld extends HTMLElement {
constructor() {
super()
this.innerHTML = "Hello World"
}
});
<hello-world>Test</hello-world>

Create class with arguments or call its methods separately

How JS code should be structered when instantiating new classes inside controller class Main.
Solutions:
A: pass arguments while creating new class - new Options(args) - and let Options's constructor call its own methods.
B: create new class and call the classes' methods on the object.
Later I'd use properties from Options in another classes.
// A
class Main {
constructor(options) {
this.options = new Options(options);
{ firstProperty, secondProperty } = this.options;
this.another = new Another(firstProperty, secondProperty);
}
}
// B
class Main {
constructor(options) {
this.options = new Options();
const firstProperty = this.options.methodA(options);
const secondProperty = this.options.methodB(options);
this.another = new Another();
const anotherPropety = this.another.methodA(firstProperty);
(...)
}
}
For the purposes of decoupling I would suggest a third option.
//main.js
class Main {
constructor(options) {
this.options = options;
// all instances of class would have:
this.options.foo = 'bar'
}
method() {
return `${this.options.foo} - ${this.options.setup}`
}
}
// example.js
const options = new Options({setup: 'options'});
const example = new Main(options);
console.log(example.method());
This lets your inject your dependencies into a given class, which makes writing tests for your code far simpler. It also gives you the benefit of (as long as you maintain a common interface) swapping out Options for NewAwesomeOptions at some later point without having to find everywhere you might have hard coded it into a class.

How to access parent methods from submodule?

I have the following module/class and submodule setup
MyAPI.js
class MyAPI {
construction(){
this.food = require('./Food');
}
}
module.exports = MyAPI;
Food.js
class Food {
constructor(){
...
}
}
module.exports = Food;
app.js
var api = require('./MyAPI');
var taco = new api.food;
var cheeseburger = new api.food;
What I'm wondering, is it possible to call upon MyAPI properties and functions form within Food.js? Do I need to pass this into the require somehow?
this.food = require('./Food')(this); // this didn't work...
The above resulted in this:
TypeError: Class constructors cannot be invoked without 'new'
But why would I use new in the MyAPI constructor?
What is the best approach here to do subclasses and submodules and creating new objects from them?
I think you are confusing classes and instances:
var MyAPI = require('./MyAPI');//this is a class
var apiInstance = new MyAPI();//the creates a new instance of your class
var taco = new apiInstance.food //the food property on your api is a class not an instance
var tacoInstance = new taco();
this.food is assigned in the constructor of MyApi, so you will need to instantiate MyApi to have the property accessible.
var Api = require('./MyAPI');
var apiInstance = new Api();
var foodInstance = new apiInstance.food();
From your comment, it seems like you want properties of MyApi, particularly config to be accessible by submodules. I don't see a way of doing this except to make your top level API object a singleton:
var MyAPI = {
config: { setting: 'default' },
Food: require('./Food')
}
module.exports = MyAPI;
var MyApi = require('./my-api.js');
class Food {
constructor(){
// MyApi.config
}
}
module.exports = Food;
Looking at the AWS source they are doing something similar (except config is it's own module mounted on the top level AWS object).

Categories