Creating And Extending A Lodash / Underscore Service In AngularJS
This is a really minor post, but it represents a pattern for including 3rd-party libraries in an AngularJS application. In this example, I happen to be using Lodash; but, this could just as easily apply to any other external JavaScript library. Since AngularJS relies on dependency injection, I like to take all my 3rd-party scripts and expose them through AngularJS factories so that they can be injected into my Controllers, Services, Directives, and Run blocks.
Run this demo in my JavaScript Demos project on GitHub.
When you include a 3rd-party library, like Lodash or Underscore, it's typically made available on the global scope (ie, window). But, in an AngularJS application, we don't want to rely on the global scope - we want to rely on dependency injection. This allows our components to be intuitive, predictable, and testable.
As such, I will generally create an AngularJS factory that returns a reference to the 3rd-party library. Inside that factory, we can do a couple of interesting things. First, we can delete the global reference to the 3rd-party library; doing so removes the ability for global-references to leak into our various other modules. But, the factory also provides a nice opportunity to extend the 3rd-party library with custom methods.
To see this in action, I've created a "lodash" service that both deletes the global reference to "_" and adds the custom method, _.naturalList(). This custom function is then available wherever I have AngularJS inject the "_" singleton.
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Creating And Extending A Lodash / Underscore Service In AngularJS
</title>
</head>
<body ng-controller="AppController">
<h1>
Creating And Extending A Lodash / Underscore Service In AngularJS
</h1>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs/angular-1.2.22.min.js"></script>
<script type="text/javascript" src="../../vendor/lodash/lodash-2.4.1.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope, _ ) {
// NOTE: We are injecting _ into this controller using Angular's
// dependency-injection framework.
// Set up friends collection for demo.
var friends = [
{
id: 1,
name: "Sarah",
isGoodFriend: true,
isBestFriend: false
},
{
id: 2,
name: "Tricia",
isGoodFriend: false,
isBestFriend: false
},
{
id: 3,
name: "Kim",
isGoodFriend: true,
isBestFriend: true
},
{
id: 4,
name: "Joanna",
isGoodFriend: true,
isBestFriend: false
}
];
// Find the good friends.
var goodFriends = _.where( friends, "isGoodFriend" );
// Of the good friends, find the best friends!
var bestFriends = _.where( goodFriends, "isBestFriend" );
// Log us some interesting stats on friendship.
console.info( "Good friends: %s", _.naturalList( _.pluck( goodFriends, "name" ) ) );
console.info( "Best friends: %s", _.naturalList( _.pluck( bestFriends, "name" ) ) );
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// Make sure _ is invoked at runtime. This does nothing but force the "_" to
// be loaded after bootstrap. This is done so the "_" factory has a chance to
// "erase" the global reference to the lodash library.
app.run(
function( _ ) {
// ...
}
);
// I provide an injectable (and exteded) version of the underscore / lodash lib.
app.factory(
"_",
function( $window ) {
// Get a local handle on the global lodash reference.
var _ = $window._;
// OPTIONAL: Sometimes I like to delete the global reference to make sure
// that no one on the team gets lazy and tried to reference the library
// without injecting it. It's an easy mistake to make, and one that won't
// throw an error (since the core library is globally accessible).
// ALSO: See .run() block above.
delete( $window._ );
// ---
// CUSTOM LODASH METHODS.
// ---
// I return the given collection as a natural language list.
_.naturalList = function( collection ) {
if ( collection.length > 2 ) {
var head = collection.slice( 0, -1 );
var tail = collection[ collection.length - 1 ];
return( head.join( ", " ) + ", and " + tail );
}
if ( collection.length === 2 ) {
return( collection.join( " and " ) );
}
if ( collection.length ) {
return( collection[ 0 ] );
}
return( "" );
};
// Return the [formerly global] reference so that it can be injected
// into other aspects of the AngularJS application.
return( _ );
}
);
</script>
</body>
</html>
As you can see, I'm defining my "_" factory, which augments the underlying lodash library and then deletes it from the global scope (ie, window). I'm then injecting that service into the main Controller where I can use it to access data more efficiently. And, when we run the above code, we get the following console output:
Good friends: Sarah, Kim, and Joanna
Best friends: Kim
Above my factory, you might notice that I have an empty .run() block. This run block does nothing but ensure that the "_" factory is invoked. If you recall from one of my earlier posts, AngularJS factories and services are instantiated on-demand. This means that if I never request the "_" component, AngularJS will never call the factory method, which means that I'll never delete the underlying 3rd-party library from the global scope, which means that global references to "_" could leak into the code-base. The .run() block forces AngularJS to call my factory method after bootstrap which makes sure to remove the global reference to my 3rd-party library.
Of course, you may not always be able to delete global references - you may be including other plugins that rely on them. But, generally speaking, this is still the pattern I try to follow - wrapping all 3rd-party libraries inside a factory method that makes the library explicitly available to the application through AngularJS' dependency injection framework.
Want to use code from this post? Check out the license.
Reader Comments
Cool, good observation on `.run` to make sure this factory always happens. Also, good decision on deleting from window scope to make sure people do not use _ directly. You could also extend lodash using their own built-in method mixin.
@Gleb,
Thanks my man. I learned the .run() approach when I started to see more global-references slip into the code.
Also, one other nice thing about this is that you have an opportunity to rename the library, if you want. For example, you could define the lodash as "utils" or something. Not that you want to create that kind of misdirection... but, just that you have a lot of flexibility.
Nice pattern! You can also use https://lodash.com/docs#mixin inside your factory to create mixins. Yay.
@Chief,
Ah, interesting. I was not familiar with that method. Thanks!
@Ben
I liked your example so much, I wrote ng-wrap https://github.com/bahmutov/ng-wrap that can wrap arbitrary global (and optionally remove it) to make it available as a dependency.
<pre>
angular.module('App', ['ng-wrap'])
.run(function (ngWrap) {
ngWrap('_');
// or to leave it in global space
ngWrap('_', true);
})
.controller('AppController', function ($scope, _) {
// use _
</pre>
@Gleb,
Very interesting! I haven't really dove down into the "provider" layer much before. My understanding at that layer is fairly sparse. I think I follow what your code is doing, though. It's like you're creating a factory after the app has been bootstrapped.
Great article! Just out of curiosity what are you thoughts on deleting the global reference to underscore inside of the .run() block instead of the service?
While the difference might be negligible it seems that by injecting _ only when a service or controller is explicitly using it you might save a bit of memory.
@Ben
Yup, the $provider allows you to add new stuff to the injector (this is what gets called when you do angular.module().value or angular.module().factory).
I use it to add new stuff to the injector at run time.
@Mike,
If you're going to use the .run(), then you end up deleting the global reference right away, any way (since it forces the factory method to be called, which deletes the global reference). In that case, it doesn't make too much of a difference as to where it is deleted.... I think. Unless I'm misunderstanding what you mean.
@Gleb,
That sounds really powerful. In the past, I've dipped my toe into the provider-pool by storing referencing to various providers during the bootstrap phase:
www.bennadel.com/blog/2553-loading-angularjs-components-after-your-application-has-been-bootstrapped.htm
... but, I wouldn't say that I had a really firm understanding of what was happening under the hood. It seems like the provider interaction you're using would allow for a less brute-force approach to post-boostrap modification of the dependency injection framework.
More stuff to learn! :D
Hey Ben,
I like your pattern and I think it's a good practice but right now I'm dealing with a legacy application where libraries have to be referenced outside of controllers so this isn't an option yet for me.
Speaking of libraries, a while ago I abandoned Underscore and Lo-Dash in favor of http://ramdajs.com/ and will never look back. You're welcome to give it a try. One of my other favorite libraries is https://github.com/epeli/underscore.string which contains toSentence() that looks pretty similar to your naturalList() function.
Lastly, it seems a bit misleading that you use the delete and return statements with parentheses. It suggests that parentheses are required but they're optional in this case. Do you do this on purpose?
Thanks for the article and keep it up!
@László,
I've not heard of the RamdaJS library. Can you share any thoughts on why you made the switch? At first glance, it seems to have many of the same kinds of functions. What are you finding attractive about it?
As far the use of parenthesis, I just like them :D I like the way they look. And, for me at least, I think they make the code easier to understand. If you don't use parenthesis, I think people may be too tempted to make code that looks like this:
function foo() {
. . . return function bar() {
. . . . . . // ... more stuff there.
. . . };
}
Without the parenthesis, I think people try to "jam too much stuff" into the return statement. If you have to add the parenethesis, code like that above starts to look more bizarre:
function foo() {
. . . return(
. . . . . . function bar() {
. . . . . . . . . // ... more stuff there.
. . . . . . }
. . . );
}
... which gets people to refactor it to something that tries to be a little less "clever":
function foo() {
. . . function bar() {
. . . . . . // ... more stuff there.
. . . };
. . . return( bar );
}
But, keep in mind this is PERSONAL preference. I'm not saying my way is better. It's just how I personally like to write code.
@Ben
http://bahmutov.calepin.co/lodash-to-ramda-example.html
@Gleb,
Ah, groovy, thanks!
@Ben,
Ramda functions are curried and always take the array as the last argument unlike Underscore or Lo-Dash. This allows for more powerful functional composition.
I checked the linked article and saw that you mainly use .find() and .where(). Here's how I use .find() in Ramda for various scenarios:
R.find(R.eq(42), myList);
R.find(R.propEq('objectProperty', 42), myList);
R.find(R.pathEq('objectProperty.objectSubProperty', 42), myList);
* First, we're trying to .find() the object in myList that equals to 42.
* Second, we're trying to .find() the object in myList with the property 'objectProperty' that is 42.
* Third, we're trying to .find the object in myList with the property and subproperty of 'objectProperty.objectSubProperty' that equals to 42.
Ramda functions being curried, it's also possible to create reusable functions easily:
var findObjectProperty42 = R.find(R.propEq('objectProperty', 42));
findObjectProperty42(myList);
Might not seem like a huge deal but this is just the tip of the iceberg and it's possible to a whole lot with very little.
I'm not fond of the idea of injecting '_' on run() just to clear the global scope. What about the following approach:
app.config(['$provide', function ($provide) {
$provide.value('_', window._);
delete window._;
}]);
I know I'm using the global 'window' instead of '$window' but it's just for this particular case. I believe this can still be unit tested with Karma.
My question now becomes, would it be so wrong to do it like this? Would really love to hear everyone's thoughts about this.
Those functional fans will be happy to know lodash now offers lodash-fp for lodash with auto-curried iteratee-first methods. This way you get the quality, performance, & innovation lodash offers with the method signature you prefer - https://www.npmjs.com/package/lodash-fp
@Ricardo,
I remembered a small issue with my implementation, $provide.value is going to create a mutable value, which allows anyone to rewrite it anywhere. However, I tried the same for a constant and factory and I could still rewrite the lodash injection in one place and then get an undefined error thrown while accessing it in another.
I don't understand why this is happening...
@Ricardo,
I'd have to dig into the $provide a bit more to see what is going on. I've only use $provide's decorator() method, I don't think I've ever used any of the other values. I'm also not entirely sure what you mean by "mutable." If you inject _ into another component, it will be able to edit it in any way.
@All,
I'll have to give Ramda some time. Frankly, Lodash is already a bit of a brain-overload :D I feel like remember what all the Lodash functions do is a bit akin to remembering 1,000 places of Pi. Sure, it's possible, but it takes a lot of practice and will make your brain bleed ;)
I'm trying this approach and finding that it fails in my karma test runs (0.13.9) with PhantomJS (1.9.8) & angular (1.4.7). Seems it runs the factory once, grabs the ref and then deletes it from $window, only to call the factory again and now $window._ is gone. So, the first test with that dependency passes, but the others fail :)
Of course if I don't delete $window._ it is fine... deleting works fine as the app runs, just not under test.
Confirmed by putting a console.log in the factory, it is run many times during the tests. Just once at runtime.
Any idea?
@Mark,
Did you find any solution to this problem? Our solution right now is to put _ back on window in a afterEach function. It works but feels fragile.
@Gejope,
no, just left it on $window for now :/