Skip to content

Commit

Permalink
Merge pull request #161 from Esri/chore/beforeCss
Browse files Browse the repository at this point in the history
insertCssBefore option to insert esri css before any node using a selector
  • Loading branch information
tomwayson committed Mar 25, 2019
2 parents da609d8 + 3f33180 commit 410562c
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 92 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Expand Up @@ -4,7 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- option to insert CSS link before an existing link tag
- `insertCssBefore` option to insert CSS link before an existing element
### Changed
### Fixed
### Removed
Expand Down
156 changes: 98 additions & 58 deletions README.md
Expand Up @@ -23,15 +23,16 @@ See the [Examples](#examples) section below for links to applications that use t
- [Table of contents](#table-of-contents)
- [Install](#install)
- [Usage](#usage)
- [Loading Styles](#loading-styles)
- [Loading Modules from the ArcGIS API for JavaScript](#loading-modules-from-the-arcgis-api-for-javascript)
- [Lazy Loading the ArcGIS API for JavaScript](#lazy-loading-the-arcgis-api-for-javascript)
- [Loading Styles](#loading-styles)
- [Why is this needed?](#why-is-this-needed)
- [Examples](#examples)
- [Advanced Usage](#advanced-usage)
- [Overriding ArcGIS Styles](#overriding-arcgis-styles)
- [Configuring Dojo](#configuring-dojo)
- [Pre-loading the ArcGIS API for JavaScript](#pre-loading-the-arcgis-api-for-javascript)
- [Isomorphic/universal applications](#isomorphicuniversal-applications)
- [Configuring Dojo](#configuring-dojo)
- [Using your own script tag](#using-your-own-script-tag)
- [ArcGIS Types](#arcgis-types)
- [Using the esriLoader Global](#using-the-esriloader-global)
Expand Down Expand Up @@ -61,30 +62,6 @@ yarn add esri-loader

The code snippets below show how to load the ArcGIS API and its modules and then 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

Before you can use the ArcGIS API in your app, you'll need to load the styles that correspond to the version you are using. You can use the provided `loadCss(url)` function. For example:

```js
// load esri styles for version 4.x using loadCss
import { loadCss } from 'esri-loader';
loadCss('https://js.arcgis.com/4.10/esri/css/main.css');
```

Alternatively, you can manually load them by more traditional means such as adding `<link>` tags to your HTML, or `@import` statements to your CSS. For example:

```html
<!-- load esri styles for version 4.x via an html link tag -->
<link rel="stylesheet" href="https://js.arcgis.com/4.10/esri/css/main.css">
```

or:

```css
/* load esri styles for version 3.x via css import */
@import url('https://js.arcgis.com/3.27/esri/css/esri.css');
```

### Loading Modules from the ArcGIS API for JavaScript

#### From the Latest Version
Expand Down Expand Up @@ -150,6 +127,48 @@ Lazy loading the ArcGIS API can dramatically improve the initial load performanc

See the [Advanced Usage](#advanced-usage) section below for more advanced techniques such as [pre-loading the ArcGIS API](#pre-loading-the-arcgis-api-for-javascript), [using in isomorphic/universal applications](#isomorphicuniversal-applications), [configuring Dojo](#configuring-dojo), and more.

### Loading Styles

Before you can use the ArcGIS API in your app, you'll need to load the styles that correspond to the version you are using. Just like the ArcGIS API modules, you'll probably want to [lazy load](#lazy-loading-the-arcgis-api-for-javascript) the styles only once they are needed by the application. The easiest way to do that is to pass the `css` option to `loadModules()`:

```js
import { loadModules } from 'esri-loader';

// lazy-load the CSS before loading the modules
const options = {
css: 'https://js.arcgis.com/4.10/esri/css/main.css'
};

loadModules(['esri/views/MapView', 'esri/WebMap'], options)
.then(([MapView, WebMap]) => {
// you can safely create a map now that the styles are loaded
});
```

Alternatively, you can use the provided `loadCss(url)` function to control when the ArcGIS styles are loaded. For example:

```js
// load esri styles for version 4.x using loadCss
import { loadCss } from 'esri-loader';
loadCss('https://js.arcgis.com/4.10/esri/css/main.css');
```

See below for information on how to [override ArcGIS styles](#override-arcgis-styles) that you've lazy-loaded with `loadModules()` or `loadCss()`.

Finally, you can manually load them by more traditional means such as adding `<link>` tags to your HTML, or `@import` statements to your CSS. For example:

```html
<!-- load esri styles for version 4.x via an html link tag -->
<link rel="stylesheet" href="https://js.arcgis.com/4.10/esri/css/main.css">
```

or:

```css
/* load esri styles for version 3.x via css import */
@import url('https://js.arcgis.com/3.27/esri/css/esri.css');
```

## Why is this needed?

Unfortunately, you can't simply `npm install` the ArcGIS API and then `import` ArcGIS modules directly from other modules in a non-Dojo application. The only reliable way to load ArcGIS API for JavaScript modules is using Dojo's AMD loader. However, when using the ArcGIS API in an application built with another framework, you typically want to use the tooling and conventions of that framework rather than the Dojo build system. This library lets you do that by providing an ES 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.
Expand Down Expand Up @@ -247,6 +266,58 @@ See the [examples over at ember-esri-loader](https://github.com/Esri/ember-esri-

## Advanced Usage

### Configuring Dojo

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
import { loadModules } from 'esri-loader';

// 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/fcl',
name: 'fcl'
}
]
}
};

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);
});
```

### Overriding ArcGIS Styles

If you want to override ArcGIS styles that you have lazy-loaded using `loadModules()` or `loadCss()`, you may need to insert the ArcGIS styles into the document _above_ your custom styles in order to ensure the [rules of CSS precedence](https://css-tricks.com/precedence-css-order-css-matters/) are applied correctly. For this reason, `loadCss()` accepts a [selector](https://developer.mozilla.org/en-US/docs/Web/API/Document_object_model/Locating_DOM_elements_using_selectors#Selectors) (string) as optional second argument that will be used to query the DOM node (i.e. `<link>` or `<script>`) that contains your custom styles and it will insert the ArcGIS styles above that node. You can also pass that selector as the `insertCssBefore` option to `loadModules()`:

```js
import { loadModules } from 'esri-loader';

// lazy-load the CSS before loading the modules
const options = {
css: 'https://js.arcgis.com/4.10/esri/css/main.css',
// insert the stylesheet link above the first <style> tag on the page
insertCssBefore: 'style'
};

// before loading the modules, this will call:
// loadCss('https://js.arcgis.com/4.10/esri/css/main.css', 'style')
loadModules(['esri/views/MapView', 'esri/WebMap'], options);
```

Alternatively you could insert it before the first `<link>` tag w/ `insertCssBefore: 'link[rel="stylesheet"]'`, etc.

### 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:
Expand Down Expand Up @@ -288,40 +359,9 @@ if (typeof window !== 'undefined') {
}
```

### Configuring Dojo

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
import { loadModules } from 'esri-loader';

// 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/fcl',
name: 'fcl'
}
]
}
};

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). In this case you will need to add a `data-esri-loader` attribute to the script tag you use to load the ArcGIS API for JavaScript. Example:
It is possible to use this library only to load modules (i.e. not to lazy-load or pre-load the ArcGIS API). In this case 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
<!-- index.html -->
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -21,7 +21,7 @@
"precompile": "npm run lint",
"prepublish": "npm run build:release",
"preversion": "npm run test && git add README.md CHANGELOG.md",
"start": "npm run clean && npm run build && concurrently \"onchange 'src/esri-loader.ts' -- npm run build\" \"karma start\"",
"start": "npm run clean && npm run build && concurrently \"onchange 'src/**/*.ts' -- npm run build\" \"karma start\"",
"test": "npm run build:release && karma start --single-run=true --browsers Firefox"
},
"repository": {
Expand Down
17 changes: 8 additions & 9 deletions src/esri-loader.ts
Expand Up @@ -10,7 +10,7 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
import { ILoadCssOptions, loadCss } from './utils/css';
import { loadCss } from './utils/css';

const isBrowser = typeof window !== 'undefined';
const DEFAULT_URL = 'https://js.arcgis.com/4.10/';
Expand Down Expand Up @@ -61,8 +61,9 @@ function handleScriptError(script, callback) {
// interfaces
export interface ILoadScriptOptions {
url?: string;
css?: string | ILoadCssOptions;
css?: string;
dojoConfig?: { [propName: string]: any };
insertCssBefore?: string;
}

// allow consuming libraries to provide their own Promise implementations
Expand All @@ -84,9 +85,7 @@ export function isLoaded() {
// load the ArcGIS API on the page
export function loadScript(options: ILoadScriptOptions = {}): Promise<HTMLScriptElement> {
// default options
if (!options.url) {
options.url = DEFAULT_URL;
}
const url = options.url || DEFAULT_URL;

return new utils.Promise((resolve, reject) => {
let script = getScript();
Expand All @@ -95,7 +94,7 @@ export function loadScript(options: ILoadScriptOptions = {}): Promise<HTMLScript
// 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) {
if (src !== url) {
// potentially trying to load a different version of the API
reject(new Error(`The ArcGIS API for JavaScript is already loaded (${src}).`));
} else {
Expand All @@ -116,15 +115,15 @@ export function loadScript(options: ILoadScriptOptions = {}): Promise<HTMLScript
// this is the first time attempting to load the API
if (options.css) {
// load the css before loading the script
loadCss(options.css);
loadCss(options.css, options.insertCssBefore);
}
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 = createScript(options.url);
_currentUrl = options.url;
script = createScript(url);
_currentUrl = url;
// once the script is loaded...
handleScriptLoad(script, () => {
// update the status of the script
Expand Down
26 changes: 9 additions & 17 deletions src/utils/css.ts
@@ -1,18 +1,16 @@

function createStylesheetLink(url) {
function createStylesheetLink(href: string): HTMLLinkElement {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
link.href = href;
return link;
}

function insertLink(link, before?) {
// if we need to insert before an existing link, get all link tags
const allLinks = (before || before === 0) && document.getElementsByTagName('link');
if (allLinks) {
// insert the link before the link tag
const beforeLink = allLinks[before];
beforeLink.parentNode.insertBefore(link, beforeLink);
function insertLink(link: HTMLLinkElement, before?: string) {
if (before) {
// the link should be inserted before a specific node
const beforeNode = document.querySelector(before);
beforeNode.parentNode.insertBefore(link, beforeNode);
} else {
// append the link to then end of the head tag
document.head.appendChild(link);
Expand All @@ -24,19 +22,13 @@ function getCss(url) {
return document.querySelector(`link[href*="${url}"]`) as HTMLLinkElement;
}

export interface ILoadCssOptions {
url: string;
before?: number;
}

// lazy load the CSS needed for the ArcGIS API
export function loadCss(css: string | ILoadCssOptions) {
const url = typeof css === 'string' ? css : css.url;
export function loadCss(url: string, before?: string) {
let link = getCss(url);
if (!link) {
// create & load the css link
link = createStylesheetLink(url);
insertLink(link, (css as ILoadCssOptions).before);
insertLink(link, before);
}
return link;
}
19 changes: 13 additions & 6 deletions test/esri-loader.spec.js
Expand Up @@ -74,22 +74,29 @@ describe('esri-loader', function () {
});
});
});
describe('when inserting before an existing link', function () {
describe('when inserting before an existing node', function () {
var url = 'https://js.arcgis.com/4.10/esri/css/main.css';
// insert before the first <style> tag
var before = 'style';
var link;
var mockBeforeLink = {
parentNode: {
insertBefore: function (node, beforeNode) {}
}
}
beforeAll(function () {
// spyOn(document, 'querySelector');
spyOn(document, 'getElementsByTagName').and.returnValue([mockBeforeLink]);
spyOn(document, 'querySelector').and.callFake(function (selector) {
if (selector === before) {
return mockBeforeLink;
} else {
return null;
}
});
spyOn(mockBeforeLink.parentNode, 'insertBefore');
link = esriLoader.loadCss({url, before: 0});
link = esriLoader.loadCss(url, before);
});
it('should have queried all the links', function () {
expect(document.getElementsByTagName.calls.argsFor(0)[0]).toEqual(`link`);
it('should have queried for the selector', function () {
expect(document.querySelector.calls.argsFor(1)[0]).toEqual(before);
});
it('should have inserted before the mock node', function () {
expect(mockBeforeLink.parentNode.insertBefore.calls.argsFor(0)[0]).toEqual(link);
Expand Down

0 comments on commit 410562c

Please sign in to comment.