Confusion on how to work with module pattern - javascript

I am confused on how to work with module pattern (and design patterns in general) in JavaScript.
I already wrote some functioning code in my application using module pattern that does what I want to, but it doesn't seem to be very modular to me, and I keep having this feeling that I am doing it wrong. I didn't manage to find any concrete and complete application example with any design pattern.
Here is how I work with it :
Let's say I have forms in my application that I'll use for different modules (post a thread, reply to a thread, comment the guests book), with some JavaScript I'll give users some functionalities, as such as popping a smiley bubble and handling insertion of them in my forms, sending data posts to my server code to return the HTML code in order to add the message without reloading the page, I'll do something like that:
let Form = function (selector_form, selector_textarea, selector_emoticonsButton, selector_postButton) {
let form, textarea, emoticonsButton, postButton;
let emoticonsBubble = new EmoticonsBubble()
return {
selectors: function () {
return {
form: function () { return selector_form },
sendButton: function () { return selector_sendButton }
}
}
setElements: function (obj) {
form = $(obj).get(0);
textarea = $(form).find(selector_textarea).get(0);
emoticonsButton = $(form).find(emoticonsButton).get(0);
postButton = $(form).find(selector_postButton).get(0);
emoticonsBubble.setElements(form, emoticonsButton);
},
get: function () {
return {
form: function () { return form },
//...
emoticonsBubble: function () { return emoticonsBubble }
}
},
post: function (moduleId, callback) {
$.ajax({
//parameters
}).done(function (data) {
callback(data);
});
}
}
}
let EmoticonsBubble = function () {
let thisContainerToAppendTo, thisTextarea;
return {
setElements: function (container, textarea) {
thisContainerToAppendTo = container;
thisTextarea = textarea;
},
pop: function () {
this.ajax().pop(function (data) {
$(thisContainerToAppendTo).append(data);
});
}
insert: function (emoticon) {
$(thisTextarea).append(emoticon);
},
ajax: function () {
return {
pop: function (callback) {
$.ajax({
//parameters
}).done(function (data) {
callback(data);
});
}
}
}
}
}
// Events part
let form = new Form('#threadForm', '.textarea', 'button[name="emoticons"]', 'button[name="send"]');
let emoticonsBubble = form.get().emoticonsBubble();
$(form.selectors().form()).on('click', function (e) {
form.setElements(this);
});
$(form.selectors().sendButton()).on('click', function (e) {
let moduleId = // retrieve module id, if it belongs to guests book, thread creation module or reply module
form.post(moduleId, function (data) {
// append data to something
});
});
// etc for emoticons handling
The fact that I have to rewrite the event part for every different form I have in my application while keeping everything the same but variables name, annoys me a lot.
Could you guys tell me how you would handle those functionalities and what may be wrong with my way of coding?

The Module Pattern is about keeping units of code from colliding with other scopes (usually the Global scope).
As we know, in JavaScript, variables defined with:
let and const are scoped to their parent block
var are scoped to their containing function (or Global if not in a
function)
So, if you were to take your Form function:
let Form = function (x,y,z) {
let form, textarea, emoticonsButton, postButton;
let emoticonsBubble = new EmoticonsBubble()
return {
. . .
}
setElements: function (obj) {
. . .
},
get: function () {
. . .
},
post: function (moduleId, callback) {
. . .
}
}
}
The variable Form is Global because there is no containing block. This is a problem because what if there is already another Global called Form (which there very well could be because of the generic nature of the word "Form"). So, this code doesn't cut off your code from being exposed. To use the Module Pattern on it, we'd wrap it with an IIFE (Immediately Invoked Function Expression) and within that IIFE, we'd create a custom namespace in the Global scope that we're sure doesn't exist (thereby avoiding name collisions):
(function(){
// This is going to be exposed as publicly available via the module namespace
function Form(x,y,z) {
. . .
}
// This will remain private within the module
function helper(){
}
// **********************************************************************
let temp = {}; // Create a temporary object to bind only the public API
temp.Form = Form; // Bind the public members to the object
// Expose the module to the Global scope by creating a custom namespace
// and mapping the temp object to it
window.myCustomAPI = temp;
})();
// Now, outside of the module (in some higher scope), your public portions
// of the Module are accessible:
let myForm = new myCustomAPI.Form(arg, arg, arg);

