Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Proof-Of-Concept: Injecting Features Into An InVision Share Link Experience Using AngularJS 1.6.7

By Ben Nadel on

About a year-and-a-half ago, I put together a proof-of-concept for a Screen Flow feature that I thought might be of interest at InVision. Unfortunately, in the last 20-months, I've been unable to generate any interest in the concept. However, I still think that there may be one or two users out there that are interested in this idea. So, as a follow-up experiment, I wanted to see if there was a way that I could inject the Screen Flow feature into the InVision runtime without having to mess with the product itself. As it turns out, AngularJS 1.6.7 added a method called .loadNewModules() that allows developers to augment the Angular app Injector after bootstrapping. Before applying this to Screen Flow, I wanted to try a simple proof-of-concept to see how .loadNewModules() even works.

View this code in my JavaScript Demos project on GitHub.

Seven years ago, I did a little bit of experimentation with lazy-loading components into an AngularJS application. But, in that experiment, it required that the application developers add special glue-code that would allow the AngularJS injector to be populated even after the application had been bootstrapped. In this new experiment, however, I don't want the existing AngularJS application to know anything about what is about to happen.

Thankfully, in a Google search, I came across this gist by Felix Mosheev. In said gist, Felix pointed out that AngularJS 1.6.7 added a new method, .loadNewModules(), which allows new modules to be added to the AngularJS injector. I had never seen this method before, so I figured it was worth a little research-and-development.

For this proof-of-concept, all I'm going to do is define a new Directive that has a template and a Controller. I'm going to use the .loadNewModules() method to pull this Directive into the runtime. And then, I'm going to use the $compile() function in order to "bootstrap" and inject this Directive into an active Share link experience.

Here's the remote JavaScript file that contains this logic:

(function( ng ) {

	var moduleName = ng.module( "LazyLoadPOC", [ "InVision" ] )
		.directive(
			"lazyPoc",
			function LazyPocDirective( fakeCssModuleCompiler ) {

				// The fakeCssModuleCompiler is a Service defined in the existing
				// application. This will make sure that this lazy-injected directive can
				// make use of the existing application aspects.
				return({
					compile: fakeCssModuleCompiler( "c-lazy" ),
					controller: "lazyPoc.Controller",
					controllerAs: "vm",
					restrict: "E",
					scope: true,
					template: (
						"<div style='background: white ; color: black ; padding: 10px 15px ; position: fixed ; z-index: 999 ;'>" +
							"URL: {{ vm.url }}" +
						"</div>"
					)
				});

			}
		)
		.controller(
			"lazyPoc.Controller",
			function LazyController( $location, $scope ) {

				var vm = this;

				vm.url = $location.url();

				// Listen for changes in the URL so that we can make sure that change-
				// detection digests are being applied to this injected element.
				$scope.$on(
					"$locationChangeSuccess",
					function handleLocationChange() {

						vm.url = $location.url();

					}
				);

			}
		)
		.run(
			function init( $compile, $rootScope, config ) {

				console.group( "Lazy Load POC" );
				console.log( "Initializing run blocks." );
				console.log( "Prototype:", config.project.name );
				console.log( "Share Key:", config.share.key );
				console.groupEnd();

				// Compile, inject, and integrate the test component with the existing
				// AngularJS application.
				$compile( "<lazy-poc></lazy-poc>" )(
					$rootScope.$new(),
					function injectClone( ngElement, scope ) {

						document.body.appendChild( ngElement[ 0 ] );
						scope.$applyAsync();

					}
				);

			}
		)
		.name
	;

	// As of AngularJS 1.6.7, we can use .loadNewModules() to lazy-load modules into an
	// AngularJS application. Huge shout-out to Felix Mosheev for pointing me in this
	// direction with his demo gist:
	// --
	// https://gist.github.com/felixmosh/088657e92390d7450c94216f62ddc738
	ng
		.element( document )
		.injector()
		.loadNewModules( [ moduleName ] )
	;

})( angular );

This "feature" does two things:

  1. It pulls-in the fakeCssModuleCompiler() service from the existing application. This function allows scoped CSS modules to be hacked into AngularJS. And, I'm using it to test whether or not I can depend on existing Share services.

  2. It echoes the $location.url() value in the DOM. I'm using this to make sure that I can plug into the AngularJS change-detection digest of the existing application.

Notice that at the end, I'm grabbing the AngularJS injector for the running application and then I'm loading the new feature module. I put this feature.js file up on a small GitHub repository so that I could host it using GitHub Pages.

With that file in place, I then hand-crafted a tiny bookmarklet that uses jQuery - which is already loaded in AngularJS - and its .getScript() function to pull that feature.js file into the currently-rendered webpage:

javascript:(function(w,$,k){(w[k]||(w[k]=$.getScript("https://bennadel.github.io/poc-invision-share-lazy-load/feature.js?_="+Date.now())));})(window,jQuery,"LazyLoadPOC");void(0);

Now, if I load up an InVision Share link and trigger the bookmarklet, we get the following Browser output:

A proof-of-concept being injected into an InVision share link using AngularJS.

As you can see, we were able to compile and inject the Directive defined in the lazy-loaded module using the Bookmarklet. And, as we navigate around the Share experience, we can see that the Directive hooks into the AngularJS change-detection digest and updates to reflect the current URL.

Awesome sauce!

So, it seems that - at least in theory - I could use the .loadNewModules() function in AngularJS 1.6.7 to inject custom features into an InVision Share link. Of course, there's a massive difference between a proof-of-concept and fully-functioning and user-friendly feature. But, it definitely gives my brain a whole lot of fat to chew on!



Reader Comments

Why hasn't this been upgraded beyond Angular 1.x? Not cynical here, but with the belief that there are always tradeoffs are we waiting for a compelling reason to update, just not needed or what is the thinking?

Reply to this Comment

@John,

Valid question -- just a lot of code and not enough people to actually focus on the upgrade. We've been on AngularJS 1.x for 8-years; so, you can imagine how much legacy code is in place. I am not sure we'll ever be able to focus on upgrading to modern Angular :( :( :(

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.