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

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"
  });

});

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

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: