UNPKG

@arcgis/core

Version:

ArcGIS Maps SDK for JavaScript: A complete 2D and 3D mapping and data visualization API

451 lines (438 loc) • 20.2 kB
/** * * [Overview](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#overview) * * [Using reactiveUtils](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#using-reactiveutils) * * [Working with collections](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#working-with-collections) * * [Working with objects](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#working-with-objects) * * [WatchHandles and Promises](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#watchhandles-and-promises) * * [Working with truthy values](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#truthy-values) * * <span id="overview"></span> * ## Overview * `reactiveUtils` provide capabilities for observing changes to the state of the SDK's properties, * and is an important part of managing your application's life-cycle. * State can be observed on a variety of different data types and structures * including strings, numbers, arrays, booleans, collections, and objects. * * <span id="using-reactiveutils"></span> * ## Using reactiveUtils * * `reactiveUtils` provides five methods that offer different patterns and capabilities for observing state: * [on()](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#on), [once()](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#once), [watch()](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#watch), [when()](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#when) and [whenOnce()](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#whenOnce). * * The following is a basic example using [watch()](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#watch). It demonstrates how to track the * Map component [updating](https://developers.arcgis.com/javascript/latest/references/map-components/components/arcgis-map/#updating) property and then send a message to the console * when the property changes. This snippet uses a `getValue` function as an expression that evaluates the * `updating` property, and when a change is observed the new value is passed to the callback: * * ```js * // Basic example of watching for changes on a boolean property * const viewElement = document.querySelector("arcgis-map"); * reactiveUtils.watch( * // getValue function * () => viewElement.updating, * // callback * (updating) => { * console.log(updating) * }); * ``` * * <span id="working-with-collections"></span> * ### Working with collections * * `reactiveUtils` can be used to observe changes within a collection, such as [Map.allLayers](https://developers.arcgis.com/javascript/latest/references/core/Map/#allLayers). Out-of-the-box JavaScript methods * such as [`.map()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) and [`.filter()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) can be used as * expressions to be evaluated in the `getValue` function. * * ```js * // Watching for changes within a collection * // whenever a new layer is added to the map * const viewElement = document.querySelector("arcgis-map"); * reactiveUtils.watch( * () => viewElement.map.allLayers.map( layer => layer.id), * (ids) => { * console.log(`FeatureLayer IDs ${ids}`); * }); * ``` * * <span id="working-with-objects"></span> * ### Working with objects * * With `reactiveUtils` you can track named object properties through dot notation (e.g. `viewElement.updating`) or * through bracket notation (e.g. `viewElement["updating"]`). You can also use the * [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) operator (`?.`). This operator * simplifies the process of verifying that properties used in the `getValue` function * are not `undefined` or `null`. * * ```js * // Watch for changes in an object using optional chaining * // whenever the map's extent changes * const viewElement = document.querySelector("arcgis-map"); * reactiveUtils.watch( * () => viewElement?.extent?.xmin, * (xmin) => { * console.log(`Extent change xmin = ${xmin}`) * }); * ``` * * <span id="watchhandles-and-promises"></span> * ### WatchHandles and Promises * * The [watch()](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#watch), [on()](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#on) and * [when()](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#when) methods return a [ResourceHandle](https://developers.arcgis.com/javascript/latest/references/core/core/Handles/#ResourceHandle). Be sure to remove watch handles when they are no longer needed to avoid memory leaks. * * ```js * // Use a WatchHandle to stop watching * const viewElement = document.querySelector("arcgis-map"); * const handle = reactiveUtils.watch( * () => viewElement?.extent?.xmin, * (xmin) => { * console.log(`Extent change xmin = ${xmin}`) * }); * * // In another function * handle.remove() * ``` * * The [once()](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#once) and [whenOnce()](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#whenOnce) methods return a Promise instead of a `WatchHandle`. * In some advanced use cases where an API action may take additional time, these * methods also offer the option to cancel the async callback via an * [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal). * Be aware that if the returned Promise is not resolved, it can also result in a memory leak. * * ```js * // Use an AbortSignal to cancel an async callback * // during view animation * const abortController = new AbortController(); * * // Observe the View's animation state * reactiveUtils.whenOnce( * () => view?.animation, {signal: abortController.signal}) * .then((animation) => { * console.log(`View animation state is ${animation.state}`) * }); * * // Cancel the async callback * const someFunction = () => { * abortController.abort(); * } * ``` * * <span id="truthy-values"></span> * ### Working with truthy values * * The [when()](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#when) and [whenOnce()](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#whenOnce) methods watch for *truthy* values, these are values that evaluate to `true` * in boolean contexts. To learn more about using truthy, visit this * [MDN Web doc](https://developer.mozilla.org/en-US/docs/Glossary/Truthy) article. The snippets below use the [Popup.visible](https://developers.arcgis.com/javascript/latest/references/core/widgets/Popup/) property, which is a boolean. * * ```js * // Observe changes on a boolean property * const viewElement = document.querySelector("arcgis-map"); * reactiveUtils.when(() => viewElement.popup?.visible, () => console.log("Truthy")); * reactiveUtils.when(() => !viewElement.popup?.visible, () => console.log("Not truthy")); * reactiveUtils.when(() => viewElement.popup?.visible === true, () => console.log("True")); * reactiveUtils.when(() => viewElement.popup?.visible !== undefined, () => console.log("Defined")); * reactiveUtils.when(() => viewElement.popup?.visible === undefined, () => console.log("Undefined")); * ``` * * @since 4.23 * @see [Watch for changes guide topic](https://developers.arcgis.com/javascript/latest/watch-for-changes/) * @see [Sample - Watch for changes in components using reactiveUtils](https://developers.arcgis.com/javascript/latest/sample-code/watch-for-changes-reactiveutils-components/) * @see [Sample - Property changes with reactiveUtils](https://developers.arcgis.com/javascript/latest/sample-code/watch-for-changes-reactiveutils/) * @see [Samples with `reactiveUtils`](https://developers.arcgis.com/javascript/latest/sample-code/?tagged=reactiveUtils) */ import type { EventedMixin } from "./Evented.js"; import type { ResourceHandle } from "./Handles.js"; import type { AbortOptions } from "./promiseUtils.js"; /** * Options used to configure how auto-tracking is performed and how the callback * should be called. */ export interface ReactiveWatchOptions<T = unknown> { /** * Whether to fire the callback immediately after initialization, if the necessary conditions are met. * * @default false */ readonly initial?: boolean; /** * Whether to fire the callback synchronously or on the next tick. * * @default false */ readonly sync?: boolean; /** * Whether to fire the callback only once. * * @default false */ readonly once?: boolean; /** Function used to check whether two values are the same, in which case the callback isn't called. Checks whether two objects, arrays or primitive values are shallow equal, e.g. one level deep. Non-plain objects are considered equal if they are strictly equal (===). */ readonly equals?: ReactiveEqualityFunction<T>; } /** * Function used to check whether two values are the same, in which case the * watch callback isn't called. * * @param newValue - The new value. * @param oldValue - The old value. * @returns Whether the new value is equal to the old value. */ export type ReactiveEqualityFunction<T> = (newValue: T, oldValue: T) => boolean; /** * Function which is auto-tracked and should return a value to pass to the [ReactiveWatchCallback](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#ReactiveWatchCallback) * * @returns The new value. */ export type ReactiveWatchExpression<T> = () => T; /** * Function which is auto-tracked and should return an event target to which * an event listener is to be added. * * @returns The event target. */ export type ReactiveOnExpression<T> = () => T; /** * Function to be called when a value changes. * * @param newValue - The new value. * @param oldValue - The old value. */ export type ReactiveWatchCallback<T> = (newValue: T, oldValue?: T) => void; /** * Function called to be called when an event is emitted or dispatched. * * @param event - The event emitted by the target. */ export type ReactiveOnCallback<T> = (event: T) => void; /** * Tracks any properties accessed in the `getValue` function and calls the callback * when any of them change. * * @param getValue - Function used to get the current value. All accessed properties will be tracked. * @param callback - The function to call when there are changes. * @param options - Options used to configure how the tracking happens and how the callback is to be called. * @returns A watch handle. * @example * // Watching for changes in a boolean value * // Equivalent to watchUtils.watch() * const viewElement = document.querySelector("arcgis-map"); * reactiveUtils.watch( * () => viewElement.popup?.visible, * () => { * console.log(`Popup visible: ${viewElement.popup.visible}`); * }); * @example * // Watching for changes within a Collection * const viewElement = document.querySelector("arcgis-map"); * reactiveUtils.watch( * () => viewElement.map.allLayers.length, * () => { * console.log(`Layer collection length changed: ${viewElement.map.allLayers.length}`); * }); * @example * // Watch for changes in a numerical value. * // Providing `initial: true` in ReactiveWatchOptions * // checks immediately after initialization * // Equivalent to watchUtils.init() * const viewElement = document.querySelector("arcgis-map"); * reactiveUtils.watch( * () => viewElement.zoom, * () => { * console.log(`zoom changed to ${viewElement.zoom}`); * }, * { * initial: true * }); * @example * // Watch properties from multiple sources * const viewElement = document.querySelector("arcgis-map"); * const handle = reactiveUtils.watch( * () => [viewElement.stationary, viewElement.zoom], * ([stationary, zoom]) => { * // Only print the new zoom value when the map component is stationary * if(stationary){ * console.log(`Change in zoom level: ${zoom}`); * } * } * ); */ export function watch<T, U extends T>(getValue: ReactiveWatchExpression<T>, callback: ReactiveWatchCallback<T>, options?: ReactiveWatchOptions<U>): ResourceHandle; /** * Watches the value returned by the `getValue` function and calls the callback when it becomes truthy. * * @param getValue - Function used to get the current value. All accessed properties will be tracked. * @param callback - The function to call when the value becomes truthy. * @param options - Options used to configure how the tracking happens and how the callback is to be called. * @returns A watch handle. * @example * // Observe when a boolean property becomes not truthy * // Equivalent to watchUtils.whenFalse() * reactiveUtils.when( * () => !layerView.updating, * () => { * console.log("LayerView finished updating."); * }); * @example * // Observe when a boolean property becomes true * // Equivalent to watchUtils.whenTrue() * const viewElement = document.querySelector("arcgis-map"); * reactiveUtils.when( * () => viewElement?.stationary === true, * async () => { * console.log("User is no longer interacting with the map"); * await drawBuffer(); * }); * @example * // Observe a boolean property for truthiness. * // Providing `once: true` in ReactiveWatchOptions * // only fires the callback once * // Equivalent to watchUtils.whenFalseOnce() * const featuresComponent = document.querySelector("arcgis-features"); * reactiveUtils.when( * () => !featuresComponent.closed, * () => { * console.log(`The features component is closed: ${featuresComponent.closed}`); * }, * { * once: true * }); */ export function when<T, U extends T>(getValue: ReactiveWatchExpression<T | null | undefined>, callback: (newValue: T, oldValue?: T) => void, options?: ReactiveWatchOptions<U>): ResourceHandle; /** * Callback to be called when an event listener is added or removed. * * @param target - The event target to which the listener was added or from which it was removed. */ export type ReactiveListenerChangeCallback<T> = (target: T) => void; /** Options used to configure the behavior of [on()](https://developers.arcgis.com/javascript/latest/references/core/core/reactiveUtils/#on). */ export interface ReactiveOnOptions<Target> { /** * Whether to fire the callback synchronously or on the next tick. * * @default false */ sync?: boolean; /** * Whether to fire the callback only once. * * @default false */ once?: boolean; /** Called when the event listener is added. */ onListenerAdd?: ReactiveListenerChangeCallback<Target>; /** Called when the event listener is removed. */ onListenerRemove?: ReactiveListenerChangeCallback<Target>; } /** * Watches the value returned by the `getTarget` function for changes and * automatically adds or removes an event listener for a given event, as * needed. * * @param getTarget - Function which returns the object to which the event listener is to be added. * @param eventName - The name of the event to add a listener for. * @param callback - The event handler callback function. * @param options - Options used to configure how the tracking happens and how the callback is to be called. * @returns A watch handle. * @example * // Adds a click event on a map component when it changes * const viewElement = document.querySelector("arcgis-map"); * reactiveUtils.on( * () => viewElement, * "arcgisViewClick", * (event) => { * console.log("arcgisViewClick event emitted: ", event); * }); * @example * // Adds a drag event on a map component and adds a callback * // to check when the listener is added and removed. * // Providing `once: true` in the ReactiveListenerOptions * // removes the event after first callback. * const viewElement = document.querySelector("arcgis-map"); * reactiveUtils.on( * () => viewElement, * "arcgisViewDrag", * (event) => { * console.log(`Drag event emitted: ${event}`); * }, * { * once: true, * onListenerAdd: () => console.log("Drag listener added!"), * onListenerRemove: () => console.log("Drag listener removed!") * }); */ export function on<Target extends EventTarget | EventedMixin>(getTarget: ReactiveOnExpression<Target | null | undefined>, eventName: string, callback: ReactiveOnCallback<any>, options?: ReactiveOnOptions<Target>): ResourceHandle; /** * Tracks any properties being evaluated by the `getValue` function. When `getValue` changes, it * returns a promise containing the value. This method only tracks a single change. * * @param getValue - Expression to be tracked. * @param signal - Abort signal which can be used to cancel the promise from resolving. * @returns A promise which resolves when the tracked expression changes. * @example * // Observe the first time a property equals a specific string value * // Equivalent to watchUtils.once() * reactiveUtils.once( * () => featureLayer.loadStatus === "loaded") * .then(() => { * console.log("featureLayer loadStatus is loaded."); * }); * @example * // Use a comparison operator to observe a first time * // difference in numerical values * const viewElement = document.querySelector("arcgis-map"); * const someFunction = async () => { * await reactiveUtils.once(() => viewElement.zoom > 20); * console.log("Zoom level is greater than 20!"); * } * @example * // Use a comparison operator and optional chaining to observe a * // first time difference in numerical values. * reactiveUtils.once( * () => map?.allLayers?.length > 2) * .then((value) => { * console.log(`The map now has ${value} layers.`); * }); */ export function once<T>(getValue: ReactiveWatchExpression<T>, signal?: AbortSignal | AbortOptions | null | undefined): Promise<T>; /** * Tracks any properties being evaluated by the `getValue` function. When `getValue` becomes truthy, * it returns a promise containing the value. This method only tracks a single change. * * @param getValue - Expression to be tracked. * @param signal - Abort signal which can be used to cancel the promise from resolving. * @returns A promise which resolves once the tracked expression becomes truthy. * @example * // Check for the first time a property becomes truthy * // Equivalent to watchUtils.whenOnce() * const viewElement = document.querySelector("arcgis-map"); * reactiveUtils.whenOnce( * () => viewElement.popup?.visible) * .then(() => { * console.log("Popup used for the first time"); * }); * @example * // Check for the first time a property becomes not truthy * // Equivalent to watchUtils.whenFalseOnce() * const someFunction = async () => { * await reactiveUtils.whenOnce(() => !layerView.updating); * console.log("LayerView is no longer updating"); * } * @example * // Check for the first time a property becomes truthy * // And, use AbortController to potentially cancel the async callback * const abortController = new AbortController(); * const viewElement = document.querySelector("arcgis-map"); * * // Observe the map component's updating state * reactiveUtils.whenOnce( * () => viewElement?.updating, {signal: abortController.signal}) * .then((updating) => { * console.log(`Map component's updating state is ${updating.state}`) * }); * * // Cancel the async callback * const someFunction = () => { * abortController.abort(); * } */ export function whenOnce<T>(getValue: ReactiveWatchExpression<T | null | undefined>, signal?: (AbortSignal | AbortOptions) | null | undefined): Promise<T>;