Dynamically load ArcGIS online modeule - javascript

It is possible to dynamically decide which models to require()?
It takes a while to load a bunch of widgets, using the JavaScript require() function, so I want to be able to only include the ones I'm using in any given instance to the ones I'm using.
Currently, I do this:
var reqs="esri/Map", "esri/WebMap", ... "esri/widgets/Legend";
require(reqs, function(Map, WebMap, ... ,Legend,) {
...
It's easy enough to fill the reqs array, but how can I vary the names in the function?

Not sure in which situation you would want to dynamically change the imports at runtime as the code where they are used is most likely static. Do you mean changing the list of imports?
You can always change this
require(
["esri/Map", "esri/WebMap", "esri/widgets/Legend"],
function(Map, WebMap, Legend) {
// Source code using Map, WebMap and Legend
});
to the list of dependencies you need:
require(
["esri/Map", "esri/SceneView", "esri/layers/KMLLayer"],
function(Map, WebMap, KMLLayer) {
// Source code using Map, SceneView and KMLLayer
});
See the ArcGIS API for JavaScript samples that all have different require() statements.
If you meant lazy-loading to reduce initial startup time, there is a nice modular example using Webpack and TypeScript:
https://github.com/Esri/jsapi-resources/tree/master/4.x/webpack/demo
It imports/requires all ArcGIS API for JavaScript classes after the main UI is finished loading. This is essentially done by calling import / require inside a method:
https://github.com/Esri/jsapi-resources/blob/master/4.x/webpack/demo/src/widgets/App.tsx#L53

Came up with a solution. Not as elegant as one would like (i.e. having to spec the class names directly), but it works.
var reqs="esri/Map", "esri/WebMap", ... "esri/widgets/Legend";
require(app.reqs, function()
var Map, WebMap, MapView, SceneView, KMLLayer. ... Legend;
for (i=0;i<app.reqs.length;++i) {
key=app.reqs[i].match(/([^\/]+)$/i)[1];
if (key == "Map") Map=arguments[i];
else if (key == "WebMap") WebMap=arguments[i];
...
else if (key == "Legend") Legend=arguments[i];
}

Related

Leaflet conditionalLayer plugin map variable is undefined

I'm moving a project to webpack, splitting code into modules, but at some places i bump into a situation where plugin or some other code expects map to be a visible global variable. The following exception are thrown because map is not exposed to it:
onRemove: function() {
this._removeMarkers();
this.onMap = false;
map.off("moveend", this._update);
},
Uncaught ReferenceError: map is not defined
at NewClass.onRemove (leaflet.conditionalLayer.js:77)
at NewClass.removeLayer (Layer.js:185)
at NewClass._onInputClick (Control.Layers.js:378)
at HTMLInputElement.handler (DomEvent.js:79)
Currently i create the map variable in the bundle file (map.js in my case) which i load with the page, and import it in other modules like this
import { map } from './map.js';
All the leaflet code and leaflet plugins are imported as scripts without webpack. How can I share a map instance across all the scripts just like as it would be created without a module?
For now i worked around this problem and declared map as window.map in a module and it works, but it seems there is more convenient way of importing instead of using window object.
Looks like a bug in leaflet-conditionalLayer plugin.
Its onRemove method should either:
Accept a map argument, as shown in Leaflet tutorial for extending Controls:
onRemove: function(map) {
// (do something)
}
Or use this._map (instead of map) as suggested in (your?) issue https://github.com/Eclipse1979/leaflet-conditionalLayer/issues/6
The good news is that Leaflet class system should enable you to easily patch that plugin, without having to wait for the plugin author to fix this bug:
L.ConditionalMarkers.include({
onRemove: function (map) {
this._removeMarkers();
this.onMap = false;
map.off("moveend", this._update);
}
});

JavaScript intercept module import

I have a SPA (in Aurelia / TypeScript but that should not matter) which uses SystemJS. Let's say it runs at http://spa:5000/app.
It sometimes loads JavaScript modules like waterservice/external.js on demand from an external URL like http://otherhost:5002/fetchmodule?moduleId=waterservice.external.js. I use SystemJS.import(url) to do this and it works fine.
But when this external module wants to import another module with a simple import { OtherClass } from './other-class'; this (comprehensiblely) does not work. When loaded by the SPA it looks at http://spa:5000/app/other-class.js. In this case I have to intercept the path/location to redirect it to http://otherhost:5002/fetchmodule?moduleId=other-class.js.
Note: The Typescript compilation for waterservice/external.ts works find because the typescript compiler can find ./other-class.ts easily. Obviously I cannot use an absolute URL for the import.
How can I intercept the module loading inside a module I am importing with SystemJS?
One approach I already tested is to add a mapping in the SystemJS configuration. If I import it like import { OtherClass } from 'other-class'; and add a mapping like "other-class": "http://otherhost:5002/fetchmodule?moduleId=other-class" it works. But if this approach is good, how can I add mapping dynamically at runtime?
Other approaches like a generic load url interception are welcome too.
Update
I tried to intercept SystemJS as suggest by artem like this
var systemLoader = SystemJS;
var defaultNormalize = systemLoader.normalize;
systemLoader.normalize = function(name, parentName) {
console.error("Intercepting", name, parentName);
return defaultNormalize(name, parentName);
}
This would normally not change anything but produce some console output to see what is going on. Unfortunately this seems to do change something as I get an error Uncaught (in promise) TypeError: this.has is not a function inside system.js.
Then I tried to add mappings with SystemJS.config({map: ...});. Surprisingly this function works incremental, so when I call it, it does not loose the already provided mappings. So I can do:
System.config({map: {
"other-class": `http://otherhost:5002/fetchModule?moduleId=other-class.js`
}});
This does not work with relative paths (those which start with . or ..) but if I put the shared ones in the root this works out.
I would still prefer to intercept the loading to be able to handle more scenarios but at the moment I have no idea which has function is missing in the above approach.
how can I add mapping dynamically at runtime?
AFAIK SystemJS can be configured at any time just by calling
SystemJS.config({ map: { additional-mappings-here ... }});
If it does not work for you, you can override loader.normalize and add your own mapping from module ids to URLs there. Something along these lines:
// assuming you have one global SystemJS instance
var loader = SystemJS;
var defaultNormalize = loader.normalize;
loader.normalize = function(name, parentName) {
if (parentName == 'your-external-module' && name == 'your-external-submodule') {
return Promise.resolve('your-submodule-url');
} else {
return defaultNormalize.call(loader, name, parentName);
}
}
I have no idea if this will work with typescript or not. Also, you will have to figure out what names exactly are passed to loader.normalize in your case.
Also, if you use systemjs builder to bundle your code, you will need to add that override to the loader used by builder (and that's whole another story).

How can I make requirejs polymorphically load a module?

I am currently developing a single page application using Cordova 3.4.0, Requirejs and Backbone. While porting my application from iPhone to iPad, I need to change some functions in some views and keep other parts intact.
To keep the change minimal, my solution is to create new object for each view I need to change, inherit all properties from original view and override only necessary functions.
To do so, I need to configure Requirejs so that in iPad, if I require, for instance, 'user/view/edit-profile.js', it will check whether there was a 'user/ipad/view/edit-profile.js' file, if there is one, requires it, otherwise require 'user/view/edit-profile.js'.
I have tried i18n, but it is not right for this situation. I am coming up with an idea of creating a new plugin for requirejs to do the task.
Does anyone have any suggestion for my problem?
Btw, Since the required file changes dynamically according to the platform. I call it polymorphism.
You could use path fallbacks:
paths: {
"user/view/edit-profile": ["user/ipad/view/edit-profile", "user/view/edit-profile"]
}
The above will make RequireJS try to load the ipad variant first. If as you develop your application you end up with logic to complex for fallbacks, you can use errbacks:
function onload(module) {
// Whatever you want to do...
};
require([module_a], onload, function (err) {
require([module_b], onload);
});
The code above will try to load a module from module_a and then from module_b. I use this kind of code to load modules with names that are computed at run time.
Since every module is a Backbone View, I come up with a solution, which will override extend function of Backbone View to return modified object depending on the existence of ipad, iphone or android dependencies.
The solution will require that if a base view have iPad version, it have to declare the iPad versions at dependencies, the extend function will extend existing view by the iPad view so that the iPad view can inherit all properties of base view and override only necessary functions.
I name the view PolyplatformView. Each view need to declare its ID, for example: user/view/edit-profile
Backbone.PolyplatformView = Backbone.View.extend({
});
Backbone.PolyplatformView.extend = function() {
if(arguments[0].moduleId) {
var extendPropsPath = arguments[0].moduleId.replace('/', '/' + constants.platform_name + '/');
// turn user/view/edit-profile -> user/ipad/view/edit-profile
if(require.defined(extendPropsPath)) {
_.extend(arguments[0], require(extendPropsPath));
}
} else {
console.warn('No module Id in polyplatform view -> cannot extend its parent');
}
var extended = Backbone.View.extend.apply(this, arguments);
return extended;
}

Cleaning up a large piece of Javascript code

I am writing an application in Titanium for Android. I have a lot of code in a single JS file. I would like to know if there is any function like php's include to break the code into multiple files and then just include them.
Thanks
Use the CommonJS / RequireJS approach, specifically the require command. This is the (strongly) recommended way for dealing with large systems in Titanium, and is well documented on their website, along with many best practices for dealing with JavaScript modularization specific to Titanium. Here is the documentation from Titanium on this.
For example, to create a module that encapsulates a 'view' of some kind, put it in a file named MyCustomView.js with this content:
// MyCustomView.js
function MyCustomView(message) {
var self = Ti.UI.createView({
backgroundColor : 'red'
});
var label = Ti.UI.createLabel({
text : message,
top : 15,
.... // Other initialization
});
// ... Other initialization for your custom view, event listeners etc.
return self;
}
module.exports = MyCustomView;
Now you can easily use this module in another class, lets assume you put this in your /Resources folder, lets load the module inside app.js and add it to the main window.
// app.js
var MyCustomView = require('MyCustomView');
var myView = new MyCustomView('A message!');
Titanium.UI.currentWindow.add(myView);
You can use this approach to make custom views, libraries of reusable code, and anything else you would like. Another common thing would be to have a Utility class that has many different helper functions:
// Utility.js
exports.cleanString = function(string) {
// Replace evil characters
var ret = string.replace(/[|&;$%#"<>()+,]/g, "");
// replace double single quotes
return ret.replace(/"/g, "''");
}
This method can be easily used like this:
// app.js
var Utility = require('Utility.js');
Ti.API.info(Utility.cleanString('He##o W&orld$'));
Another common method I use it for is to implement the Singleton pattern as each module loaded is its own functional context, so if you like, you can have values that persist:
// ManagerSingleton.js
var SpriteManager = {
count : 0
};
exports.addSprite = function() {
SpriteManager.count++;
}
exports.removeSprite = function() {
SpriteManager.count--;
}
You would load and use this much the same way as Utility:
// app.js
var ManagerSingleton = require('ManagerSingleton');
ManagerSingleton.addSprite();
This is something of a more elegant solution instead of using global variables. These methods are by no means perfect, but they have saved me a lot of time and frustration, and added depth, elegance, and readability to my Titanium code for Apps of all sizes and types.
There are two dominant module systems in the Javascript world.
One is the CommonJS and the second is AMD. There is a lot of discussion about which one is best and for what purpose. CommonJS is more widely used for server side JS while AMD is used mostly for browser JS.
RequireJS (requirejs.org) seems to be the most popular AMD system.
For information on JS module systems please read here: http://addyosmani.com/writing-modular-js/

Loading JavaScript in order of dependencies with Require.js

I'm diving into Require.js to build a Google Maps application using this link and this link as a guide. My goal is to build a modular application that loads "base" functionality first, and then I can "plug-in" client-specific functionality - without duplicating code. So every project will use the "base" JS, but the client-specific JS will be different each project. Below is a list of my loading dependencies. This is the order I need things to load, with the previous item in the list needing to be fully loaded before moving onto the next:
Load jQuery and Google Maps API (I got this one working)
Load JavaScript to initialize my map on the page with base application functionality
Load additional/client-specific JavaScript.
I can get 1 and 2 to work just fine using this:
main.js:
require.config({
paths:{
jquery: "jquery-1.7.1.min",
jqueryui: "jquery-ui-1.8.22.custom.min",
async: "async",
requiremap: "requiremap"
}
});
require(
[ "jquery", "jqueryui", "requiremap" ],
function( $, jqueryui, requiremap ) {
}
);
requiremap.js:
define(
[ "async!http://maps.google.com/maps/api/js?sensor=false" ],
function() {
require(['js/basemapfunctionality.js'], function() {
});
}
);
But now I need to wait until #2 is completely loaded before loading #3. Is this possible with Require.js, and if so, how? (and if not, are there alternative frameworks that can do this) I tried adding another nested require method to load the additional functionality (illustrated below), but it acts like #2 hasn't loaded yet.
define(
[ "async!http://maps.google.com/maps/api/js?sensor=false" ],
function() {
require(['js/basemapfunctionality.js'], function() {
require(['js/additionalfunctionality.js'], function() {
// now everything should be loaded, but it ain't
});
});
}
);
Got this to work. Just had to break up the loading of Google Maps API, base functionality, and additional functionality into different modules and declare each in main.js.
main.js
require.config({
paths:{
jquery: "jquery-1.7.1.min",
jqueryui: "jquery-ui-1.8.22.custom.min",
async: "async",
requiremap: "requiremap",
basemapfunctionality: "basemapfunctionality",
additionalfunctionality: "additionalfunctionality"
}
});
require(
[ "jquery", "jqueryui", "requiremap", "basemapfunctionality", "additionalfunctionality" ],
function( $, jqueryui, requiremap, basemapfunctionality, additionalfunctionality ) {
}
);
requiremap.js
define([ "async!https://maps.google.com/maps/api/js?sensor=false" ], function() {});
basemapfunctionality.js
define(['requiremap'], function(requiremap) {
// basemap functionality here
}
additionalfunctionality.js
define(['requiremap', 'basemapfunctionality'], function(requiremap, basemapfunctionality) {
// additional functionality here
}
I'm not much into RequireJS, but this issue and the commentary implies a way to resolve your question.
Relevant quote from the link:
This is due to the interaction of $.extend and the order of execution of modules.
In a built file, the 'other/config' module is evaluated before the inner call to
require(['other/controller'], an since $.extend only mixes in properties, it causes the problem.
You can avoid it by using the Object.create() type of approach, where you create a new object whose
prototype is the BaseConfig, so that later, any properties you add to the BaseConfig will show up
automatically on that new object.
See Asynchronously Loading the API. There is a callback parameter where you can indicate a callback function that will be called when the API is fully loaded.
Did you try with Order.js plugin ?

Categories