The repetition in your code basically comes from the selection of elements and their helpers, and that can easily be abstracted into a function:
function Elements(selectors, children, options) {
let elements = { ...children };
return {
selectors,
elements,
setElements(obj) {
for(const [name, selector] of Object.entries(selectors))
elements[name] = $(obj).find(selector).get(0);
for(const child of Object.values(child))
child.parent && child.parent(this, obj);
},
...options
}
}
That can then be used as:
function Form(form, textarea, emoticonsButton, postButton) {
const emoticonsBubble = EmoticonsBubble();
return Elements({ form, textarea, emoticonButtons }, { emoticonsBubble }, {
post() {
//...
}
});
}
function EmoticonsBubble() {
return Elements({ /*...*/ }, {}, {
parent(parent, obj) {
this.setElements(parent);
}
});
}
But you are basically reinventing a lot of wheels here, have you thought about using one of the MVCs that are out there (React, Vue, ...) ?

Ok the boilerplate for some common tasks that you have in the event part is driving you crazy right ?
So checking your code you can fix them in many ways.
A. Encapsulate your code in real modules I mean this.
const Form = (function(/*receive here dependencies as arguments */){
// your code module goes here
})(/*inject dependencies here to module*/);
B. You can create a event pattern module, to drive your internal and externals events for module.
C. You know what are the listener that the module needs , so apply them into your module.
That way should be more reusable than now

Related

Get function arguments from outside without executing this function in javascript

Is it possible to receive the arguments list of the function without executing it somehow? I can't find a normal solution unfortunately.
I'm working on some Dependency Injection system and I'd like to know arguments naming in the function (service) before I create the instance of it. Because I want to create the Dependencies first and then pass them into the function (service).
for ex:
function MyServiceA(myServiceB) {
say() {
...code.........
myServiceB.beep();
}
}
function MyServiceB() {
beep() {
...code.........
}
}
function Creator() {
// Is it possible to receive it somehow? this is the question
const argumentsList = MyServiceA
// iterate list and instantiate the ServiceB dependency and inject in into the MyServiceA
argumentsList.forEach(dependency => {
const dependencyInstance = new dependency()
new MyServiceA(dependencyInstance )
})
}
Thanks for any help!
You could turn the function into a string, and then parse that string to find the names of the arguments. AngularJs did something like this for its dependency injection. Be aware that if the code gets minified, the names will change and this can break your code.
function needsA(a) {
console.log("in needsA");
a();
}
function needsB(b) {
console.log("in needsB");
b();
}
function needsAandB(a, b) {
console.log("in needsAandB");
a();
b();
}
const dependencyLookup = {
a: () => console.log('in A'),
b: () => console.log('in B'),
}
function injector(fxn) {
const argNames = fxn.toString()
.match(/^function\s*[^\(]*\(([^\)]*)\)/)[1]
.split(',');
const dependencies = argNames
.map(str => dependencyLookup[str.trim()]);
fxn.apply(null, dependencies);
}
injector(needsA);
injector(needsB);
injector(needsAandB);
A useful article on angularjs's dependency injection: https://teropa.info/blog/2014/06/04/angularjs-dependency-injection-from-the-inside-out.html

How can I expose a public api from this specific IIFE JS structure?

