roberto tomás roberto tomás - 3 months ago 16
Javascript Question

angular route resolve - wait on value to be defined

I wrote a wrapper to load/unload javascript and css for routes conditionally. It is called in the route resolve section like:

resolve: {
factory: function (Res) {
controllersAndServices(Res, {styles: 'estilos/check_insurance.css'})
}
}


It it generally works like a charm. Problem is, some libraries, like Plotly defer creating their main variable, so the library loads before it is avaialable. As a result, I get errors that Plotly is not defined in my controller, then later, they go away. definitely by the time I get to the console, Plotly is happily waiting for me.

I do realize that this problably could happen to a lot more libraries with my custom loader, but happily most are too small to have this problem. plotly is both large and can take many hundreds or even thousands of milliseconds to set up.

So I need to wait on Plotly being defined in my
resolve
, somehow. How do you do that?

Library:

res_service.js



angular.module('app').service('Res', ['$rootScope', function ($rootScope) {
var scope = $rootScope
scope._scripts = scope._scripts || []
scope._styles = scope._styles || []
scope._rawScripts = scope._rawScripts || []
scope._rawStyles = scope._rawStyles || []

return {
style: function (inp) {
var hrefs = []
if (typeof inp === 'string') {
hrefs.push(inp)
} else {
hrefs = inp
}

scope._styles = []
for (var i in hrefs) {
console.log('styling page with ' + hrefs[i])
var style = document.createElement('link')
if (!/^https?\:\/\//.test(hrefs[i])) {
style.type = 'text/css'
}
style.href = hrefs[i]
style.rel = 'stylesheet'

if (scope._rawStyles === undefined) {
scope._rawStyles = []
}
scope._rawStyles.push([hrefs[i], style])
scope._styles.push(document.head.appendChild(style))

scope.$on('$destroy', this.clean_styles)
}
},

clean_styles: function () {
var removables = []
for (var key in scope._rawStyles) {
console.log('deleting style ' + scope._rawStyles[key][0])
scope._styles[key].parentNode.removeChild(scope._rawStyles[key][1])
removables.push(key)
}

scope._styles = scope._styles.filter(function (v) { return (removables.indexOf(v) !== -1)? true: false })
scope._rawStyles = scope._rawStyles.filter(function (v) { return (removables.indexOf(v) !== -1)? true: false })
},

script: function (inp) {
var hrefs = []
if (typeof inp === 'string') {
hrefs.push(inp)
} else {
hrefs = inp
}

scope._scripts = []
for (var i in hrefs) {
console.log('loading javascript: ' + hrefs[i])
var script = document.createElement('script')
if (!/^https?\:\/\//.test(hrefs[i])) {
script.type = 'text/javascript'
}
script.src = hrefs[i]

if (scope._rawScripts === undefined) {
scope._rawScripts = []
}
scope._rawScripts.push([hrefs[i], script])
scope._scripts.push(document.head.appendChild(script))

scope.$on('$destroy', this.clean_scripts)
}
},

clean_scripts: function () {
var removables = []
for (var key in scope._rawScripts) {
console.log('deleting script ' + scope._rawScripts[key][0])
scope._scripts[key].parentNode.removeChild(scope._rawScripts[key][1])
removables.push(key)
}

scope._scripts = scope._scripts.filter(function (v) { return (removables.indexOf(v) !== -1)? true: false })
scope._rawScripts = scope._rawScripts.filter(function (v) { return (removables.indexOf(v) !== -1)? true: false })
}

}
}])


example breaking route usage:

route.js



...
when('/', {
templateUrl: 'hipermídia/check_insurance.template.html',
controller: 'pedidos_controller',
resolve: {
factory: function (Res) {
controllersAndServices(Res, {scripts: 'https://cdn.plot.ly/plotly-latest.min.js'})
}
}
}).


At this point, if you use plotly in your controller like:

var data = [
{
x: ['giraffes', 'orangutans', 'monkeys'],
y: [20, 14, 23],
type: 'bar'
}
];

Plotly.newPlot('myDivID', data);


You'll likely get something like
ReferenceError: Plotly is not defined
.
If you move it back aways, with a synchronous calls, maybe a setTimeout, or even just angular's document ready, the likelyhood of seeing a a proper graph will increase.

Answer

A bit hacky, but I would loop/test if the Plotly var exists in a root route from which every route that need the library is a child of (the resolve is applied to child routes if they inject the property as well).

something like the following would be resolved in the root route:

resolve: {
    plotly: function () {
        const resolvePlotly = function () {
          while (typeOf Plotly) === 'undefined') {
            return setTimeout(function () { resolvePlotly() }, 150)
          }
          return Plotly
        }

        return resolvePlotly()
    }
}

I'd add that resolved injection to that route, and pass it down to the child routes so the dependency would still wait for the Plotly object to be defined before running any dependent controller.

You could also write a factory wrapping the Plotly object that returns it when available, and inject/use it instead of directly accessing Plotly.