From 61f2e913e42e12ea6babf69529f6f119ed869feb Mon Sep 17 00:00:00 2001 From: Tom Wayson Date: Tue, 7 Nov 2017 06:33:45 -0800 Subject: [PATCH 1/9] add promise-based function to load script --- karma.conf.js | 3 +- src/esri-loader.ts | 92 ++++++++++++++++- test/esri-loader.spec.js | 206 +++++++++++++++++++++++++++++++++++++-- test/mocks/jsapi3x.js | 7 ++ test/mocks/jsapi4x.js | 7 ++ tsconfig.json | 1 + tslint.json | 3 +- 7 files changed, 305 insertions(+), 14 deletions(-) create mode 100644 test/mocks/jsapi3x.js create mode 100644 test/mocks/jsapi4x.js diff --git a/karma.conf.js b/karma.conf.js index a2df549..d3e0664 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -20,7 +20,8 @@ module.exports = function(config) { // list of files / patterns to load in the browser files: [ builtFile, - 'test/**/*.js' + 'test/*.js', + { pattern: 'test/mocks/*.js', included: false } ], diff --git a/src/esri-loader.ts b/src/esri-loader.ts index bb9d6ae..55ca7fe 100644 --- a/src/esri-loader.ts +++ b/src/esri-loader.ts @@ -11,9 +11,16 @@ limitations under the License. */ +let loadScriptPromise; + // get the script injected by this library function getScript() { - return document.querySelector('script[data-esri-loader]'); + return document.querySelector('script[data-esri-loader]') as HTMLScriptElement; +} + +// TODO: at next breaking change replace the public isLoaded() API with this +function _isLoaded() { + return typeof window['require'] !== 'undefined'; } // interfaces @@ -28,10 +35,89 @@ export interface IBootstrapOptions { // has ArcGIS API been loaded on the page yet? export function isLoaded() { // TODO: instead of checking that require is defined, should this check if it is a function? - return typeof window['require'] !== 'undefined' && getScript(); + return _isLoaded() && getScript(); } // load the ArcGIS API on the page +export function loadScript(options: IBootstrapOptions = {}): Promise { + // default options + if (!options.url) { + options.url = 'https://js.arcgis.com/4.5/'; + } + + // if (loadScriptPromise) { + // // loadScript has already been called + // return loadScriptPromise + // .then((loadedScript) => { + // if (loadedScript.src !== options.url) { + // // potentailly trying to load a different version of the JSAPI + // return Promise.reject(new Error(`The ArcGIS API for JavaScript is already loaded (${loadedScript.src}).`)); + // } else { + // return loadedScript; + // } + // }); + // } + + loadScriptPromise = new Promise((resolve, reject) => { + let script = getScript(); + if (script) { + // the API is already loaded or in the process of loading... + // NOTE: have to test against scr attribute value, not script.src + // b/c the latter will return the full url for relative paths + const src = script.getAttribute('src'); + if (src !== options.url) { + // potentailly trying to load a different version of the JSAPI + reject(new Error(`The ArcGIS API for JavaScript is already loaded (${src}).`)); + } else { + if (_isLoaded()) { + // the script has already successfully loaded + resolve(script); + } else { + // wait for the script to load and then resolve + script.addEventListener('load', () => { + // TODO: remove this event listener + resolve(script); + }, false); + script.addEventListener('error', (err) => { + // TODO: remove this event listener + reject(err); + }, false); + } + } + } else { + if (_isLoaded()) { + // the API has been loaded by some other means + // potentailly trying to load a different version of the JSAPI + reject(new Error(`The ArcGIS API for JavaScript is already loaded.`)); + } else { + // this is the first time attempting to load the API + if (options.dojoConfig) { + // set dojo configuration parameters before loading the script + window['dojoConfig'] = options.dojoConfig; + } + // create a script object whose source points to the API + script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = options.url; + script.dataset['esriLoader'] = 'loading'; + // once the script is loaded... + script.onload = () => { + // update the status of the script + script.dataset['esriLoader'] = 'loaded'; + // return the script + resolve(script); + }; + // handle script loading errors + script.onerror = reject; + // load the script + document.body.appendChild(script); + } + } + }); + return loadScriptPromise; +} + +// TODO: deprecate export function bootstrap(callback?: (error: Error, dojoRequire?: any) => void, options: IBootstrapOptions = {}) { // default options if (!options.url) { @@ -112,6 +198,8 @@ export function dojoRequire(modules: string[], callback: (...modules: any[]) => // export a namespace to expose all functions export default { isLoaded, + loadScript, + // TODO: deprecate bootstrap, dojoRequire }; diff --git a/test/esri-loader.spec.js b/test/esri-loader.spec.js index 095fec9..a98bd1c 100644 --- a/test/esri-loader.spec.js +++ b/test/esri-loader.spec.js @@ -1,13 +1,33 @@ +// helper functions +// stub require function +function stubRequire() { + window.require = function (moduleNames, callback) { + if (callback) { + // call the callback w/ the modulenames that were passed in + callback.apply(this, moduleNames); + } + } +} +// remove script tags added by esri-loader +function removeScript() { + const script = document.querySelector('script[data-esri-loader]'); + if (script) { + script.parentElement.removeChild(script); + } +} +// remove previously stubbed require function +function removeRequire() { + delete window.require; +} + describe('esri-loader', function () { describe('when has not yet been loaded', function () { beforeEach(function() { - // remove previously stubbed require function - delete window.require; - // esri-loader script has not yet been loaded - spyOn(document, 'querySelector').and.returnValue(null); + removeRequire(); + removeScript(); }); it('isLoaded should be false', function () { - expect(esriLoader.isLoaded()) + expect(esriLoader.isLoaded()).toBeFalsy(); }); it('should throw error when trying to load modules', function() { function loadModules () { @@ -17,6 +37,172 @@ describe('esri-loader', function () { }); }); + describe('when loading the script', function () { + const jaspi3xUrl = 'base/test/mocks/jsapi3x.js'; + describe('with defaults', function () { + var scriptEl; + beforeAll(function (done) { + spyOn(document.body, 'appendChild').and.callFake(function (el) { + // call the onload callback + el.onload(); + }); + esriLoader.loadScript() + .then((script) => { + // hold onto script element for assertions below + scriptEl = script; + done(); + }); + }); + it('should default to latest version', function () { + expect(scriptEl.src).toEqual('https://js.arcgis.com/4.5/'); + }); + it('should not have set dojoConfig', function () { + expect(window.dojoConfig).not.toBeDefined(); + }); + }); + describe('with different API version', function () { + var scriptEl; + beforeAll(function (done) { + spyOn(document.body, 'appendChild').and.callFake(function (el) { + // call the onload callback + el.onload(); + }); + esriLoader.loadScript({ + url: 'https://js.arcgis.com/3.20' + }) + .then((script) => { + // hold onto script element for assertions below + scriptEl = script; + done(); + }); + }); + it('should load different version', function () { + expect(scriptEl.src).toEqual('https://js.arcgis.com/3.20'); + }); + }); + describe('with dojoConfig option', function () { + var dojoConfig = { + async: true, + packages: [ + { + location: 'path/to/somelib', + name: 'somelib' + } + ] + }; + beforeAll(function (done) { + spyOn(document.body, 'appendChild').and.callFake(function (el) { + // call the onload callback + el.onload(); + }); + esriLoader.loadScript({ + dojoConfig: dojoConfig + }) + .then((script) => { + done(); + }); + }); + it('should have set global dojoConfig', function () { + expect(window.dojoConfig).toEqual(dojoConfig); + }); + afterAll(function() { + window.dojoConfig = undefined; + }); + }); + describe('when already loaded by some other means', function () { + beforeAll(function () { + stubRequire(); + }); + it('should reject', function (done) { + esriLoader.loadScript({ + url: jaspi3xUrl + }) + .then(script => { + done.fail('call to loadScript should have failed'); + }) + .catch((e) => { + expect(e.message).toEqual(`The ArcGIS API for JavaScript is already loaded.`); + done(); + }); + }); + afterAll(function () { + // clean up + removeRequire(); + removeScript(); + }); + }); + describe('when called twice', function () { + describe('when loading the same script', function () { + it('should resolve the script if it is already loaded', function (done) { + esriLoader.loadScript({ + url: jaspi3xUrl + }) + .then(firstScript => { + // try loading the same script after the first one has already loaded + esriLoader.loadScript({ + url: jaspi3xUrl + }) + .then(script => { + expect(script.getAttribute('src')).toEqual(jaspi3xUrl); + done(); + }) + .catch((e) => { + done.fail('second call to loadScript should not have failed' + e); + }); + }) + .catch(() => { + done.fail('first call to loadScript should not have failed'); + }); + }); + it('should resolve an unloaded script once it loads', function (done) { + esriLoader.loadScript({ + url: jaspi3xUrl + }) + .catch(() => { + done.fail('first call to loadScript should not have failed'); + }); + // try loading the same script again + esriLoader.loadScript({ + url: jaspi3xUrl + }) + .then(script => { + expect(script.getAttribute('src')).toEqual(jaspi3xUrl); + done(); + }) + .catch((e) => { + done.fail('second call to loadScript should not have failed' + e); + }); + }); + }); + describe('when loading different scripts', function () { + it('should reject', function (done) { + esriLoader.loadScript({ + url: jaspi3xUrl + }) + .catch(() => { + done.fail('first call to loadScript should not have failed'); + }); + // try loading a different script + esriLoader.loadScript({ + url: 'base/test/mocks/jsapi4x.js' + }) + .then(script => { + done.fail('second call to loadScript should have failed'); + }) + .catch((e) => { + expect(e.message).toEqual(`The ArcGIS API for JavaScript is already loaded (${jaspi3xUrl}).`); + done(); + }); + }); + }); + afterEach(function () { + // clean up + removeRequire(); + removeScript(); + }); + }); + }); + describe('when bootstraping the API', function () { describe('with defaults', function () { var scriptEl; @@ -82,6 +268,9 @@ describe('esri-loader', function () { it('should have set global dojoConfig', function () { expect(window.dojoConfig).toEqual(dojoConfig); }); + afterAll(function() { + window.dojoConfig = undefined; + }); }); describe('when called twice', function () { var scriptEl; @@ -133,11 +322,8 @@ describe('esri-loader', function () { }); }); afterAll(function () { - // remove script tag - const script = document.querySelector('script[data-esri-loader]'); - if (script) { - script.parentElement.removeChild(script); - } + // clean up + removeScript(); }); }); }); diff --git a/test/mocks/jsapi3x.js b/test/mocks/jsapi3x.js new file mode 100644 index 0000000..2b173dd --- /dev/null +++ b/test/mocks/jsapi3x.js @@ -0,0 +1,7 @@ +// stub require function +window.require = function (moduleNames, callback) { + if (callback) { + // call the callback w/ the modulenames that were passed in + callback.apply(this, moduleNames); + } +} diff --git a/test/mocks/jsapi4x.js b/test/mocks/jsapi4x.js new file mode 100644 index 0000000..2b173dd --- /dev/null +++ b/test/mocks/jsapi4x.js @@ -0,0 +1,7 @@ +// stub require function +window.require = function (moduleNames, callback) { + if (callback) { + // call the callback w/ the modulenames that were passed in + callback.apply(this, moduleNames); + } +} diff --git a/tsconfig.json b/tsconfig.json index d1c120b..19fda28 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "declaration": true, "target": "es5", "module": "es2015", + "lib": ["dom", "es2015"], "sourceMap": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, diff --git a/tslint.json b/tslint.json index fc4b02f..62ece8b 100644 --- a/tslint.json +++ b/tslint.json @@ -11,6 +11,7 @@ } ], "object-literal-sort-keys": false, - "variable-name": [true, "check-format", "allow-leading-underscore"] + "variable-name": [true, "check-format", "allow-leading-underscore"], + "no-console": false } } \ No newline at end of file From fcda1049ff2b4fc77e3b28f40a49f7b639a37a30 Mon Sep 17 00:00:00 2001 From: Tom Wayson Date: Wed, 8 Nov 2017 12:20:11 -0800 Subject: [PATCH 2/9] added loadModules function that wraps require() in a Promise also will lazy load the API if it has not yet been loaded --- src/esri-loader.ts | 50 ++++++++++-------- test/esri-loader.spec.js | 109 ++++++++++++++++++++++++++++++++++----- test/mocks/jsapi3x.js | 10 ++-- test/mocks/jsapi4x.js | 10 ++-- 4 files changed, 130 insertions(+), 49 deletions(-) diff --git a/src/esri-loader.ts b/src/esri-loader.ts index 55ca7fe..0d24555 100644 --- a/src/esri-loader.ts +++ b/src/esri-loader.ts @@ -11,8 +11,6 @@ limitations under the License. */ -let loadScriptPromise; - // get the script injected by this library function getScript() { return document.querySelector('script[data-esri-loader]') as HTMLScriptElement; @@ -20,10 +18,12 @@ function getScript() { // TODO: at next breaking change replace the public isLoaded() API with this function _isLoaded() { + // TODO: instead of checking that require is defined, should this check if it is a function? return typeof window['require'] !== 'undefined'; } // interfaces +// TODO: rename to ILoadScriptOptions export interface IBootstrapOptions { url?: string; // NOTE: stole the type definition for dojoConfig from: @@ -34,7 +34,6 @@ export interface IBootstrapOptions { // has ArcGIS API been loaded on the page yet? export function isLoaded() { - // TODO: instead of checking that require is defined, should this check if it is a function? return _isLoaded() && getScript(); } @@ -45,20 +44,7 @@ export function loadScript(options: IBootstrapOptions = {}): Promise { - // if (loadedScript.src !== options.url) { - // // potentailly trying to load a different version of the JSAPI - // return Promise.reject(new Error(`The ArcGIS API for JavaScript is already loaded (${loadedScript.src}).`)); - // } else { - // return loadedScript; - // } - // }); - // } - - loadScriptPromise = new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { let script = getScript(); if (script) { // the API is already loaded or in the process of loading... @@ -66,7 +52,7 @@ export function loadScript(options: IBootstrapOptions = {}): Promise { + return new Promise((resolve, reject) => { + // If something goes wrong loading the esri/dojo scripts, reject with the error. + window['require'].on('error', reject); + window['require'](modules, (...args) => { + // Resolve with the parameters from dojo require as an array. + resolve(args); + }); + }); +} + +// returns a promise that resolves with an array of the required modules +// also will attempt to lazy load the ArcGIS API if it has not already been loaded +export function loadModules(modules: string[], loadScriptOptions?: IBootstrapOptions): Promise { + if (!_isLoaded()) { + // script is not yet loaded, attept to load it + return loadScript(loadScriptOptions).then(() => requireModules(modules)); + } else { + return requireModules(modules); + } +} + +// TODO: deprecate the following functions export function bootstrap(callback?: (error: Error, dojoRequire?: any) => void, options: IBootstrapOptions = {}) { // default options if (!options.url) { diff --git a/test/esri-loader.spec.js b/test/esri-loader.spec.js index a98bd1c..f841bcf 100644 --- a/test/esri-loader.spec.js +++ b/test/esri-loader.spec.js @@ -7,6 +7,12 @@ function stubRequire() { callback.apply(this, moduleNames); } } + window.require.on = function(name, callback) { + // if (callback) { + // // call the callback w/ the event name that was passed in + // callback(name); + // } + } } // remove script tags added by esri-loader function removeScript() { @@ -21,6 +27,7 @@ function removeRequire() { } describe('esri-loader', function () { + const jaspi3xUrl = 'base/test/mocks/jsapi3x.js'; describe('when has not yet been loaded', function () { beforeEach(function() { removeRequire(); @@ -29,16 +36,19 @@ describe('esri-loader', function () { it('isLoaded should be false', function () { expect(esriLoader.isLoaded()).toBeFalsy(); }); - it('should throw error when trying to load modules', function() { + it('should throw error when trying to load modules w/ dojoRequire', function() { function loadModules () { esriLoader.dojoRequire(['esri/map', 'esri/layers/VectorTileLayer'], function (Map, VectorTileLayer) {}); } expect(loadModules).toThrowError('The ArcGIS API for JavaScript has not been loaded. You must first call esriLoader.bootstrap()'); }); + afterEach(function() { + removeRequire(); + removeScript(); + }); }); describe('when loading the script', function () { - const jaspi3xUrl = 'base/test/mocks/jsapi3x.js'; describe('with defaults', function () { var scriptEl; beforeAll(function (done) { @@ -107,7 +117,7 @@ describe('esri-loader', function () { }); afterAll(function() { window.dojoConfig = undefined; - }); + }); }); describe('when already loaded by some other means', function () { beforeAll(function () { @@ -120,15 +130,14 @@ describe('esri-loader', function () { .then(script => { done.fail('call to loadScript should have failed'); }) - .catch((e) => { - expect(e.message).toEqual(`The ArcGIS API for JavaScript is already loaded.`); + .catch(err => { + expect(err.message).toEqual(`The ArcGIS API for JavaScript is already loaded.`); done(); }); }); afterAll(function () { // clean up removeRequire(); - removeScript(); }); }); describe('when called twice', function () { @@ -146,8 +155,8 @@ describe('esri-loader', function () { expect(script.getAttribute('src')).toEqual(jaspi3xUrl); done(); }) - .catch((e) => { - done.fail('second call to loadScript should not have failed' + e); + .catch(err => { + done.fail('second call to loadScript should not have failed with: ' + err); }); }) .catch(() => { @@ -169,8 +178,8 @@ describe('esri-loader', function () { expect(script.getAttribute('src')).toEqual(jaspi3xUrl); done(); }) - .catch((e) => { - done.fail('second call to loadScript should not have failed' + e); + .catch(err => { + done.fail('second call to loadScript should not have failed with: ' + err); }); }); }); @@ -189,8 +198,8 @@ describe('esri-loader', function () { .then(script => { done.fail('second call to loadScript should have failed'); }) - .catch((e) => { - expect(e.message).toEqual(`The ArcGIS API for JavaScript is already loaded (${jaspi3xUrl}).`); + .catch(err => { + expect(err.message).toEqual(`The ArcGIS API for JavaScript is already loaded (${jaspi3xUrl}).`); done(); }); }); @@ -203,6 +212,78 @@ describe('esri-loader', function () { }); }); + describe('when loading modules', function () { + var expectedModuleNames = ['esri/map', 'esri/layers/VectorTileLayer']; + describe('when script has been loaded', function() { + beforeEach(function () { + // stub window require + stubRequire(); + }); + it('should have registered an error handler', function (done) { + spyOn(window.require, 'on'); + esriLoader.loadModules(expectedModuleNames) + .then(() => { + expect(window.require.on.calls.argsFor(0)[0]).toEqual('error'); + done(); + }) + .catch(err => { + done.fail('call to loadScript should not have failed with: ' + err); + }); + }); + it('should call require w/ correct args', function (done) { + spyOn(window, 'require').and.callThrough(); + esriLoader.loadModules(expectedModuleNames) + .then(() => { + expect(window.require.calls.argsFor(0)[0]).toEqual(expectedModuleNames); + done(); + }) + .catch(err => { + done.fail('call to loadScript should not have failed with: ' + err); + }); + }); + afterEach(function () { + // clean up + removeRequire(); + }); + }); + describe('when the script has not yet been loaded', function() { + beforeEach(function() { + // uh oh, not sure why this is needed + // seems like some test above did not clean up after itself + // but I can't find where + // TODO: remove this line + removeRequire(); + // w/o it, test fails w/ + // TypeError: Cannot read property 'argsFor' of undefined + // b/c require is defined so it's not trying to add the script + // and doesn't enter the appendChild spyOn() block below + }); + it('should not reject', function (done) { + spyOn(document.body, 'appendChild').and.callFake(function (el) { + stubRequire(); + spyOn(window, 'require').and.callThrough(); + el.onload(); + }); + esriLoader.loadModules(expectedModuleNames, { + url: jaspi3xUrl + }) + .then(() => { + expect(window.require.calls.argsFor(0)[0]).toEqual(expectedModuleNames); + done(); + }) + .catch(err => { + done.fail('call to loadScript should not have failed with: ' + err); + }); + }); + afterEach(function () { + // clean up + removeRequire(); + removeScript(); + }); + }); + }); + + // TODO: remove the following suites once the APIs they test have been removed describe('when bootstraping the API', function () { describe('with defaults', function () { var scriptEl; @@ -328,14 +409,14 @@ describe('esri-loader', function () { }); }); - describe('when loading modules', function () { + describe('when loading modules w/ dojoRequire', function () { var expectedModuleNames = ['esri/map', 'esri/layers/VectorTileLayer']; var context = { requireCallback: function () {} }; var actualModuleNames; beforeAll(function () { - // mock window require + // stub window require window.require = function (names, callback) { actualModuleNames = names; callback(); diff --git a/test/mocks/jsapi3x.js b/test/mocks/jsapi3x.js index 2b173dd..2aa3875 100644 --- a/test/mocks/jsapi3x.js +++ b/test/mocks/jsapi3x.js @@ -1,7 +1,3 @@ -// stub require function -window.require = function (moduleNames, callback) { - if (callback) { - // call the callback w/ the modulenames that were passed in - callback.apply(this, moduleNames); - } -} +// this is defined in spec +console.log('3.x'); +stubRequire(); \ No newline at end of file diff --git a/test/mocks/jsapi4x.js b/test/mocks/jsapi4x.js index 2b173dd..79a56ed 100644 --- a/test/mocks/jsapi4x.js +++ b/test/mocks/jsapi4x.js @@ -1,7 +1,3 @@ -// stub require function -window.require = function (moduleNames, callback) { - if (callback) { - // call the callback w/ the modulenames that were passed in - callback.apply(this, moduleNames); - } -} +// this is defined in spec +console.log('4.x'); +stubRequire(); \ No newline at end of file From f81add0cbb299c876966f08a03ec2aeea1609824 Mon Sep 17 00:00:00 2001 From: Tom Wayson Date: Wed, 8 Nov 2017 15:34:18 -0800 Subject: [PATCH 3/9] refactor to shared functions and deprecate v1 API --- src/esri-loader.ts | 122 ++++++++++++++++++++++++--------------- test/esri-loader.spec.js | 36 +++++++++--- 2 files changed, 104 insertions(+), 54 deletions(-) diff --git a/src/esri-loader.ts b/src/esri-loader.ts index 0d24555..904d06e 100644 --- a/src/esri-loader.ts +++ b/src/esri-loader.ts @@ -11,6 +11,8 @@ limitations under the License. */ +const DEFAULT_URL = 'https://js.arcgis.com/4.5/'; + // get the script injected by this library function getScript() { return document.querySelector('script[data-esri-loader]') as HTMLScriptElement; @@ -22,9 +24,46 @@ function _isLoaded() { return typeof window['require'] !== 'undefined'; } +function createScript(url) { + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = url; + // TODO: remove this if no longer needed + script.dataset['esriLoader'] = 'loading'; + return script; +} + +// add a one-time load handler to script +function handleScriptLoad(script, callback) { + const onScriptLoad = () => { + // pass the script to the callback + callback(script); + // remove this event listener + script.removeEventListener('load', onScriptLoad, false); + }; + script.addEventListener('load', onScriptLoad, false); +} + +// add a one-time error handler to the script +function handleScriptError(script, callback) { + const onScriptError = (e) => { + // reject the promise and remove this event listener + callback(e.error || new Error(`There was an error attempting to load ${script.src}`)); + // remove this event listener + script.removeEventListener('error', onScriptError, false); + }; + script.addEventListener('error', onScriptError, false); +} + // interfaces -// TODO: rename to ILoadScriptOptions +// TODO: remove this next breaking change +// it has been replaced by ILoadScriptOptions export interface IBootstrapOptions { + url?: string; + dojoConfig?: { [propName: string]: any }; +} + +export interface ILoadScriptOptions { url?: string; // NOTE: stole the type definition for dojoConfig from: // https://github.com/nicksenger/esri-promise/blob/38834f22ffb3f70da3f57cce3773d168be990b0b/index.ts#L18 @@ -38,10 +77,10 @@ export function isLoaded() { } // load the ArcGIS API on the page -export function loadScript(options: IBootstrapOptions = {}): Promise { +export function loadScript(options: ILoadScriptOptions = {}): Promise { // default options if (!options.url) { - options.url = 'https://js.arcgis.com/4.5/'; + options.url = DEFAULT_URL; } return new Promise((resolve, reject) => { @@ -60,14 +99,9 @@ export function loadScript(options: IBootstrapOptions = {}): Promise { - // TODO: remove this event listener - resolve(script); - }, false); - script.addEventListener('error', (err) => { - // TODO: remove this event listener - reject(err); - }, false); + handleScriptLoad(script, resolve); + // handle script loading errors + handleScriptError(script, reject); } } } else { @@ -82,19 +116,18 @@ export function loadScript(options: IBootstrapOptions = {}): Promise { + // TODO: once we no longer need to update the dataset, replace this w/ + // handleScriptLoad(script, resolve); + handleScriptLoad(script, () => { // update the status of the script script.dataset['esriLoader'] = 'loaded'; // return the script resolve(script); - }; + }); // handle script loading errors - script.onerror = reject; + handleScriptError(script, reject); // load the script document.body.appendChild(script); } @@ -105,31 +138,33 @@ export function loadScript(options: IBootstrapOptions = {}): Promise { return new Promise((resolve, reject) => { - // If something goes wrong loading the esri/dojo scripts, reject with the error. - window['require'].on('error', reject); - window['require'](modules, (...args) => { - // Resolve with the parameters from dojo require as an array. - resolve(args); - }); + // If something goes wrong loading the esri/dojo scripts, reject with the error. + window['require'].on('error', reject); + window['require'](modules, (...args) => { + // Resolve with the parameters from dojo require as an array. + resolve(args); + }); }); } // returns a promise that resolves with an array of the required modules // also will attempt to lazy load the ArcGIS API if it has not already been loaded -export function loadModules(modules: string[], loadScriptOptions?: IBootstrapOptions): Promise { +export function loadModules(modules: string[], loadScriptOptions?: ILoadScriptOptions): Promise { if (!_isLoaded()) { - // script is not yet loaded, attept to load it + // script is not yet loaded, attept to load it then load the modules return loadScript(loadScriptOptions).then(() => requireModules(modules)); } else { + // script is already loaded, just load the modules return requireModules(modules); } } -// TODO: deprecate the following functions +// TODO: remove this next major release export function bootstrap(callback?: (error: Error, dojoRequire?: any) => void, options: IBootstrapOptions = {}) { + console.warn('bootstrap() has been depricated and will be removed the next major release. Use loadScript() instead.'); // default options if (!options.url) { - options.url = 'https://js.arcgis.com/4.5/'; + options.url = DEFAULT_URL; } // don't reload API if it is already loaded or in the process of loading @@ -146,10 +181,7 @@ export function bootstrap(callback?: (error: Error, dojoRequire?: any) => void, } // create a script object whose source points to the API - const script = document.createElement('script'); - script.type = 'text/javascript'; - script.src = options.url; - script.dataset['esriLoader'] = 'loading'; + const script = createScript(options.url); // once the script is loaded... script.onload = () => { @@ -167,22 +199,20 @@ export function bootstrap(callback?: (error: Error, dojoRequire?: any) => void, } }; - // handle any script loading errors - const onScriptError = (e) => { - if (callback) { - // pass the error to the callback - callback(e.error || new Error(`There was an error attempting to load ${script.src}`)); - } - // remove this event listener - script.removeEventListener('error', onScriptError, false); - }; - script.addEventListener('error', onScriptError, false); + if (callback) { + // handle any script loading errors + handleScriptError(script, callback); + } // load the script document.body.appendChild(script); } +// TODO: remove this next major release export function dojoRequire(modules: string[], callback: (...modules: any[]) => void) { + /* tslint:disable max-line-length */ + console.warn('dojoRequire() has been depricated and will be removed the next major release. Use loadModules() instead.'); + /* tslint:enable max-line-length */ if (isLoaded()) { // already loaded, just call require window['require'](modules, callback); @@ -191,11 +221,9 @@ export function dojoRequire(modules: string[], callback: (...modules: any[]) => const script = getScript(); if (script) { // Not yet loaded but script is in the body - use callback once loaded - const onScriptLoad = () => { + handleScriptLoad(script, () => { window['require'](modules, callback); - script.removeEventListener('load', onScriptLoad, false); - }; - script.addEventListener('load', onScriptLoad, false); + }); } else { // Not bootstrapped throw new Error('The ArcGIS API for JavaScript has not been loaded. You must first call esriLoader.bootstrap()'); @@ -207,7 +235,7 @@ export function dojoRequire(modules: string[], callback: (...modules: any[]) => export default { isLoaded, loadScript, - // TODO: deprecate + // TODO: remove these the next major release bootstrap, dojoRequire }; diff --git a/test/esri-loader.spec.js b/test/esri-loader.spec.js index f841bcf..4bd9021 100644 --- a/test/esri-loader.spec.js +++ b/test/esri-loader.spec.js @@ -53,8 +53,8 @@ describe('esri-loader', function () { var scriptEl; beforeAll(function (done) { spyOn(document.body, 'appendChild').and.callFake(function (el) { - // call the onload callback - el.onload(); + // trigger the onload event listeners + el.dispatchEvent(new Event('load')); }); esriLoader.loadScript() .then((script) => { @@ -74,8 +74,8 @@ describe('esri-loader', function () { var scriptEl; beforeAll(function (done) { spyOn(document.body, 'appendChild').and.callFake(function (el) { - // call the onload callback - el.onload(); + // trigger the onload event listeners + el.dispatchEvent(new Event('load')); }); esriLoader.loadScript({ url: 'https://js.arcgis.com/3.20' @@ -89,6 +89,9 @@ describe('esri-loader', function () { it('should load different version', function () { expect(scriptEl.src).toEqual('https://js.arcgis.com/3.20'); }); + it('should not have set dojoConfig', function () { + expect(window.dojoConfig).not.toBeDefined(); + }); }); describe('with dojoConfig option', function () { var dojoConfig = { @@ -102,8 +105,8 @@ describe('esri-loader', function () { }; beforeAll(function (done) { spyOn(document.body, 'appendChild').and.callFake(function (el) { - // call the onload callback - el.onload(); + // trigger the onload event listeners + el.dispatchEvent(new Event('load')); }); esriLoader.loadScript({ dojoConfig: dojoConfig @@ -140,6 +143,24 @@ describe('esri-loader', function () { removeRequire(); }); }); + describe('when loading an invalid url', function () { + it('should pass an error to the callback', function (done) { + esriLoader.loadScript({ + url: 'not a valid url' + }) + .then(script => { + done.fail('call to loadScript should have failed'); + }) + .catch(err => { + expect(err.message.indexOf('There was an error attempting to load')).toEqual(0); + done(); + }); + }); + afterAll(function () { + // clean up + removeScript(); + }); + }); describe('when called twice', function () { describe('when loading the same script', function () { it('should resolve the script if it is already loaded', function (done) { @@ -262,7 +283,8 @@ describe('esri-loader', function () { spyOn(document.body, 'appendChild').and.callFake(function (el) { stubRequire(); spyOn(window, 'require').and.callThrough(); - el.onload(); + // trigger the onload event listeners + el.dispatchEvent(new Event('load')); }); esriLoader.loadModules(expectedModuleNames, { url: jaspi3xUrl From 6e35ef755fe75d6f8ebdbc1b0bf466528d5f9433 Mon Sep 17 00:00:00 2001 From: Tom Wayson Date: Wed, 8 Nov 2017 23:27:18 -0800 Subject: [PATCH 4/9] updated README and CHANGELOG for new promise-based API --- CHANGELOG.md | 2 + README.md | 156 +++++++++++++++++++++++++++++---------------------- 2 files changed, 91 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9164b10..b45d69e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added +- add promise-based functions to load the script and modules ### Changed +- deprecate `bootstrap()` and `dojoRequire()` - add code coverage - add release script ### Fixed diff --git a/README.md b/README.md index ed36f6d..ccb3728 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # esri-loader -A tiny library to help load [ArcGIS API for JavaScript](https://developers.arcgis.com/javascript/) modules in non-Dojo applications. +A tiny library to help load modules from either the [4.x](https://developers.arcgis.com/javascript/) or [3.x](https://developers.arcgis.com/javascript/3/) versions of the [ArcGIS API for JavaScript](https://developers.arcgis.com/javascript/) in non-Dojo applications. See below for more information on [why this library is needed](#why-is-this-needed) and how it can help improve application load performance. @@ -20,7 +20,7 @@ npm install esri-loader ``` ## Usage -The code below shows how you can load the ArcGIS API for JavaScript and then create a map. Where you place this code in your application will depend on what framework you are using. See below for [example applications](#examples). +The code snippets below show how to load ArcGIS API for JavaScript modules use them to create a map. Where you would place similar code in your application will depend on which application framework you are using. See below for [example applications](#examples). ### Loading Styles @@ -38,103 +38,119 @@ If you're using a specific version other than the latest 4.x: @import url('https://js.arcgis.com/3.22/esri/css/esri.css'); ``` -### Pre-loading the ArcGIS API for JavaScript - -If you have good reason to believe that the user is going to transition to a map route, you may want to start pre-loading the ArcGIS API as soon as possible w/o blocking rendering, for example: - -```js -import * as esriLoader from 'esri-loader'; - -// preload the ArcGIS API -esriLoader.bootstrap((err) => { - if (err) { - // handle any loading errors - console.error(err); - } else { - // optional execute any code once it's preloaded - createMap(); - } -}, { - // use a specific version instead of latest 4.x - url: 'https://js.arcgis.com/3.22/' -}); -``` - -### Lazy Loading the ArcGIS API for JavaScript +### Loading Modules from the ArcGIS API for JavaScript -Alternatively, if users may never end up visiting any map routes, you can lazy load the ArcGIS API for JavaScript the first time a user visits a route with a map, for example: +Here's an example of how you could load and use the 4.x `Map` and `MapView` classes in a component to create a map (based on [this sample](https://developers.arcgis.com/javascript/latest/sample-code/sandbox/index.html?sample=webmap-basic)): ```js -// import the esri-loader library -import * as esriLoader from 'esri-loader'; - -// has the ArcGIS API been added to the page? -if (!esriLoader.isLoaded()) { - // no, lazy load it the ArcGIS API before using its classes - esriLoader.bootstrap((err) => { - if (err) { - console.error(err); - } else { - // once it's loaded, create the map - createMap(); +// first, we use Dojo's loader to require the map class +esriLoader.loadModules(['esri/views/MapView', 'esri/WebMap']) +.then(([MapView, WebMap]) => { + // then we load a web map from an id + var webmap = new WebMap({ + portalItem: { // autocasts as new PortalItem() + id: 'f2e9b762544945f390ca4ac3671cfa72' } - }, { - // use a specific version instead of latest 4.x - url: 'https://js.arcgis.com/3.22/' }); -} else { - // ArcGIS API is already loaded, just create the map - createMap(); -} + // and we show that map in a container w/ id #viewDiv + var view = new MapView({ + map: webmap, + container: 'viewDiv' + }); +}) +.catch(err => { + // handle any errors + console.error(err); +}); ``` -### Loading Modules from the ArcGIS API for JavaScript +#### Lazy Loading the ArcGIS API for JavaScript -Once you've loaded the API using one of the above methods, you can then load modules. Here's an example of how you could load and use the 3.x `Map` and `VectorTileLayer` classes in a component to create a map: +If users may never end up visiting any map routes, you can lazy load the ArcGIS API for JavaScript the first time a user visits a route with a map. + +In the above snippet, the first time `loadModules()` is called, it will attempt to lazy load the most recent 4.x version of the ArcGIS API by calling `loadScript()` for you if the API has not already been loaded. The snippet below uses version 3.x of the ArcGIS API to create a map. ```js -// create a map on the page -function createMap() { - // first, we use Dojo's loader to require the map class - esriLoader.dojoRequire(['esri/map'], (Map) => { - // create map with the given options at a DOM node w/ id 'mapNode' - let map = new Map('mapNode', { - center: [-118, 34.5], - zoom: 8, - basemap: 'dark-gray' - }); +// if the API hasn't already been loaded (i.e. the frist time this is run) +// loadModules() will call loadScript() and pass these options, which, +// in this case are only needed b/c we're using v3.x instead of the latest 4.x +const options = { + url: 'https://js.arcgis.com/3.22/' +}; +esriLoader.loadModules(['esri/map'], options) +.then(([Map]) => { + // create map with the given options at a DOM node w/ id 'mapNode' + let map = new Map('mapNode', { + center: [-118, 34.5], + zoom: 8, + basemap: 'dark-gray' }); -} +}) +.catch(err => { + // handle any script or module loading errors + console.error(err); +}); ``` -### Using your own script tag +### Pre-loading the ArcGIS API for JavaScript -It is possible to use this library only to load modules (i.e. not to pre-load or lazy load the ArcGIS API), then you will need to add a `data-esri-loader` attribute to the script tag you use to load the ArcGIS API for JavaScript. Example: +If you have good reason to believe that the user is going to transition to a map route, you may want to start pre-loading the ArcGIS API as soon as possible w/o blocking rendering, for example: -```html - - +```js +// preload the ArcGIS API +// NOTE: in this case, we're not passing any options to loadScript() +// so it will default to loading the latest 4.x version of the API from the CDN +this.loadScriptPromise = esriLoader.loadScript(); + +// later, for example once a component has been rendered, +// you can wait for the above promise to resolve (if it hasn't already) +this.loadScriptPromise +.then(() => { + // you can now load the map modules and create the map +}) +.catch(err => { + // handle any script loading errors + console.error(err); +}); ``` ### Configuring Dojo -You can pass a [`dojoConfig`](https://dojotoolkit.org/documentation/tutorials/1.10/dojo_config/) option to `bootstrap()` to configure Dojo before the script tag is loaded. This is useful if you want to use esri-loader to load Dojo packages that are not included in the ArcGIS API for JavaScript such as [FlareClusterLayer](https://github.com/nickcam/FlareClusterLayer). +You can pass a [`dojoConfig`](https://dojotoolkit.org/documentation/tutorials/1.10/dojo_config/) option to `loadScript()` or `loadModules()` to configure Dojo before the script tag is loaded. This is useful if you want to use esri-loader to load Dojo packages that are not included in the ArcGIS API for JavaScript such as [FlareClusterLayer](https://github.com/nickcam/FlareClusterLayer). ```js -esriLoader.bootstrap(callbackFn, { +// in this case options are only needed so we can configure dojo before loading the API +const options = { // tell Dojo where to load other packages dojoConfig: { async: true, packages: [ { - location: 'path/to/somelib', - name: 'somelib' + location: '/path/to/fcl', + name: 'fcl' } ] } +}; +esriLoader.loadModules(['esri/map', 'fcl/FlareClusterLayer_v3'], options) +.then(([Map, FlareClusterLayer]) => { + // you can now create a new FlareClusterLayer and add it to a new Map +}) +.catch(err => { + // handle any errors + console.error(err); }); ``` +### Using your own script tag + +It is possible to use this library only to load modules (i.e. not to pre-load or lazy load the ArcGIS API), then you will need to add a `data-esri-loader` attribute to the script tag you use to load the ArcGIS API for JavaScript. Example: + +```html + + +``` + ## Why is this needed? Unfortunately, you can't simply `npm install` the ArcGIS API and then `import` ArcGIS modules directly from the modules in a non-Dojo application. The only reliable way to load ArcGIS API for JavaScript modules is using Dojo's AMD loader. When using the ArcGIS API in an application built with another framework, you typically want to use the tooling and conventions of that framework instead of the Dojo build system. This library lets you do that by providing a module that you can `import` and use to dynamically inject an ArcGIS API script tag in the page and then use its Dojo loader to load only the ArcGIS API modules as needed. @@ -172,6 +188,12 @@ Here are some applications that use this library (presented by framework in alph - [CreateMap](https://github.com/oppoudel/CreateMap) - Create Map: City of Baltimore - https://gis.baltimorecity.gov/createmap/#/ - [City of Baltimore: Map Gallery](https://github.com/oppoudel/MapGallery_Vue) - Map Gallery built with Vue.js that uses this library to load the ArcGIS API +## Dependencies + +This library doesn't have any external dependencies, but it expects to be run in a browser (i.e. not Node.js). Since v1.5 asynchronous functions like `loadScript()` and `loadModules()` return [`Promise`s](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), so if your application has to support [browers that don't support Promise (i.e. IE)](https://caniuse.com/#search=promise), then you should consider using a [Promise polyfill](https://www.google.com/search?q=promise+polyfill), ideally [only when needed](https://philipwalton.com/articles/loading-polyfills-only-when-needed/). + +Alternatively, you can still use `bootstrap()` and `dojoRequire()` which are the callback-based equivalents of the above functions. See the [v1.4.0 documentation](https://github.com/Esri/esri-loader/blob/v1.4.0/README.md#usage) for how to use the callback-based API, but _keep in mind that these functions have been deprecated and will be removed at the next major release_. + ## Issues Find a bug or want to request a new feature? Please let us know by [submitting an issue](https://github.com/Esri/esri-loader/issues/). From af6528b42283760a3d87583320aaa6ca7063a4e8 Mon Sep 17 00:00:00 2001 From: Tom Wayson Date: Thu, 9 Nov 2017 14:55:33 -0800 Subject: [PATCH 5/9] remove loadModules error handler once succesfully loaded --- src/esri-loader.ts | 4 +++- test/esri-loader.spec.js | 9 ++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/esri-loader.ts b/src/esri-loader.ts index 904d06e..33ecaf6 100644 --- a/src/esri-loader.ts +++ b/src/esri-loader.ts @@ -139,8 +139,10 @@ export function loadScript(options: ILoadScriptOptions = {}): Promise { return new Promise((resolve, reject) => { // If something goes wrong loading the esri/dojo scripts, reject with the error. - window['require'].on('error', reject); + const errorHandler = window['require'].on('error', reject); window['require'](modules, (...args) => { + // remove error handler + errorHandler.remove(); // Resolve with the parameters from dojo require as an array. resolve(args); }); diff --git a/test/esri-loader.spec.js b/test/esri-loader.spec.js index 4bd9021..586b9bf 100644 --- a/test/esri-loader.spec.js +++ b/test/esri-loader.spec.js @@ -8,10 +8,9 @@ function stubRequire() { } } window.require.on = function(name, callback) { - // if (callback) { - // // call the callback w/ the event name that was passed in - // callback(name); - // } + return { + remove: function() {} + } } } // remove script tags added by esri-loader @@ -241,7 +240,7 @@ describe('esri-loader', function () { stubRequire(); }); it('should have registered an error handler', function (done) { - spyOn(window.require, 'on'); + spyOn(window.require, 'on').and.callThrough(); esriLoader.loadModules(expectedModuleNames) .then(() => { expect(window.require.on.calls.argsFor(0)[0]).toEqual('error'); From 4ebb65d6e4701ac94d5ef2302138968454d6e5c2 Mon Sep 17 00:00:00 2001 From: Tom Wayson Date: Thu, 9 Nov 2017 15:13:28 -0800 Subject: [PATCH 6/9] _isLoaded() uses a more strict test for Dojo's require() --- src/esri-loader.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/esri-loader.ts b/src/esri-loader.ts index 33ecaf6..e003c88 100644 --- a/src/esri-loader.ts +++ b/src/esri-loader.ts @@ -20,8 +20,9 @@ function getScript() { // TODO: at next breaking change replace the public isLoaded() API with this function _isLoaded() { - // TODO: instead of checking that require is defined, should this check if it is a function? - return typeof window['require'] !== 'undefined'; + const globalRequire = window['require']; + // .on() ensures that it's Dojo's AMD loader + return globalRequire && globalRequire.on; } function createScript(url) { @@ -73,7 +74,8 @@ export interface ILoadScriptOptions { // has ArcGIS API been loaded on the page yet? export function isLoaded() { - return _isLoaded() && getScript(); + // TODO: replace this implementation with that of _isLoaded() on next major release + return typeof window['require'] !== 'undefined' && getScript(); } // load the ArcGIS API on the page From 2023d6ccc0743d9c637caf4dd7c80c6fda2c43df Mon Sep 17 00:00:00 2001 From: Tom Wayson Date: Thu, 9 Nov 2017 15:34:26 -0800 Subject: [PATCH 7/9] remove loadScript() error handlers when script successfully loads --- src/esri-loader.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/esri-loader.ts b/src/esri-loader.ts index e003c88..73ede8f 100644 --- a/src/esri-loader.ts +++ b/src/esri-loader.ts @@ -35,12 +35,22 @@ function createScript(url) { } // add a one-time load handler to script -function handleScriptLoad(script, callback) { +// and optionally add a one time error handler as well +function handleScriptLoad(script, callback, errback?) { + let onScriptError; + if (errback) { + // set up an error handler as well + onScriptError = handleScriptError(script, errback); + } const onScriptLoad = () => { // pass the script to the callback callback(script); // remove this event listener script.removeEventListener('load', onScriptLoad, false); + if (onScriptError) { + // remove the error listener as well + script.removeEventListener('error', onScriptError, false); + } }; script.addEventListener('load', onScriptLoad, false); } @@ -54,6 +64,7 @@ function handleScriptError(script, callback) { script.removeEventListener('error', onScriptError, false); }; script.addEventListener('error', onScriptError, false); + return onScriptError; } // interfaces @@ -101,9 +112,7 @@ export function loadScript(options: ILoadScriptOptions = {}): Promise { // update the status of the script script.dataset['esriLoader'] = 'loaded'; // return the script resolve(script); - }); - // handle script loading errors - handleScriptError(script, reject); + }, reject); // load the script document.body.appendChild(script); } From dc88d9a07499099edbcbe4579d6bda053809416a Mon Sep 17 00:00:00 2001 From: Tom Wayson Date: Thu, 9 Nov 2017 16:09:40 -0800 Subject: [PATCH 8/9] oops, forgot to include `loadModules` in the default export --- src/esri-loader.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/esri-loader.ts b/src/esri-loader.ts index 73ede8f..cca3e6f 100644 --- a/src/esri-loader.ts +++ b/src/esri-loader.ts @@ -246,6 +246,7 @@ export function dojoRequire(modules: string[], callback: (...modules: any[]) => export default { isLoaded, loadScript, + loadModules, // TODO: remove these the next major release bootstrap, dojoRequire From a62b40b4838403a3d5c82da81f46af4e8fe48161 Mon Sep 17 00:00:00 2001 From: Tom Wayson Date: Thu, 9 Nov 2017 21:55:02 -0800 Subject: [PATCH 9/9] allow consumers to override the Promise implementation --- README.md | 16 ++++++++++++++-- src/esri-loader.ts | 10 ++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ccb3728..4426c0e 100644 --- a/README.md +++ b/README.md @@ -190,9 +190,21 @@ Here are some applications that use this library (presented by framework in alph ## Dependencies -This library doesn't have any external dependencies, but it expects to be run in a browser (i.e. not Node.js). Since v1.5 asynchronous functions like `loadScript()` and `loadModules()` return [`Promise`s](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), so if your application has to support [browers that don't support Promise (i.e. IE)](https://caniuse.com/#search=promise), then you should consider using a [Promise polyfill](https://www.google.com/search?q=promise+polyfill), ideally [only when needed](https://philipwalton.com/articles/loading-polyfills-only-when-needed/). +This library doesn't have any external dependencies, but it expects to be run in a browser (i.e. not Node.js). Since v1.5 asynchronous functions like `loadScript()` and `loadModules()` return [`Promise`s](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), so if your application has to support [browers that don't support Promise (i.e. IE)](https://caniuse.com/#search=promise) you have a few options. -Alternatively, you can still use `bootstrap()` and `dojoRequire()` which are the callback-based equivalents of the above functions. See the [v1.4.0 documentation](https://github.com/Esri/esri-loader/blob/v1.4.0/README.md#usage) for how to use the callback-based API, but _keep in mind that these functions have been deprecated and will be removed at the next major release_. +If there's already a Promise implementation loaded on the page you can configure esri-loader to use that implementation. For example, in [ember-esri-loader](https://github.com/Esri/ember-esri-loader), we configure esri-loader to use the RSVP Promise implementation included with Ember.js. + +```js + init () { + this._super(...arguments); + // have esriLoader use Ember's RSVP promise + esriLoader.utils.Promise = Ember.RSVP.Promise; + }, +``` + +Otherwise, you should consider using a [Promise polyfill](https://www.google.com/search?q=promise+polyfill), ideally [only when needed](https://philipwalton.com/articles/loading-polyfills-only-when-needed/). + +Finally, for now you can still use `bootstrap()` and `dojoRequire()` which are the callback-based equivalents of the above functions. See the [v1.4.0 documentation](https://github.com/Esri/esri-loader/blob/v1.4.0/README.md#usage) for how to use the callback-based API, but _keep in mind that these functions have been deprecated and will be removed at the next major release_. ## Issues diff --git a/src/esri-loader.ts b/src/esri-loader.ts index cca3e6f..7a1a975 100644 --- a/src/esri-loader.ts +++ b/src/esri-loader.ts @@ -75,6 +75,11 @@ export interface IBootstrapOptions { dojoConfig?: { [propName: string]: any }; } +// allow consuming libraries to provide their own Promise implementations +export const utils = { + Promise: window['Promise'] +}; + export interface ILoadScriptOptions { url?: string; // NOTE: stole the type definition for dojoConfig from: @@ -96,7 +101,7 @@ export function loadScript(options: ILoadScriptOptions = {}): Promise { + return new utils.Promise((resolve, reject) => { let script = getScript(); if (script) { // the API is already loaded or in the process of loading... @@ -146,7 +151,7 @@ export function loadScript(options: ILoadScriptOptions = {}): Promise { - return new Promise((resolve, reject) => { + return new utils.Promise((resolve, reject) => { // If something goes wrong loading the esri/dojo scripts, reject with the error. const errorHandler = window['require'].on('error', reject); window['require'](modules, (...args) => { @@ -247,6 +252,7 @@ export default { isLoaded, loadScript, loadModules, + utils, // TODO: remove these the next major release bootstrap, dojoRequire