In AspNetZero/ABP, the default index.js for an entity looks like the following:
(function () {
$(function () {
var _$formulationFrequenciesTable = $('#FormulationFrequenciesTable');
var _formulationFrequenciesService = abp.services.app.formulationFrequencies;
function additionalFilter() {
return {
nameFilter: // something
prop2: // something else
};
}
// more code
});
})();
We are using Telerik's AspNetCore Kendo on the Razor, so we define the grid like so:
Html.Kendo().Grid<PatientManagement.Formulations.Dtos.GetFormulationFrequencyForViewDto>()
.Name("gridFormulationFrequencies")
.DataSource(d =>
{
d.Custom()
.Type("aspnetmvc-ajax")
.ServerFiltering(true)
.ServerPaging(true)
.ServerSorting(true)
.Schema(s => s.Aggregates("aggregateResults").Data("data").Errors("errors").Total("total").Model(model => model.Id(m => m.Id)))
.Transport(t => t.Read(read => read.Action("FormulationFrequency_Read", "FormulationFrequencies", new { area = "App" }).Data("additionalData").Type(HttpVerbs.Get)));
})
.Deferred(true).Render();
How can I make additionalData "public" so that I can use it in the grid definition? I understand that the first line of the js is IIFE, and the second is short hand js for jQuery(document).ready().
I've been having issues trying to define a public API because everything is defined within the scope of the document ready, and IIFE examples I've seen don't include this curveball.
One way would be to assign a reference to the function to a window property.
If you have to do this often you could have a global object of your own in window namespace and assign as a property to that object instead
(function () {
$(function () {
var _$formulationFrequenciesTable = $('#FormulationFrequenciesTable');
var _formulationFrequenciesService = abp.services.app.formulationFrequencies;
function additionalFilter() {
return {
nameFilter: // something
prop2: // something else
};
}
// add to global window namespace
window.additionalFilter = additionalFilter;
// more code
});
})();

webpack plugin to replace a function with another

