Programming patterns
Introduction
This section discusses programming patterns and best practices for writing applications with the ArcGIS Maps SDK for JavaScript.
Loading classes
After getting the ArcGIS Maps SDK for JavaScript, use require()
to asynchronously load ArcGIS Maps SDK for JavaScript classes into an application.
The method requires two parameters:
- An array of ordered strings as the full namespaces for each imported API class
- Each API class is loaded as the positional arguments in a callback function
For example, to load the Map
and Map
class, first go to the documentation and find the full namespace for each class. In this case, Map
has the namespace "esri/Map"
and the namespace for Map
is "esri/views/Map
. Then pass these strings as an array to require()
, and use the local variable names Map
and Map
as the positional arguments for the callback function:
Not every module needs to be loaded with require()
, as many classes can be initialized from within a constructor using autocasting.
Learn more about the AMD module format with the Dojo Toolkit introduction to AMD modules.
Constructors
All classes in the ArcGIS Maps SDK for JavaScript have a single constructor, and all properties can be set by passing parameters to the constructor.
For example, here is an example of calling the constructor for the Map
and Map
classes.
Alternatively, the properties of class instances can be specified directly using setters.
Properties
The ArcGIS Maps SDK for JavaScript supports a simple, consistent way of getting, setting, and watching all properties of a class.
Many API classes are subclasses of the Accessor
class, which defines the following methods.
Method Name | Return Type | Description |
---|---|---|
get(property | Varies | Gets the value of the property with the name property |
set(property | N/A | For each key /value pair in property , this method sets the value of the property with the name key to value |
watch(property | Watch | Calls the callback function callback whenever the value of the property with the name property changes |
Getters
The get
method returns the value of a named property.
This method is a convenience method because, without using get()
, to return the value nested properties, e.g., to return the title
of the property basemap
of a Map
object, requires an if
statement to check whether basemap
is either undefined
or null
.
The get
method removes the need for the if
statement, and returns the value of map.basemap.title
if map.basemap
exists, and null
otherwise.
Setters
The values of properties may be set directly.
When several property values need to be changed, set()
can be passed a JavaScript Object
with the property names and the new values.
Prior to 4.x, some properties could be get or set by calling getMethodname() or setMethodname(). These getter and setter methods are no longer supported.
Watching for property changes
The API provides two patterns for monitoring when property values change: reactiveUtils and Accessor.watch()
.
reactiveUtils
reactive
provides the ability to track changes in API properties with a variety of different data types and structures, such as strings, booleans, arrays, collections, and objects. The module also allows for combining properties from multiple sources. It includes the following methods: watch()
, on()
, once()
, when()
, and when
.
reactive
provide TypeScript type checking. You can access properties, build objects or perform other calculations and it is all properly checked by the TypeScript compiler. Callback parameters are also correctly inferred from the get
function.
One of the most common implementation patterns is tracking when simple properties get accessed. The pattern uses the watch()
method and looks like this:
The watch()
method returns a WatchHandle. To stop watching for changes, call the remove()
method on the watch handle, for example handle.remove()
. It is a best practice to remove watchers when they are no longer needed.
The next code snippet uses watch()
to track when the view.updating
boolean property is accessed:
This code snippet determines if all layers become visible or not using the map.all
property, which is a Collection
:
The following code snippet shows how to track when two different properties (view.stationary
and view.zoom
) are accessed using the watch()
method:
With reactive
you can track objects through the use of dot notation (e.g. object.property
), as well as bracket notation (e.g. object["property"]
), and through optional chaining (e.g. object.value?.property
).
For example, you could track when a 3D application has ambient occlusion shading enabled or disabled:
Accessor.watch()
Accessor.watch()
is a convenience method that is available in subclasses of Accessor
to watch for property changes on the class instance. The constructor takes two parameters:
- A property name specified as a
String
orString[]
, and - A callback function that is called whenever the property value changes
The following code snippet watches the Map
, and calls the callback function whenever the scale value changes.
Accessor.watch()
also returns a Watch
. To stop watching for changes, call the remove()
method on the watch handle, for example handle.remove()
. It is a best practice to remove watchers when they are no longer needed.
Autocasting
Autocasting casts JavaScript objects as ArcGIS Maps SDK for JavaScript class types without the need for these classes to be explicitly imported by the app developer.
In the following code sample, five API classes are needed to create a SimpleRenderer for a FeatureLayer.
With autocasting, you don't have to import renderer and symbol classes; the only module you need to import is esri/layers/Feature
.
To know whether a class can be autocasted, look at the ArcGIS Maps SDK for JavaScript reference for each class. If a property can be autocasted, the following image will appear:
For example, the documentation for the property renderer
of the Feature
class has an autocast
tag.
Notice the code using autocasting is simpler and is functionally identical to the above code snippet where all the modules are explicitly imported. The ArcGIS Maps SDK for JavaScript will take the values passed to the properties in the constructor and instantiate the typed objects internally.
Keep in mind there is no need to specify the type
on properties where the module type is known, or fixed. For example, look at the outline
property in the SimpleMarkerSymbol class from the snippet above. It doesn't have a type
property because the only Symbol
subclass with an outline
property is Simple
.
In cases where the type
is more generic, such as FeatureLayer.renderer, then type
must always be specified for autocasting to work properly.
All code samples document whether a class or property is being autocasted.
Async data
This section covers JavaScript Promises and the Loading pattern in the ArcGIS Maps SDK for JavaScript.
Promises
Promises play an important role in the ArcGIS Maps SDK for JavaScript. Using promises allows for writing cleaner code when working with asynchronous operations.
What is a Promise?
On the most basic level, a promise is a representation of a future value returned from an asynchronous task. When the task executes, the promise allows other processes to run simultaneously while it waits for a future value to be returned. This is particularly useful when making multiple network requests where timing and download speeds can be unpredictable.
A promise is always in one of three states:
- pending
- fulfilled
- rejected
When a promise resolves, it can resolve to a value or another promise as defined in a callback
function. When a promise is rejected, it should be handled in an err
function.
Using Promises
Promises are commonly used with then()
. This is a powerful method that defines the callback function that is called if the promise is resolved, and an error function that is called if the promise is rejected. The first parameter is always the success callback and the second, optional parameter, is the error callback.
The callback
is invoked once the promise resolves and the err
is called if the promise is rejected.
The catch()
method can also be used to specify the error callback function for a promise.
See Esri Error for more information on error handling.
Example: GeometryService
In this example, the geometryService is used to project several point geometries to a new spatial reference. In the documentation for geometry
, notice that project()
returns a promise that resolves to an array of projected geometries.
Using async/await
One advantage of promises is they can be used within an async, or asynchronous, function that allows expressions to run synchronously without blocking the execution of other code. This can be accomplished by placing the async
keyword before a function definition, and then setting the await
keyword on an expression that returns a promise. Code outside of the async function can run in parallel until the promise in the await
expression is fulfilled or rejected.
Here is an example using reactive
. This expression waits for the first time the zoom level is greater than 20. Then it returns a promise. After the promise is resolved, a message is written to the console:
Async functions can contain zero or more await
expressions that run sequentially as each promise is resolved. In this example, the compile
function runs simultaneously with the query
function. The compile
function runs view.when
and waits for the promise to resolve that indicates the Layer
has been created on the View
. Then it uses reactive
to evaluate when the layer view has finished updating. When that expression evaluates the updating
property to be true
, the code execution continues to the third expression that calculates statistics, and then the function returns the results.
Additional resources
Read more about promises in the MDN Promise documentation as well as async
and await
to get a more in-depth look at their structure and usage.
The following are additional links to blogs that explain promises with other helpful examples:
Loadable
Resources such as layers, maps, and portal items frequently rely on remote services or datasets on disk to initialize their state. Accessing such data requires the resources to initialize their state asynchronously. The loadable design pattern unifies this behavior, and resources that adopt this pattern are referred to as "loadable."
Loadable resources handle concurrent and repeated requests to allow sharing the same resource instance among various parts of an application. This pattern permits the cancellation of loading a resource for scenarios such as when the service is slow to respond. Finally, loadable resources provide information about their initialization status through explicit states that can be inspected and monitored.
Load status
The loadStatus property on loadable classes returns the state of the loadable resource. Four states are possible.
State | Description |
---|---|
not-loaded | The resource has not been asked to load its metadata and its state isn't properly initialized. |
loading | The resource is in the process of loading its metadata asynchronously |
failed | The resource failed to load its metadata, and the error encountered is available from the load property. |
loaded | The resource successfully loaded its metadata and its state is properly initialized. |
The following state transitions represent the stages that a loadable resource goes through.
The Loadable interface includes listeners that make it easy to monitor the status of loadable resources, display progress, and take action when the state changes.
Loading
A resource commences loading its metadata asynchronously when load()
is invoked.
At that time, the load status changes from not-loaded
to loading
. When the asynchronous operation completes, the callback is called. If the operation encounters an error, the error argument in the callback is populated, and load
is set to failed
. If the operation completes successfully, the error argument is null
and the load status is set to loaded
, which means the resource has finished loading its metadata and is now properly initialized.
Many times, the same resource instance is shared by different parts of the application. For example, a legend component and Layer
may have access to the same layer, and they both may want to access the layer's properties to populate their UI. Or the same portal instance may be shared across the application to display the user's items and groups in different parts of the application. load()
supports multiple "listeners" to simplify this type of application development. It can be called concurrently and repeatedly, but only one attempt is made to load the metadata. If a load operation is already in progress (loading
state) when load()
is called, it simply piggy-backs on the outstanding operation and the callback is queued to be invoked when that operation completes.
If the operation has already completed (loaded
or failed
state) when load()
is called, the callback is immediately invoked with the passed result of the operation, be it success or failure, and the state remains unchanged. This makes it safe to liberally call load()
on a loadable resource without having to check whether the resource is loaded and without worrying that it will make unnecessary network requests every time.
If a resource has failed to load, calling load()
will not change its state. The callback will be invoked immediately with the past load error.
Cancel loading
A resource cancels any outstanding asynchronous operation to load its metadata when cancel
is invoked. This transitions the state from loading
to failed
. The load
property will return information that reflects the operation was cancelled.
This method should be used carefully because all enqueued callbacks for that resource instance will get invoked with an error stating that the operation was cancelled. Thus, one component in the application can cancel the load initiated by other components when sharing the same resource instance.
The cancel
method does nothing if the resource is not in the loading
state.
Cascading load dependencies
It is common for a loadable resource to depend on loading other loadable resources to properly initialize its state. For example, a portal item cannot finish loading until its parent portal finishes loading. A feature layer cannot be loaded until its associated feature service is first loaded. This situation is referred to as a load dependency.
Loadable operations invoked on any resource transparently cascade through its dependency graph. This helps simplify using loadable resources and puts the responsibility on the resource to correctly establish and manage its load dependencies.
The following code example shows how this cascading behavior leads to concise code. Loading the map causes the portal item to begin loading, which in turn initiates loading its portal. It is unnecessary to load resources explicitly.
It is possible that dependencies may fail to load. Some dependencies might be critical, such as a portal item's dependency on its portal. If a failure is encountered while loading such a dependency, that error would bubble up to the resource that initiated the load cycle, which would also fail to load. Other load dependencies may be incidental, such as a map's dependency on one of its operational layers, and the resource may be able to load successfully even if one of its dependency fails to load.
Using when()
The when() method can be used with any loadable class to determine when the class has loaded.
This method differs from load()
since it does not invoke the loading of a resource.
Instead, it should be used with classes that are loaded automatically, like Map
or Scene
.
Using fromJSON
Many classes, including all symbols, geometries, Camera, Viewpoint, Color, and FeatureSet, contain a method called from
.
This function creates an instance of the given class from JSON generated by an ArcGIS product. JSON in this format is typically created from a t
method or a query via the REST API. See the ArcGIS REST API documentation for information and examples of how geometries, symbols, webmaps, etc. are queried and represented in JSON.
The following sample shows how to create a SimpleMarkerSymbol with JSON that was previously retrieved from a query using the REST API.
The JSON object passed as the input parameter to from
may look similar to the object passed as a constructor parameter in the same class. However, these two objects are different in various ways and should not be interchanged. This is because the values and default units of measurement differ between the REST API and the ArcGIS Maps SDK for JavaScript (e.g. symbol size is measured in points with the REST API whereas the ArcGIS Maps SDK for JavaScript uses pixels).
The parameter passed in class constructors is a simple JSON object. This pattern should always be used to create a new instance of a class, unless dealing with JSON previously generated elsewhere from t
or a query to the REST API. Always work with from
, not the constructor, when creating a class instance from a JSON object in cases where the JSON was previously generated using the REST API or another ArcGIS product (e.g. ArcGIS Server, ArcGIS Online, Portal for ArcGIS, etc.).
Using jsonUtils
There are several json
classes that are provided as convenience classes when using from
to instantiate an object, but the type of the object is unknown.
These classes are available for scenarios when a JSON object represents either a geometry, renderer, or symbol from the REST API, but the the object type is unknown. For example, when the renderer of a layer if taken from a REST request and there is uncertainty about whether the renderer is a UniqueValueRenderer, require()
the esri/renderers/support/jsonUtils class to help determine the type of the renderer.
Widget viewModel pattern
Additional functionality can be implemented using the viewModel for many of the out-of-the-box widgets.
There are two parts to working with widgets: the widget, and the widget's viewModel. The widget (i.e. view) part is responsible for handling the User Interface (UI) of the widget, meaning how the widget displays and handles user interaction via the DOM, for example the Sketch widget. The viewModel part is responsible for the underlying functionality of the widget, or rather, its business logic, for example SketchViewModel.
Why divide the widget framework into these two separate parts? One reason is reusability. The viewModel exposes the API properties and methods needed for functionality required to support the view, whereas the view contains the DOM logic. Since viewModels extend from esri/core/Accessor, they take advantage of all Accessor's capabilities. This helps keeps consistency between various parts of the API since many other modules derive from this class as well.
So how do these two parts work together? When a widget renders, it renders its state
. This state is derived from both view and viewModel properties. At some point within the widget's lifecycle, the view calls upon the viewModel's methods/properties which causes a change to a property or result. After a change is triggered, the view is then notified and will update on the UI.
Here is an example using the Sketch
property to override the default drawing symbology while actively creating a new graphic: