I mentioned in a previous post that I've been trying to implement the module pattern to our front-end javascript code at work. And I'm happy to report that so far it's going well. I've made a few discoveries that I thought I'd share!
Default Module Functionality
When I first started learning about the module pattern -- and IIFE's in general -- I never considered the idea of adding default functionality to the newly created modules. In hindsight, now I realize that actually would have been quite useful! If you have a web app that contains many smaller apps within it, it can be tough to bring on new developers if you're not using a framework. Why? Because each app may be coded in an entirely different style -- one issue among many. Remember, one of the main reasons for introducing the module pattern is to begin to standardize.
Anyway, let's get to the code. Let's imagine we have a standard MAIN
module from which all other modules will be created. Here, it's written in two different ways to show what's possible:
// In this example, our modules are just objects stored within objects. | |
// In my opinion, this is the better of the two approaches. It's simpler | |
// and lends itself to object destructuring for easier access to heavily | |
// nested variables/fn's. | |
const MAIN = (() => { | |
let _modules = {}; | |
const _createModule = (_name) { | |
((M) => { | |
_modules[_name] = {}; // Our new module. | |
M[_name] = { | |
get n() { return _modules[_name]; } // A reference to the module. | |
}; | |
})(MAIN); | |
}; | |
return { | |
createModule(moduleName) { | |
_createModule(moduleName); | |
} | |
}; | |
})(); | |
// In this example, instead of being references to an anonymous object | |
// stored within our main IIFE, modules are references to new IIFEs. | |
// The notation here is really confusing. So I break it apart | |
// as best I can. | |
const MAIN2 = (() => { | |
const _createModule = (_name) { | |
((M) => { | |
M[_name] = { get _name() { // The reference for our module. | |
return (() => { return {}; })(); // Our new module. | |
}}; | |
})(MAIN2); | |
}; | |
return { | |
createModule(moduleName) { | |
_createModule(moduleName); | |
} | |
}; | |
})(); | |
// Creating modules looks like this for both: | |
MAIN.createModule("User"); | |
MAIN2.createModule("User"); | |
// Writing new code for our newly created | |
// modules works like you'd expect: | |
MAIN.User.saveInfo = function(userInfo) { | |
// ... | |
}; | |
// Or using ES6 arrow notation, like so: | |
MAIN.User.saveInfo = (userInfo) => { | |
// ... | |
}; |
As you can see, in the first IIFE -- MAIN
-- we store our modules in an object and then just point to it in the return object of the MAIN
IIFE. In the second IIFE -- MAIN2
--, we actually create a reference to another IIFE in our return object. I prefer the object references of the first method for the sake of simplicity, but the second method allows anonymously scoped functionality to be added to all of our new modules!
Let's now take a look:
// Nested object reference appraoch: | |
const MAIN = (() => { | |
let _modules = {}; | |
const _createModule = (_name) { | |
((M) => { | |
_modules[_name] = { // Our new module. | |
CONST = {}, // Creating default vars & fn's | |
JSON = {}, | |
}; | |
M[_name] = { | |
get n() { return _modules[_name]; } // A reference to the module. | |
}; | |
})(MAIN); | |
}; | |
return { | |
createModule(moduleName) { | |
_createModule(moduleName); | |
} | |
}; | |
})(); | |
// Nested IIFE approach: | |
const MAIN2 = (() => { | |
const _createModule = (_name, _pageSettingPath) { | |
((M) => { | |
M[_name] = { get _name() { // The reference for our module. | |
return (() => { // Our new IIFE module. | |
// Anonymously scoped code here. | |
let _CONST = {}; // Variable to store constants for module. | |
let _JSON = {}; // Variable to store relevant JSON data for module. | |
let _checkAuth = () => { ... }; // Pretend function to check user auth. | |
const _loadSettings = (_path) => { | |
Server.call(_path, (response) => { | |
_CONST = response.constants; | |
}); | |
}; | |
_loadSettings(_pageSettingPath); | |
// Module IIFE's return object. | |
return { | |
get CONST() { return _CONST; }, | |
get JSON() { return _JSON; }, | |
checkAuth() { _checkAuth(); } | |
}; | |
})(); | |
}}; | |
})(MAIN2); | |
}; | |
return { | |
createModule(moduleName) { | |
_createModule(moduleName); | |
} | |
}; | |
})(); |
As you can see, both methods offer ways to provide default functionality; however, the second method allows us to take that default functionality to a whole new level. By modifying our _createModule
function in MAIN2
and adding a second parameter for a file path, we are now opening up the possibility to load module settings as soon as the createModule
function is run! No interaction outside of supplying the two parameters to _createModule
required! While I still prefer the simplicity of the first method, the second method now allows us to further begin introducing a whole new set of coding standards that will unify our apps from a developers prospective. On top of this, the anonymous scoping and immediately invoked nature of IIFE's has also allowed us to start developing our very own little framework!

