import Accessor = require("esri/core/Accessor");
import { subclass } from "esri/core/accessorSupport/decorators";
@subclass("esri.guide.Color")
class Color extends Accessor {
}
Implementing Accessor
Accessor aims to make developing classes easy by providing a mechanism to get, set, and watch properties.
This guide provides a guideline for common Accessor usage patterns. Please follow the links below to get further information on how to implement classes derived from Accessor. Please see the working with properties guide topic for additional information on Accessor properties.
If working in TypeScript, you will want to install the ArcGIS API for JavaScript 4.x type definitions. You can access these typings with its command line syntax at the jsapi-resources Github repository.
Extend Accessor
Many classes in the API extend the Accessor class. These classes can expose watchable properties that may have unique characteristics, such as being read-only or computed. Under the hood, Accessor uses dojo/_base/declare to create classes.
Create a simple subclass
The ///
comments are compiler directives for TypeScript. They let the TypeScript compiler know to include additional files. In this case, it specifies helper modules for the TypeScript decorators and extending classes.
define([
"esri/core/Accessor"
],
function(
Accessor
) {
var Color = Accessor.createSubclass({
declaredClass: "esri.guide.Color"
});
});
Extend multiple classes - Deprecated
Extending multiple classes is deprecated at 4.13, and support removed at 4.16. If apps are not upgraded, a warning message will appear in the browser console at 4.14 and 4.15, and classes that leverage the API class framework and multiple inheritance will stop working at version 4.16. This feature relied on dojo/_base/declare
which is removed from the JavaScript API. Instead see how to create mixins with TypeScript and JavaScript.
When extending multiple classes using the declared helper, you can take advantage of declaration merging by giving the interface the same name as the class.
/// <amd-dependency path="esri/core/tsSupport/declareExtendsHelper" name="__extends" />
/// <amd-dependency path="esri/core/tsSupport/decorateHelper" name="__decorate" />
import Evented = require("dojo/Evented");
import Accessor = require("esri/core/Accessor");
import { subclass, declared } from "esri/core/accessorSupport/decorators";
interface Collection extends Evented {}
@subclass("esri.guide.Collection")
class Collection extends declared(Accessor, Evented) {
}
define([
"dojo/Evented",
"esri/core/Accessor"
],
function(
Evented,
Accessor
) {
var Collection = Accessor.createSubclass([Evented], {
declaredClass: "esri.guide.Collection"
});
});
Mixins with Accessor
The ArcGIS API for JavaScript uses mixins to build its classes. Read this excellent article that goes deep dive on mixins with TypeScript.
First we define our EventedMixin
to add an event system to a class.
import Accessor = require("esri/core/Accessor");
import { subclass } from "esri/core/accessorSupport/decorators";
// A type to represent a constructor function
type Constructor<T = object> = new (...args: any[]) => T;
// A type to represent a mixin function
// See for more details https://www.bryntum.com/blog/the-mixin-pattern-in-typescript-all-you-need-to-know/
type Mixin<T extends (...input: any[]) => any> = InstanceType<ReturnType<T>>;
// TBase extends Constructor<Accessor> indicates that `EventedMixin`
// expects the base class to extend `Accessor`, for example to be able to use the `watch` method.
export const EventedMixin = <TBase extends Constructor<Accessor>>(Base: TBase) => {
@subclass("esri.guide.Evented")
class Evented extends Base {
/**
* A first function defined by the mixin
*/
emit(type: string, event?: any): boolean {
// ...
}
/**
* Another function defined by the mixin
*/
on(type: string, listener: (event: any) => void): IHandle {
// ...
}
}
return Evented;
}
// define the type of the mixin. This is useful to type properties that extends this mixin
// eg: `myProperty: EventedMixin;`
export type EventedMixin = Mixin<typeof EventedMixin>;
define([], function() {
/**
* A mixin is a function that returns a class extending the `Base` superclass
* with extra functionalities.
*/
var EventedMixin = function EventedMixin(Base) {
// Assuming `Base` extends `Accessor` we can use `createSubclass`.
// For plain ECMAScript classes, see examples https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes#Mix-ins
return Base.createSubclass({
declaredClass: "esri.guide.Evented",
/**
* A first function defined by the mixin
*/
emit: function(type, event) {
// ...
},
/**
* Another function defined by the mixin
*/
on: function(type, listener) {
// ...
}
});
}
return EventedMixin;
});
A mixin is a function that creates the super class for final subclass. In this example we create a super class that extends Accessor
and adds capabilities from EventedMixin
. The Collection
class then extends the final subclass.
import Accessor = require("esri/core/Accessor");
import { subclass } from "esri/core/accessorSupport/decorators";
// import the newly created mixin
import { EventedMixin } from "esri/guide/EventedMixin";
@subclass("esri.guide.Collection")
export class Collection extends EventedMixin(Accessor) {
// Collection extends a super class composed of Accessor and EventedMixin.
}
define([
"esri/guide/EventedMixin",
"esri/core/Accessor"
],
function(
Evented,
Accessor
) {
var Collection = EventedMixin(Accessor).createSubclass({
declaredClass: "esri.guide.Collection"
});
return Collection;
});
Properties
Define a simple property
The following syntax should be used when you want to have a simple, watchable, property that does not require any additional behavior. You can define both default values and types for primitive property values. If working with TypeScript, default property values can be set in the constructor.
import Accessor = require("esri/core/Accessor");
import { subclass, property } from "esri/core/accessorSupport/decorators";
@subclass("esri.guide.Color")
class Color extends Accessor {
@property()
r: number = 255;
@property()
g: number = 255;
@property()
b: number = 255;
@property()
a: number = 1;
}
define([
"esri/core/Accessor"
],
function(
Accessor
) {
var Color = Accessor.createSubclass({
declaredClass: "esri.guide.Color",
constructor: function() {
this.r = 255;
this.g = 255;
this.b = 255;
this.a = 1;
},
properties: {
r: {},
g: {},
b: {},
a: {}
}
});
});
Define custom getter and setter
There may be times when you may need to verify, validate, or transform values set on a property. You may also need to do additional (synchronous) work when a property is being set. The following snippets show this.
import Accessor = require("esri/core/Accessor");
import { subclass, property } from "esri/core/accessorSupport/decorators";
@subclass("esri.guide.Collection")
class Collection extends Accessor {
private _items: any[] = [];
// Example: Define a custom property getter.
// Accessor caches the values returned by the getters.
// At this point `length` will never change.
// See the "Notify a property change" section
@property()
get length(): number {
return this._items.length;
}
set length(value: number) {
// Example: perform validation
if (value <= 0) {
throw new Error(`value of length not valid: ${value}`);
}
// internally you can access the cached value of `length` using `_get`.
const oldValue = this._get<number>("length");
if (oldValue !== value) {
// a setter has to update the value from the cache
this._set("length", value);
// Example: perform additional work when the length changes
this._items.length = value;
}
}
}
var Collection = Accessor.createSubclass({
declaredClass: "esri.guide.Collection",
constructor() {
this._items = [];
},
_items: null,
properties: {
length: {
// Example: Define a custom property getter.
// Accessor caches the values returned by the getters.
// At this point `length` will never change.
// See the "Notify a property change" section
get: function() {
return this._items.length;
},
set: function(value) {
// Example: perform validation
if (value <= 0) {
throw new Error(`value of length not valid: ${value}`);
}
// internally you can access the cached value of `length` using `_get`.
const oldValue = this._get("length");
if (oldValue !== value) {
// a setter has to update the value from the cache
this._set("length", value);
// Example: perform additional work when the length changes
this._items.length = value;
}
}
}
}
});
Define a read-only property
The following syntax shows how to set a read-only property.
import Accessor = require("esri/core/Accessor");
import { subclass, property } from "esri/core/accessorSupport/decorators";
@subclass("esri.guide.Person")
class Person extends Accessor {
// Example: read-only property may not be externally set
@property({ readOnly: true })
firstName: string;
@property({ readOnly: true })
lastName: string;
updateName(firstName: string, lastName: string): void {
// We may still update the read-only property internally, which will change
// the property and notify changes to watchers
this._set({
firstName: firstName,
lastName: lastName
});
}
}
var Person = Accessor.createSubclass({
declaredClass: "esri.guide.Person",
properties: {
// Example: read-only property may not be externally set
firstName: {
readOnly: true
},
lastName: {
readOnly: true
}
},
updateName: function(firstName, lastName) {
// We may still update the read-only property internally, which will change
// the property and notify changes to watchers
this._set({
firstName: firstName,
lastName: lastName
});
}
});
Define a proxy property
Sometimes you need to proxy a property when both reading and writing, in addition to possibly performing a transformation on the value. For example, exposing an inner member property.
import Accessor = require("esri/core/Accessor");
import { subclass, aliasOf } from "esri/core/accessorSupport/decorators";
@subclass("esri.guide.GroupLayer")
class GroupLayer extends Accessor {
@property()
sublayers: Collection = new Collection();
// Define a property that reflects one in another object.
@property({ aliasOf: "sublayers.length" })
length: number;
// Alternatively you can use the `@aliasOf` decorator
// @aliasOf
// length: number
// You can also proxy a method from another object.
@aliasOf("sublayers.add")
add: (item: any) => void;
}
var GroupLayer = Accessor.createSubclass({
declaredClass: "esri.guide.GroupLayer",
constructor() {
this.sublayers = new Collection();
},
properties: {
sublayers: {},
// Define a property that reflects one in another object.
length: {
aliasOf: "sublayers.length"
},
// You can also proxy a method from another object.
add: {
aliasOf: "sublayers.add"
}
}
});
Computed properties
Define a computed property
You may need to use this when a property value depends on numerous other properties. These properties are always read-only.
import Accessor = require("esri/core/Accessor");
import { subclass, property } from "esri/core/accessorSupport/decorators";
@subclass()
class Subclass extends Accessor {
@property()
firstName: string;
@property()
lastName: string;
@property({
readOnly: true,
// define the property dependencies
dependsOn: ["firstName", "lastName"]
})
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
Accessor.createSubclass({
properties: {
firstName: {},
lastName: {},
fullName: {
readOnly: true,
// define the property dependencies
dependsOn: ["firstName", "lastName"],
get: function() {
return this.firstName + " " + this.lastName;
}
}
}
});
Define a writable computed property
import Accessor = require("esri/core/Accessor");
import { subclass, property } from "esri/core/accessorSupport/decorators";
@subclass()
class Subclass extends Accessor {
@property()
firstName: string;
@property()
lastName: string;
@property({
// define the property dependencies
dependsOn: ["firstName", "lastName"]
})
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
set fullName(value: string) {
if (!value) {
this._set("firstName", null);
this._set("lastName", null);
this._set("fullName", null);
return;
}
const [firstName, lastName] = value.split(" ");
this._set("firstName", firstName);
this._set("lastName", lastName);
this._set("fullName", value);
}
}
Accessor.createSubclass({
properties: {
firstName: {},
lastName: {},
fullName: {
readOnly: true,
// define the property dependencies
dependsOn: ["firstName", "lastName"],
get: function() {
return this.firstName + " " + this.lastName;
},
set: function(value) {
if (!value) {
this._set("firstName", null);
this._set("lastName", null);
this._set("fullName", null);
return;
}
var split = value.split(" ");
this._set("firstName", split[0]);
this._set("lastName", split[1]);
this._set("fullName", value);
}
}
}
});
Notify a property change
Sometimes properties cannot notify when changed. Accessor has an internal method to notify of any changes. This will mark the property as dirty. The next time the property is accessed its value is re-evaluated.
import Accessor = require("esri/core/Accessor");
import { subclass, property } from "esri/core/accessorSupport/decorators";
@subclass("esri.guide.Collection")
class Collection extends Accessor {
private _items: any[] = [];
@property({
readOnly: true
})
get length(): number {
return this._items.length;
}
add(item: any): void {
this._items.push(item);
// We know the value of `length` is changed.
// Notify so that at next access, the getter will be invoked
this.notifyChange("length");
}
}
var Collection = Accessor.createSubclass({
declaredClass: "esri.guide.Collection",
constructor() {
this._items = [];
},
_items: null,
properties: {
length: {
get: function() {
return this._items.length;
}
}
},
add: function(item) {
this._items.push(item);
// We know the value of `length` is changed.
// Notify so that at next access, the getter will be invoked
this.notifyChange("length");
}
});
Autocast
Define the property type
It is possible to define a type for a class' property.
import Graphic = require("esri/Graphic");
import Accessor = require("esri/core/Accessor");
import Collection = require("esri/core/Collection");
import { subclass, property } from "esri/core/accessorSupport/decorators";
@subclass()
class GraphicsLayer extends Accessor {
@property({
// Define the type of the collection of Graphics
// When the property is set with an array,
// the collection constructor will automatically be called
type: Collection.ofType(Graphic)
})
graphics: Collection<Graphic>;
}
var GraphicsLayer = Accessor.createSubclass({
properties: {
// Shorthand for camera: { type: Camera }
graphics: {
type: Collection.ofType(Graphic)
}
}
});
Define a method to cast a property
Sometimes you need to ensure a property's value type when it is being set. A good example of this is having well-known, preset, names for specific values, such as map.basemap = "streets-vector"
.
The type
metadata automatically creates an appropriate cast
for Accessor and primitive types if it is not already set.
import Accessor = require("esri/core/Accessor");
import { subclass, property, cast } from "esri/core/tsSupport/declare";
@subclass()
class Color extends Accessor {
@property()
r: number = 0;
@property()
g: number = 0;
@property()
b: number = 0;
@property()
a: number = 1;
@cast("r")
@cast("g")
@cast("b")
protected castComponent(value: number): number {
// cast method that clamp the value that
// will be set on r, g or b between 0 and 255
return Math.max(0, Math.min(255, value));
}
@cast("a")
protected castAlpha(value: number): number {
// cast method that clamp the value that
// will be set on a between 0 and 1
return Math.max(0, Math.min(1, value));
}
}
function castComponent(value) {
// cast method that clamp the value that
// will be set on r, g or b between 0 and 255
return Math.max(0, Math.min(255, value));
}
function castAlpha(value) {
// cast method that clamp the value that
// will be set on a between 0 and 1
return Math.max(0, Math.min(1, value));
}
Accessor.createSubclass({
properties: {
r: {
value: 255,
cast: castComponent
},
g: {
value: 255,
cast: castComponent
},
b: {
value: 255,
cast: castComponent
},
a: {
value: 1,
cast: castAlpha
}
}
});
Define the parameters type from a method - Deprecated
Defining parameter type from a method is deprecated at 4.14. Parameter decorators is not part of the current JavaScript Decorators standardization proposal, so the support will be preemptively removed from the JavaScript API.
It is possible to autocast parameters of a method. In this case, the developer is not required to import the class of the parameter and instantiate it.
import Accessor = require("esri/core/Accessor");
import { subclass, cast } from "esri/core/accessorSupport/decorators";
import Query = require("esri/tasks/support/Query");
@subclass("Test")
export default class Test extends Accessor {
query(@cast(Query) query: Query): void {
console.log(query.declaredClass);
}
}
Additional information
Please refer to these additional links for further information: