This tutorial covers the following:
Because the JS API uses Dojo, the recommended way to create re-usable user-interface components is to use Dijit. Dijit is the user interface library of the Dojo Toolkit that provides an extensive collection of pre-built and tested widgets as well as system for building your own custom widgets. This tutorial will walk through creating a custom widget using Dijit and show how to use a custom dijit in an ArcGIS API for JavaScript application.
There are several resources available for getting to know Dijit. To gain a general understanding of how Dijit works, it is recommended to read through the material listed below. For instance, it is beneficial to know the difference between declarative and programmatic widget creation, the difference between dojo/dom.byId and dijit/registry.byId and the purpose of dijit/registry.byId (hint: all answers are in the first link in the list).
The links above do a great job of describing why Dijit exists and many of the Dijit's benefits. To re-cap, below is a list of why building custom dijits is a good idea when developing web applications:
A widget is a class defined in a module that creates a UI component. This tutorial builds on the ideas introduced in the Write a Class tutorial, which covers creating a module and class. Because a widget is a class, common practice is to create one widget per module, and that's what this tutorial will show.
The widget created in this tutorial is the HomeButton widget introduced at version 3.7 of the JS API. The widget records a map's extent (AKA bounding box) when a page loads and provides a button to let users return to that view at any time. The full source for the widget is available on github.
To create a widget, use define() to load the base class for the widget (dijit/_WidgetBase), mixins (such as dijit/_TemplatedMixin) and any modules used internally by the class. Below is the dependency list for the widget and the callback function with arguments that alias all loaded modules and is the beginning of HomeButton.js.
define([ "dijit/_WidgetBase", "dijit/_OnDijitClickMixin", "dijit/_TemplatedMixin", "dojo/Evented", "dojo/_base/declare", "dojo/_base/lang", "dojo/on", // load template "dojo/text!esri/dijit/templates/HomeButton.html", "dojo/dom-class", "dojo/dom-style" ], function ( _WidgetBase, _OnDijitClickMixin, _TemplatedMixin, Evented, declare, lang, on, dijitTemplate, domClass, domStyle ) { ... });
As mentioned previously, dijit/_WidgetBase will be used as the base class for this widget. dijit/_TemplatedMixin as well as dijit/_OnDijitClickMixin will be used as mixins when declaring the class for the widget. Other modules loaded are used to implement various widget functionality:
When using dojo/_base/declare to create a class and inheriting from multiple classes, the first class in the list of classes to inherit from acts as the parent class and additional classes are mixed in to the resulting class.
var Widget = declare( [_WidgetBase, _OnDijitClickMixin, _TemplatedMixin, Evented], { ... } );
dijit/_WidgetBase implements the Dijit lifecycle and using it as the base class allows the HomeButton to use the lifecycle as well.
The implementation of the HomeButton widget is made up of a single object passed to dojo/_base/declare. Certain properties (constructor, startup and destroy, correspond to Dijit lifecycle method names.
var Widget = declare([_WidgetBase, _OnDijitClickMixin, _TemplatedMixin, Evented], { declaredClass: "esri.dijit.HomeButton", templateString: dijitTemplate, options: { theme: "HomeButton", map: null, extent: null, visible: true }, constructor: function (options, srcRefNode) { // mix in settings and defaults declare.safeMixin(this.options, options); this._i18n = i18n; // properties this.set("map", this.options.map); this.set("theme", this.options.theme); this.set("visible", this.options.visible); this.set("extent", this.options.extent); // listeners this.watch("theme", this._updateThemeWatch); this.watch("visible", this._visible); // classes this._css = { container: "homeContainer", home: "home", loading: "loading" }; }, startup: function () { // map not defined if (!this.map) { this.destroy(); console.log('HomeButton::map required'); } // when map is loaded if (this.map.loaded) { this._init(); } else { on(this.map, "load", lang.hitch(this, function () { this._init(); })); } }, // connections/subscriptions will be cleaned up during the destroy() lifecycle phase destroy: function () { this.inherited(arguments); }, /* ---------------- */ /* Public Events */ /* ---------------- */ // home // load /* ---------------- */ /* Public Functions */ /* ---------------- */ home: function () { var defaultExtent = this.get("extent"); this._showLoading(); if (defaultExtent) { return this.map.setExtent(defaultExtent) .then(lang.hitch(this, function () { this._hideLoading(); this.emit("home", { extent: defaultExtent }); })); } else { this._hideLoading(); console.log('HomeButton::no home extent'); } }, show: function () { this.set("visible", true); }, hide: function () { this.set("visible", false); }, /* ---------------- */ /* Private Functions */ /* ---------------- */ _init: function () { this._visible(); if (!this.get("extent")) { this.set("extent", this.map.extent); } this.set("loaded", true); this.emit("load", {}); }, _showLoading: function () { domClass.add(this._homeNode, this._css.loading); }, _hideLoading: function () { domClass.remove(this._homeNode, this._css.loading); }, _updateThemeWatch: function (attr, oldVal, newVal) { domClass.remove(this.domNode, oldVal); domClass.add(this.domNode, newVal); }, _visible: function () { if (this.get("visible")) { domStyle.set(this.domNode, 'display', 'block'); } else { domStyle.set(this.domNode, 'display', 'none'); } } });
The options object defines default properties for the widget. Values provided when creating an instance of the widget overwrite these defaults. This happens in the constructor method which fires when the widget is instantiated via the new keyword. The startup method checks that a map was provided and that it's loaded before saving the extent. Destroy relies on Dijit by calling this.inherited(). Additional methods are defined, including those that manipulate the widget's DOM and emit events.
Part of creating a widget is creating a template for the widget. A widget's template is created using a combination of html and custom data-* attributes to do things like attaching event handlers and applying css classes. This is the template for the HomeButton widget:
<div class="${theme}" role="presentation"> <div class="${_css.container}"> <div data-dojo-attach-point="_homeNode" title="${_i18n.widgets.homeButton.home.title}" role="button" data-dojo-attach-event="ondijitclick:home" class="${_css.home}"><span>${_i18n.widgets.homeButton.home.button}</span></div> </div> </div>
Attributes with ${...} are replaced with the widget property with the same name. In the markup above, the root div has class=${theme}
. In this case, ${theme}
will be replaced with the value from the widget's theme property. This applies to more complex property lookups such as _i18n.widgets.homeButton.home.title as well.
Custom data attributes such as data-dojo-attach-event provide a way to attach event handlers via markup. This is convenient when an event on an element needs to trigger a widget method. In this case, clicking a HomeButton div will trigger the home method.
Once the HomeButton widget is complete it can be used in a page that uses the JS API. Check out the HomeButton page on github as an example. Clone that repo and play around with it locally to get a better feel for how everything works. If anything is unclear, please post questions on GeoNet, the Esri community.