Now, bear with me, I only made these discoveries today, so I won't go into any further details until I've had some time to mess around with these concepts. In the meantime, here is one final applied example using a pretend app for Wahoo to help visualize how this organizes your code:
// Let's pretend we're working for Wahoo, a fitness company. | |
// I've recently been using their bike trainers -- not | |
// sponsored. | |
// Our main application module. | |
// File: main.js ____________________________ | |
const Wahoo = (() => { | |
// "Anonymous" code. Anonymous here just means this code | |
// is inaccessible other than through the return object. | |
// You will very much still be able to see it in a developer | |
// console. | |
const _createModule = (_name, _filePath) => { | |
// Our function runs an IIFE that aliases the name | |
// of our main module to 'W'. | |
((W) => { | |
// We are adding a getter to our main modules | |
// return object for our new module. | |
W[_name] = { | |
get _name() { | |
// We return a new IIFE, this time our module's. | |
return (() => { | |
// Just like our main module, we have an anonymous | |
// scope available here. | |
let _settings = {}: | |
// An imaginary server call that loads a | |
// JSON file with our module's default settings | |
// such as constant variables. | |
const _loadSettings = () => { | |
// An imaginary server call function. Let's | |
// assume it's synchronous: | |
Server.sync.call(_filePath, (response) => { | |
_settings = response; | |
}); | |
}; | |
// Let's use that file path to get those settings | |
_loadSettings(); | |
// We also have a standard return object. | |
return { | |
get Settings() { | |
return _settings; | |
} | |
}; | |
})(); | |
} | |
} | |
})(Wahoo); | |
}; | |
// The IIFE's return object, through which we interact with | |
// our "anonymous" code. | |
return { | |
createModule(moduleName, moduleSettingsFilePath) { | |
_createModule(moduleName, moduleSettingsFilePath); | |
} | |
}; | |
})(): | |
// Now let's imagine a fake directory. | |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
// main.js | |
// bike | |
// | | |
// |-bike.js | |
// |-settings.json | |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
// Here are some imaginary functions to | |
// visualize how coding using this pattern | |
// looks like: | |
// File: bike.js ____________________________ | |
// Creating module and providing json file path: | |
Wahoo.createModule('Bike', 'bike/settings.json'); | |
// Now we can just code for our 'Bike' module: | |
Wahoo.Bike.loadUserBike = (userId) => { ... }; | |
Wahoo.Bike.showMileage = () => { ... }; | |
Wahoo.Bike.deleteRecord = () => { ... }; | |
// Let's pretend we need access to a setting we loaded. | |
// Again this is fake code that you can imagine | |
// the functionality for. | |
Wahoo.Bike.setDefault = () => { | |
let data = Wahoo.Bike.Settings.DEFAULTS; | |
Server.async.call('POST', data, (response) => { | |
(response.success) ? toast.success : toast.error; | |
}); | |
}; |
What are your thoughts? I may be biased, but I think that looks neat and tidy!
Object Destructuring Saves Time
Having the ability to add default settings to your modules aside, here's another small tidbit I'd like to share. Remember to de-structure your nested objects for easier access! Given that everything in your modules is in an object, you can just pick and pull what you need.
// Let's pretend Wahoo.Bike.Settings contains at | |
// least the following: | |
Wahoo.Bike.Settings = { | |
FILEPATHS: { | |
MAIN: "folder/file.html", | |
ACCOUNT: "folder/file.html", | |
FRIENDS: "folder/file.html", | |
HISTORY: "folder/file.html", | |
}, | |
VIEWS: { | |
MAIN: "Wahoo Bike", | |
ACCOUNT: "Wahoo Bike Account", | |
FRIENDS: "Wahoo Bike Friends", | |
HISTORY: "Wahoo Biking History", | |
} | |
}; | |
// And now let's imagine a function that will rely on those | |
// variables: | |
Wahoo.Bike.loadView = (viewToLoad) => { | |
// Destructuring for easy access! | |
const { FILEPATHS, VIEWS } = Wahoo.Bike.Settings; | |
switch (viewToLoad) { | |
case VIEWS.ACCOUNT: | |
Server.sync.call('GET', FILEPATHS.ACCOUNT, (response) => { | |
Wahoo.Bike.currentView = VIEWS.ACCOUNT; | |
}); | |
// ... | |
break; | |
case VIEWS.FRIENDS: | |
Server.sync.call('GET', FILEPATHS.FRIENDS, (response) => { | |
Wahoo.Bike.currentView = VIEWS.FRIENDS; | |
}); | |
// ... | |
break; | |
case VIEWS.HISTORY: | |
Server.sync.call('GET', FILEPATHS.HISTORY, (response) => { | |
Wahoo.Bike.currentView = VIEWS.HISTORY; | |
}); | |
// ... | |
break; | |
case VIEWS.MAIN: | |
Server.sync.call('GET', FILEPATHS.MAIN, (response) => { | |
Wahoo.Bike.currentView = VIEWS.MAIN; | |
}); | |
// ... | |
break; | |
default: | |
// ... | |
break; | |
}; | |
}; |
Anyway, that's all I have to share for now. Hope you found this useful!
Top comments (0)