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 JavaScript API 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.

/// <amd-dependency path="esri/core/tsSupport/declareExtendsHelper" name="__extends" />
/// <amd-dependency path="esri/core/tsSupport/decorateHelper" name="__decorate" />

import Accessor = require("esri/core/Accessor");

import { subclass, declared } from "esri/core/accessorSupport/decorators";

@subclass("esri.guide.Color")
class Color extends declared(Accessor) {

}
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. This feature currently relies on dojo/_base/declare which will be 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.

/// <amd-dependency path="esri/core/tsSupport/declareExtendsHelper" name="__extends" />
/// <amd-dependency path="esri/core/tsSupport/decorateHelper" name="__decorate" />

import Accessor = require("esri/core/Accessor");
import { subclass, declared } 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 declared(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.

/// <amd-dependency path="esri/core/tsSupport/declareExtendsHelper" name="__extends" />
/// <amd-dependency path="esri/core/tsSupport/decorateHelper" name="__decorate" />

import Accessor = require("esri/core/Accessor");
import { subclass, declared } from "esri/core/accessorSupport/decorators";

// import the newly created mixin
import { EventedMixin } from "esri/guide/EventedMixin";

@subclass("esri.guide.Collection")
export class Collection extends declared(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.

/// <amd-dependency path="esri/core/tsSupport/declareExtendsHelper" name="__extends" />
/// <amd-dependency path="esri/core/tsSupport/decorateHelper" name="__decorate" />

import Accessor = require("esri/core/Accessor");

import { subclass, declared, property } from "esri/core/accessorSupport/decorators";

@subclass("esri.guide.Color")
class Color extends declared(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.

/// <amd-dependency path="esri/core/tsSupport/declareExtendsHelper" name="__extends" />
/// <amd-dependency path="esri/core/tsSupport/decorateHelper" name="__decorate" />

import Accessor = require("esri/core/Accessor");

import { subclass, declared, property } from "esri/core/accessorSupport/decorators";

@subclass("esri.guide.Collection")
class Collection extends declared(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.

/// <amd-dependency path="esri/core/tsSupport/declareExtendsHelper" name="__extends" />
/// <amd-dependency path="esri/core/tsSupport/decorateHelper" name="__decorate" />

import Accessor = require("esri/core/Accessor");

import { subclass, declared, property } from "esri/core/accessorSupport/decorators";

@subclass("esri.guide.Person")
class Person extends declared(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.

/// <amd-dependency path="esri/core/tsSupport/declareExtendsHelper" name="__extends" />
/// <amd-dependency path="esri/core/tsSupport/decorateHelper" name="__decorate" />

import Accessor = require("esri/core/Accessor");

import { subclass, declared, aliasOf } from "esri/core/accessorSupport/decorators";

@subclass("esri.guide.GroupLayer")
class GroupLayer extends declared(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.

/// <amd-dependency path="esri/core/tsSupport/declareExtendsHelper" name="__extends" />
/// <amd-dependency path="esri/core/tsSupport/decorateHelper" name="__decorate" />

import Accessor = require("esri/core/Accessor");
import { subclass, property, declared } from "esri/core/accessorSupport/decorators";

@subclass()
class Subclass extends declared(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

/// <amd-dependency path="esri/core/tsSupport/declareExtendsHelper" name="__extends" />
/// <amd-dependency path="esri/core/tsSupport/decorateHelper" name="__decorate" />

import Accessor = require("esri/core/Accessor");
import { subclass, property, declared } from "esri/core/accessorSupport/decorators";

@subclass()
class Subclass extends declared(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.

/// <amd-dependency path="esri/core/tsSupport/declareExtendsHelper" name="__extends" />
/// <amd-dependency path="esri/core/tsSupport/decorateHelper" name="__decorate" />

import Accessor = require("esri/core/Accessor");

import { subclass, declared, property } from "esri/core/accessorSupport/decorators";

@subclass("esri.guide.Collection")
class Collection extends declared(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.

/// <amd-dependency path="esri/core/tsSupport/declareExtendsHelper" name="__extends" />
/// <amd-dependency path="esri/core/tsSupport/decorateHelper" name="__decorate" />

import Graphic = require("esri/Graphic");

import Accessor = require("esri/core/Accessor");
import Collection = require("esri/core/Collection");

import { subclass, property, declared } from "esri/core/accessorSupport/decorators";

@subclass()
class GraphicsLayer extends declared(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'.

The type metadata automatically creates an appropriate cast for Accessor and primitive types if it is not already set.

 /// <amd-dependency path="esri/core/tsSupport/declareExtendsHelper" name="__extends" />
 /// <amd-dependency path="esri/core/tsSupport/decorateHelper" name="__decorate" />

 import Accessor = require("esri/core/Accessor");
 import { subclass, property, declared, cast } from "esri/core/tsSupport/declare";

 @subclass()
 class Color extends declared(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.

/// <amd-dependency path="esri/core/tsSupport/decorateHelper" name="__decorate" />
/// <amd-dependency path="esri/core/tsSupport/declareExtendsHelper" name="__extends" />
/// <amd-dependency path="esri/core/tsSupport/paramHelper" name="__param" />

import Accessor = require("esri/core/Accessor");
import { subclass, declared, cast } from "esri/core/accessorSupport/decorators";

import Query = require("esri/tasks/support/Query");

@subclass("Test")
export default class Test extends declared(Accessor) {

  query(@cast(Query) query: Query): void {
    console.log(query.declaredClass);
  }
}

Additional information

Please refer to these additional links for further information:

Content