I am trying to create a webpack plugin, that will parse the code for a certain function and replace it with another function, that plugin will also expose the new function as a global.
class someName {
constructor(local, domain, translationFile, options) {
}
apply(compiler) {
// exposing ngt function as a global
compiler.plugin('make', function(compilation, callback) {
var childCompiler = compilation.createChildCompiler('someNameExpose');
childCompiler.apply(new webpack.DefinePlugin({
ngt: function(singular , plural, quantity) {
return quantity == 1 ? singular : plural;
}
}));
childCompiler.runAsChild(callback);
});
// searching for the getValue function
compiler.parser.plugin(`call getValue`, function someNameHandler(expr) {
// create a function to replace getValue with
let results = 'ngt('+ expr.arguments +')';
const dep = new ConstDependency(results, expr.range);
dep.loc = expr.loc;
this.state.current.addDependency(dep);
return true;
});
}
}
module.exports = someName;
update / rephrase
I have an issue here, when compiler.parser.plugin('call getValue', function someNameHandler(expr) {...} block is commented the ngt function exist as a global.
when its not commented, i get an error, ngt is undefined.
commented i mean /**/
I found a workaround for that but its far then idea. right now what I do is I export an anonymous function that does what i want.
You can see the plugin here:
Github
You can override the method based on environment. Let's say you have a method
function a(){
//original defination
}
Now based on the environment, if it's a production you could do something like this
if (environment.production) {
function a(){
//overridden defination
}
}
You can use the webpack-merge plugin, it's very useful to do exactly what do you want.
https://github.com/survivejs/webpack-merge

Modifying callback parameters in JavaScript

I'm creating a service in angular to track events, and I want the controllers to be as dumb as possible about the event tracking. For this service each controller will need to supply its own copy of the event function. My current solution is below.
//tracking module
let tracked_events = ['Event1','Event2']
.map(function(e){
return {
eventName:e,
eventProperties: {}
}
});
let finishingTracking = (event) => {
console.log("prentend this does something fancy...",event);
}
let track = (eventName,fn) => {
var event = tracked_events.filter(e => e.eventName===eventName)[0];
fn(event.eventProperties);
//some other internal tracking function
finishTracking(event);
}
//end tracking module
//called from a controller
track("Event 1",(event) => {
event["User ID"] = 123;
event["Company ID"] = 12
});
//some other controller
track("Event 2",(event) => {
event["Manager ID"] = 345;
event["Time Sent"] = Date.now()
}
This works well because each controller will only have to provide its own way to modify the event object and it won't know anything else about the tracking module. From a design perspective, is this ok? I'm not sure about modifying the callback parameters (V8 optimizations/side effects), but I can't think of another way that doesn't cause more changes to each controller that needs to do its own event tracking. Any suggestions on this?
EDIT (prior to refactor)
var eventProperties = { table: "Machines, has_rows: /*rows...*/ }
some_service.list(data)
.then(function (response) {
tracking_service.track({
eventName: 'Event 1',
eventProperties: eventProperties
});
AFTER
some_service.trackEvent(some_service.events.EVENT1, function (event) {
event["Table"] = "Machines";
event["Has Rows"] = response.data.machines.length > 0;
});
An object of shape {eventName: "Name", eventProperties:{} } is currently being defined in each controller. My solution was to pass in the eventName from a constant defined in the service and the callback function modifies the eventProperties of the object. Each controller will have a different set of properties in eventProperties.
After the callback in version two runs, the service does the actual "tracking". My aim was to have a function that prepared the eventProperties object and added whatever properties it needed before being actually tracked.
self.trackEvent = function (eventName, fn) {
//var event = TRACKED_EVENTS.filter(e => e.eventName === eventName)[0];
var event = {
eventName: self.events[eventName],
eventProperties: { }
}
fn(event.eventProperties); //this is the callback that does the prep
self.track(event); //does the actually tracking
}

Modifying some features in a public npm package

I am trying to overload/replace functions in the ami-io npm package. That package is created to talk to asterisk AMI, a socket interface.
I need to talk to a service that has almost the exact same interface, but it presents a different greeting string upon login, and it requires an extra field in the logon. All the rest is the same. Instead of just plain copying the 600 LOC ami-io package, and modifying two or three lines, I want to override the function that detects the greeting string, and the login function, and keep using the ami-io package.
Inside the ami-io package there is a file index.js which contains the following function:
Client.prototype.auth = function (data) {
this.logger.debug('First message:', data);
if (data.match(/Asterisk Call Manager/)) {
this._setVersion(data);
this.socket.on('data', function (data) {
this.splitMessages(data);
}.bind(this));
this.send(new Action.Login(this.config.login, this.config.password), function (error, response) {
if (response && response.response === 'Success') this.emit('connected');
else this.emit('incorrectLogin');
}.bind(this));
} else {
this.emit('incorrectServer', data);
}
};
Now I want to match not on Asterisk Call Manager, but on MyService, and I want to define and use Action.LoginExt(this.config.login, this.config.password) with another one with an extra parameter.
Is this possible? I tried this in my own module:
var AmiIo = require('ami-io');
var amiio = AmiIo.createClient({port:5038, host:'x.x.x.x', login:'system', password:'admin'});
amiio.prototype.auth = function (data) {
this.logger.debug('First message:', data);
if (data.match(/MyService Version/)) {
this._setVersion(data);
this.socket.on('data', function (data) {
this.splitMessages(data);
}.bind(this));
this.send(new Action.LoginExt(this.config.login, this.config.password, this.config.extra), function (error, response) {
if (response && response.response === 'Success') this.emit('connected');
else this.emit('incorrectLogin');
}.bind(this));
} else {
this.emit('incorrectServer', data);
}
};
...but it resulted in TypeError: Cannot set property 'auth' of undefined, and now I am clueless.
Also, can I define a new Action.LoginExt object in my own module? How?
The action.js module defines the Action objects as follows:
function Action(name) {
Action.super_.bind(this)();
this.id = this.getId();
this.set('ActionID', this.id);
this.set('Action', name);
}
(function(){
var Message = require('./message.js');
var util = require('util');
util.inherits(Action, Message);
})();
Action.prototype.getId = (function() {
var id = 0;
return function() {
return ++id;
}
})();
function Login(username, secret) {
Login.super_.bind(this, 'Login')();
this.set('Username', username);
this.set('Secret', secret );
}
... more functions ...
(function() {
var actions = [
Login,
... more functions ...
];
var util = require('util');
for (var i = 0; i < actions.length; i++) {
util.inherits(actions[i], Action);
exports[actions[i].name] = actions[i];
}
exports.Action = Action;
})();
What I think I understand is that Action is subclassed from Message. The Login function in its turn is subclassed from Action, and exported (in the last code block).
So I think in my code I could try something similar:
// extend ami-io with LoginExt function
function LoginExt(username, secret, company) {
Login.super_.bind(this, 'LoginExt')();
this.set('Username', username);
this.set('Secret', secret );
this.set('Company', company);
}
var util = require('util');
util.inherits(LoginExt, amiio.Action);
But util.inherits fails with undefined. I've also opened a issue on ami-io.
You can use:
var AmiIo = require('ami-io');
AmiIo.Action.Login = function NewConstructor(){}; //to override Login action
//new constructor shold extend AmiIo.Action.Action(actionName)
//and also, you can use
AmiIo.Action.SomeNewAction = function SomeNewAction(){};//to create new actuion
//it also should extend AmiIo.Action.Action(actionName);
AmiIo.Action is just an Object. All constructors are fields of it.
To create new events you don't need to do anything, because it is just an object. If server send to you
Event: Armageddon
SomeField: 123
ami-io will create event with name 'Armageddon'.
To override Client#auth() method, you just should do
var AmiIo = require('ami-io');
AmiIo.Client.prototype.auth = function (){};//new function
amiio is an instance of a Client. The prototype property is only meaningful on constructor functions, such as Client. It is not meaningful on the result of a constructor function (except in the uncommon case that the instance happens to also be a function itself -- but even in that case, altering the instance's prototype does not influence its parent constructor).
Instead, you need to get the instance's prototype with Object.getPrototypeOf:
Object.getPrototypeOf(amiio).auth = function() { ... }
If you don't need to change this for every client, but only a single client, you don't need to change the prototype at all. Changing the instance's auth is sufficient:
amiio.auth = function() { ... }
Note that you code will not work if Action.LoginExt is local to the module scope. If the module exports it, you can probably do AmiIo.Action.LoginExt instead. If it does not export LoginExt, you will need to copy the code that implements it a re-implement it in your importing scope. It may be simpler to modify the module itself.
Here's the solution I applied that worked:
// Override the AmiIo auth procedure, because the other login is slightly different
// Write our own Login function (which adds a company)
function Login(username, secret, company) {
Login.super_.bind(this, 'Login')();
this.set('Username', username);
this.set('Secret', secret );
this.set('Company', company);
}
// This function should inherit from Action
var util = require('util');
util.inherits(Login, AmiIo.Action.Action);
AmiIo.Action.Login = Login;
// replace the auth with our own, to add the company. Also
// this sends a slightly different greeting: "Service Version 1.0"
AmiIo.Client.prototype.auth = function (data) {
if (data.match(/Service Version/)) {
this._setVersion(data);
this.socket.on('data', function (data) {
this.splitMessages(data);
}.bind(this));
this.send(new AmiIo.Action.Login(this.config.login, this.config.password, this.config.company), function (error, response) {
if (response && response.response === 'Success') this.emit('connected');
else this.emit('incorrectLogin');
}.bind(this));
} else {
this.emit('incorrectServer', data);
}
};
// our own function to grab the version number from the new greeting
AmiIo.Client.prototype._setVersion = function(version){
var v = version.match(/Service Version ([\d\.]*[\-\w\d\.]*)/i);
if (v){
this.version = v[1];
}
};
So it turns out this was as doable as I hoped it would be. Both answers by #NumminorihSF and #apsillers helped me here, but I could mark only one of them as the best answer.

Categories