Dojo AMD module changes reference of "this" - dojo

I'm really confused as to some behavior I'm seeing in trying to learn the AMD style of Dojo. When I instantiate my module/object, "this" refers to the object in my constructor. I make a call to an internal function, and "this" inside that internal function refers to the Window object. So when I get to this.attachMapEventHandlers I get a "Object [object global] has no method 'attachMapEventHandlers'" error. What am I doing wrong? UPDATE: I found lang.hitch, which seems to indicate that the async nature is what's tripping me up, but I'm confused on how to implement a solution.
my script inside index.html:
require(["javascript/layout", "dijit/layout/ContentPane", "dijit/layout/BorderContainer", "dijit/layout/AccordionContainer",
"dojo/dom", "dojo/dom-attr", "dijit/Toolbar", "dijit/form/Button", "dijit/Dialog","dijit/ProgressBar", "dojo/domReady!"],
function (layout, dom, domAttr) {
mapControl = new layout();
layout.js:
define(["dojo/_base/declare"], function(declare) {
return declare(null, {
action:"pan",
activeMeasureTool:"",
aerialLayer:"",
legendLayers:"",
loadedServices:"",
popup:"",
resizeId:0,
constructor: function() {
this.init();
},
init: function() {
require(["esri/map", "esri/config", "esri/SpatialReference", "esri/geometry/Extent"],
function(Map, config, SpatialReference, Extent) {
//custom map requires a proxy to function properly.
esri.config.defaults.io.proxyUrl = "../sdc_devdata/proxy.php";
var spatRef = new SpatialReference(2276);
var startExtent = new Extent(2481416.32087491, 6963246.42495962, 2501196.36936991, 6980267.92469462, spatRef);
var appFullExtent = new Extent(2396699.46935379, 6872369.60195443, 2607745.94404633, 7107335.22319087, spatRef);
map = new Map("map", {extent: startExtent, isZoomSlider:true, logo:false, sliderStyle:"large"});
this.attachMapEventHandlers();
this.createLayers();
this.handleLayerVisibilityChange();
});
},

There are a couple of things that you could do to resolve this.
Firstly, you could add your required dependency to the define array so you don't need to do an asynchronous require within the constructor of the class.
That would look like:
define(["dojo/_base/declare", "esri/map", "esri/config", "esri/SpatialReference", "esri/geometry/Extent"], function (declare, Map, config, SpatialReference, Extent) {
return declare(null, {
action: "pan",
activeMeasureTool: "",
aerialLayer: "",
legendLayers: "",
loadedServices: "",
popup: "",
resizeId: 0,
constructor: function () {
this.init();
},
init: function () {
//custom map requires a proxy to function properly.
esri.config.defaults.io.proxyUrl = "../sdc_devdata/proxy.php";
var spatRef = new SpatialReference(2276);
var startExtent = new Extent(2481416.32087491, 6963246.42495962, 2501196.36936991, 6980267.92469462, spatRef);
var appFullExtent = new Extent(2396699.46935379, 6872369.60195443, 2607745.94404633, 7107335.22319087, spatRef);
map = new Map("map", {
extent: startExtent,
isZoomSlider: true,
logo: false,
sliderStyle: "large"
});
this.attachMapEventHandlers();
this.createLayers();
this.handleLayerVisibilityChange();
}
});
});
OR you could save the current scope of this to something in the closure when doing the require
init: function () {
var that = this;
require(["esri/map", "esri/config", "esri/SpatialReference", "esri/geometry/Extent"],
function (Map, config, SpatialReference, Extent) {
//custom map requires a proxy to function properly.
esri.config.defaults.io.proxyUrl = "../sdc_devdata/proxy.php";
var spatRef = new SpatialReference(2276);
var startExtent = new Extent(2481416.32087491, 6963246.42495962, 2501196.36936991, 6980267.92469462, spatRef);
var appFullExtent = new Extent(2396699.46935379, 6872369.60195443, 2607745.94404633, 7107335.22319087, spatRef);
map = new Map("map", {
extent: startExtent,
isZoomSlider: true,
logo: false,
sliderStyle: "large"
});
that.attachMapEventHandlers();
that.createLayers();
that.handleLayerVisibilityChange();
});
},
EDIT: your third option would be using lang.hitch, which lets you specify the scope of this in the the callback function. To use it, you would add dojo/_base/lang to you define() dependency list, and wrap the require callback in lang.hitch(this, function(){});
define(["dojo/_base/declare", "dojo/_base/lang"], function (declare, lang) {
return declare(null, {
//...
init: function () {
require(["esri/map", "esri/config", "esri/SpatialReference", "esri/geometry/Extent"],
lang.hitch(this, function (Map, config, SpatialReference, Extent) {
//this now refers to the instance of the class
}));
}
});
});
I would strongly suggest going with the first option, since it is consistent with the entire use of AMD (declaring what dependencies a module needs before it executes, rather than loading it on the fly).

You can put 'this' in another variable, like _this, and use _this in your internal function,like
init: function() {
var _this= this;
require(["esri/map", "esri/config", "esri/SpatialReference", "esri/geometry/Extent"],
function(Map, config, SpatialReference, Extent) {
...
_this.attachMapEventHandlers();
_this.createLayers();
_this.handleLayerVisibilityChange();

Related

JavaScript - Add Graphics to an existing Arcgis map

I have an ArcGis map created using following code. I have a button on the page which is supposed to add graphics to the map on click. But it throws a JavaScript error "Error: Tried to register widget with id==xxx but that id is already registered". Any clue is welcome.
<script>
var map;
require([
"esri/map",
// Map initialization code....
});
function addSecond() {
//add pre-defined geometries to map
var polygonSymbol = new SimpleFillSymbol();
var triangle = new Polygon({
"rings": [
[
[2426417, 8535508],
[4304933, 12292541],
[6183449, 8535508],
[2426417, 8535508]
]
],
"spatialReference": {
"wkid": 102100
}
});
map.graphics.add(new Graphic(triangle, polygonSymbol));
}
</script>
Jsfiddle - http://jsfiddle.net/L1peybqh/
The root problem as I mentioned in the question was, I should be able to add graphics to the map on click of a button. I was implementing the ArcGIS API incorrectly, I was supposed to add the event listener inside the required function. A simplified version of the code I ended up looked like this.
<script>
var map, editToolbar;
var mapObj;
require([
"esri/map",
"esri/toolbars/edit",
"esri/graphic",
"esri/geometry/Point",
"esri/geometry/Polyline",
"esri/geometry/Polygon",
"esri/symbols/SimpleLineSymbol",
"esri/symbols/SimpleFillSymbol",
"esri/symbols/TextSymbol",
"dojo/_base/event",
"dojo/parser",
"dojo/dom",
"dojo/dom-style",
"dijit/registry",
"dijit/Menu",
"dijit/form/ToggleButton",
"dijit/form/DropDownButton",
"dijit/CheckedMenuItem",
"dijit/layout/BorderContainer",
"dijit/layout/ContentPane",
"dojo/domReady!"
], function (
Map, Edit, Graphic,
Point, Polyline, Polygon,
SimpleLineSymbol, SimpleFillSymbol, TextSymbol,
event, parser, dom, domStyle, registry, Menu
) {
parser.parse();
domStyle.set(registry.byId("mainWindow").domNode, "visibility", "visible");
map = new Map("map", {
basemap: "streets",
center: [3.955, 59.338],
zoom: 3
});
map.on("load", createToolbar);
function addGraphics() {
//add pre-defined geometries to map
var polygonSymbol = new SimpleFillSymbol();
var polygon = new Polygon({
"rings": [
[
[-4226661, 8496372],
[-3835304, 8731187],
[-2269873, 9005137],
[-1213208, 8613780]
]
],
"spatialReference": {
"wkid": 102100
}
});
map.graphics.add(new Graphic(polygon, polygonSymbol));
}
function addSecond() {
var polygonSymbol = new SimpleFillSymbol();
console.log(mapObj.features[0].geometry.coordinates);
var triangle = new Polygon({
"rings":
mapObj.features[0].geometry.coordinates
,
"spatialReference": {
"wkid": 102100
}
});
map.graphics.add(new Graphic(triangle, polygonSymbol));
}
$('#x').click(function(){
addSecond();
});
});
</script>

dojo Cannot set property 'id' of undefined

I am making a dynamic GridContainer. However, an error occurred. console.log output a correct value is outputted. However, the execution error. i don't understand this situation.
my code.
define([ "dojo/parser", "dojo/dom", "dojo/dom-style",
"dijit/registry","dojo/on", "dojo/request", "dojo/dom-construct","dojo/json",
"dojo/_base/array", "dijit/Dialog","dijit/focus" ,"dijit/form/Button","dijit/form/Select","dijit/form/TextBox",
"dijit/form/CheckBox", "dojo/query","dojox/layout/GridContainer","dojox/widget/Portlet","dijit/layout/BorderContainer",
"dijit/layout/TabContainer", "dijit/layout/ContentPane"], function(parser, dom, domStyle,
registry,on, request, domConstruct,json, arrayUtil, Dialog,focus,Button, CheckBox,query,GridContainer,Portlet) {
function addTab(formCount,num){//make gridContainer ajax
var xhrArgs = {
url: '/checkData',
handleAs: "json",
content:{
seq:num
},
load:function(data){
var cont='';
++gridCounter;
cont+='<div dojoType="dojox.layout.GridContainer" class="test" doLayout="true" id="gc'+gridCounter+'" region="center" hasResizableColumns="false" opacity="0.3" nbZones="1" allowAutoScroll="false" withHandles="true" dragHandleClass="dijitTitlePaneTitle" minChildWidth="200" minColWidth="10" style="height:50%;">';
..........
}
}
function addGridContainer(id){
alert('vidgetAddgridNo:'+id);
var result='';
var xhrArgs = {
url: '/checkVidget',
handleAs: "json",
content:{
id:id
},
load: function(data){
++addPorNum;
result+=data;
var portletContent2 = [
domConstruct.create('div', {innerHTML: result})
];
var portlet2 = Portlet({
id: 'dynPortlet'+addPorNum,
closable: false,
title: ''+result,
content: portletContent2
});
makeGrid(portlet2);
}
}//end xhrArgs
var deferred = dojo.xhrGet(xhrArgs);
function makeGrid(por){
console.log('makeGrid in');
var selectedTab=registry.byId('tabContainer').get('selectedChildWidget');
var tabs=registry.byId("tabContainer");
var cPane=tabs.get("selectedChildWidget");
var grid=cPane.getChildren()[0];
var id=grid.id;
alert(registry.byId(id));=>[Widget dojox.layout.GridContainer,gc1]
registry.byId(id).addChild(por,0,0);=>Error:Cannot set property 'id' of undefined
}//end makeGrid()
}
plz help me..T.T
In your define() module list you defined the following modules:
"dijit/form/Button", "dijit/form/Select", "dijit/form/TextBox",
"dijit/form/CheckBox"
However, in your callback you only have:
Button, CheckBox
So, you're missing the Select and TextBox module here, meaning that the parameter CheckBox actually contains the module dijit/form/Select. Which means every module is shifted and none of them contain the actual value, try fixing that first.

Widgets inside Dojo dgrid OnDemandList

I'm trying to do something similar to this question but using an OnDemandList instead of an OnDemandGrid.
Here is what I have so far
define([
"dojo/_base/declare",
"dijit/_WidgetBase",
"dijit/_TemplatedMixin",
"dijit/_WidgetsInTemplateMixin",
"dgrid/OnDemandList",
"widget/CategoryItem",
"dojo/dom-construct",
"dojo/text!./templates/category-list.html"
], function(declare, _Widget, _TemplatedMixin, _WidgetsInTemplateMixin, OnDemandList, CategoryItem, domConstruct, template) {
var CatList = declare([OnDemandList]);
return declare([_Widget, _TemplatedMixin, _WidgetsInTemplateMixin], {
templateString: template,
baseClass: "category-list",
postCreate: function() {
this.inherited(arguments);
// Create OnDemandList, place in div defined in template.
var cat1 = this.cat1 = new CatList({
store: this.store,
renderRow: this.renderItem
}, this.categoriesLevel0);
},
renderItem: function(item) {
return new CategoryItem({
title: item.title
});
}
});
});
The problem is my renderItems function needs to somehow return a dom containing my custom widget. As it is now I get this error Error on domReady callback: Error: NotFoundError: DOM Exception 8
Yeah it definitely needs to return a dom node from renderRow. Assuming you're using _WidgetBase for CategoryItem it should work like:
renderItem: function(item) {
var category = new CategoryItem({
title: item.title
});
return category.domNode;
}
The example here: https://github.com/SitePen/dgrid/wiki/OnDemandList-and-OnDemandGrid does pretty much the same thing, except it uses put-selector, which is just constructing a div, attaching the widget to it and returning the new div.

Dojo instances of same widgets are not saparated

I have built a Dojo Widget for creating a list by entering values. the widget code is:
define(["dojo/_base/declare", "dijit/_WidgetBase", "dijit/_TemplatedMixin", 'dojo/text!apps/orders/templates/multiAddList.html', "dojo/dom", "dojo/on", "dojo/dom-construct", "dojo/dom-class", "dojo/query", "dijit/focus"],
function (declare, WidgetBase, TemplatedMixin, html, dom, on, domConstruct, domClass, query, focusUtil) {
return declare([WidgetBase, TemplatedMixin], {
templateString: html,
postCreate: function () {
this.inherited(arguments);
var that = this;
},
_checkIfEnter: function (e) {
if (e.which == 13) {
this._addUser();
}
},
_addUser: function () {
domClass.remove(this.ulAdded, "hidden");
var textToAdd = this.userTextToAdd.value;
var li = domConstruct.create("li", {}, this.ulAdded);
domConstruct.create("span", {innerHTML: textToAdd}, li);
var spanX = domConstruct.create("span", {class: 'icon-x right'}, li);
this.itemsArray.push(textToAdd);
this.userTextToAdd.value = "";
focusUtil.focus(this.userTextToAdd);
var that = this;
on(spanX, "click", function () {
domConstruct.destroy(li);
that.itemsArray.splice(that.itemsArray.indexOf(textToAdd), 1);
if (that.itemsArray.length == 0) {
domClass.add(that.ulAdded, "hidden");
}
});
},
itemsArray: []
});
});
It is all OK. However - when I instantiate it twice on same dialog like this:
allowedDomains = new MultiAddList();
allowedDomains.placeAt(dom.byId('allowedDomains'), 0);
pdlEmails = new MultiAddList();
pdlEmails.placeAt(dom.byId('pdlEmails'), 0);
and then asking for allowedDomains.itemsArray() or pdlEmails.itemsArray() - I get the same list (as if it is the same instance) - althought in the UI presentation - he adds the list items separately and correctly.
Obviously, I am doing something wrong although I followed Dojo examples.
Does anyone know what I should do in order to make it work?
Thanks
When you make a dojo class using declare, object and array members are static, meaning they are shared across instances, so I would suggest doing itemsArray: null and then this.itemsArray = [] in the constructor or postCreate somewhere.
Everything else looks fine, although I too would have a preference for using hitch, your solution is perfectly fine.
Sorry for just giving you a hint, but you might want to look at the dojo.hicth()-function, as an alternative to the "this-that" contruction
on(spanX, "click", dojo.hitch(this, function () {
domConstruct.destroy(li);
this.itemsArray.splice(this.itemsArray.indexOf(textToAdd), 1);
if (this.itemsArray.length == 0) {
domClass.add(this.ulAdded, "hidden");
}
}));
The on-construct is a good one, but just testing this kind of construct might tell you whether that is the problem or not.
_addUser: function () {
.....
.....
dojo.connect(spanX, "click", this, this.spanClicked);
or
dojo.connect(spanX, "click", dojo.hitch(this, this.spanClicked);
},
spanClicked: function(args) {
domConstruct.destroy(li); //need to keep some reference to li
this.itemsArray.splice(this.itemsArray.indexOf(textToAdd), 1);
if (that.itemsArray.length == 0) {
domClass.add(this.ulAdded, "hidden");
}
}

Routing/Modularity in Dojo (Single Page Application)

I worked with backbone before and was wondering if there's a similar way to achieve this kind of pattern in dojo. Where you have a router and pass one by one your view separately (like layers) and then you can add their intern functionality somewhere else (e.g inside the view) so the code is very modular and can be change/add new stuff very easily. This code is actually in jquery (and come from a previous project) and it's a "common" base pattern to develop single application page under jquery/backbone.js .
main.js
var AppRouter = Backbone.Router.extend({
routes: {
"home" : "home"},
home: function(){
if (!this.homeView) {
this.homeView= new HomeView();
}
$('#content').html(this.homeView.el);
this.homeView.selectMenuItem('home-link');
}};
utils.loadTemplate(['HomeView'], function() {
app = new AppRouter();
Backbone.history.start();
});
utils.js
loadTemplate: function(views, callback) {
var deferreds = [];
$.each(views, function(index, view) {
if (window[view]) {
deferreds.push($.get('tpl/' + view + '.html', function(data) {
window[view].prototype.template = _.template(data);
}));
} else {
alert(view + " not found");
}
});
$.when.apply(null, deferreds).done(callback);
}};
HomeView.js
window.HomeView = Backbone.View.extend({
initialize:function () {
this.render();
},
render:function () {
$(this.el).html(this.template());
return this;
}
});
And basically, you just pass the html template. This pattern can be called anywhere with this link:
<li class="active"><i class="icon-home"></i> Dashboard</li>
Or, what is the best way to implement this using dojo boilerplate.
The 'boilerplate' on this subject is a dojox.mvc app. Reference is here.
From another aspect, see my go at it a while back, ive setup an abstract for 'controller' which then builds a view in its implementation.
Abstract
Then i have an application controller, which does following on its menu.onClick
which fires loading icon,
unloads current pane (if forms are not dirty)
loads modules it needs (defined 'routes' in a main-menu-store)
setup view pane with a new, requested one
Each view is either simply a server-html page or built with a declared 'oocms' controller module. Simplest example of abstract implementation here . Each implements an unload feature and a startup feature where we would want to dereference stores or eventhooks in teardown - and in turn, assert stores gets loaded etc in the setup.
If you wish to use templates, then base your views on the dijit._TemplatedMixin
edit
Here is a simplified clarification of my oocms setup, where instead of basing it on BorderLayout, i will make it ContentPanes:
Example JSON for the menu, with a single item representing the above declared view
{
identifier: 'view',
label: 'name',
items: [
{ name: 'myForm', view: 'App.view.MyForm', extraParams: { foo: 'bar' } }
]
}
Base Application Controller in file 'AppPackagePath/Application.js'
Note, the code has not been tested but should give a good impression of how such a setup can be implemented
define(['dojo/_base/declare',
"dojo/_base/lang",
"dijit/registry",
"OoCmS/messagebus", // dependency mixin which will monitor 'notify/progress' topics'
"dojo/topic",
"dojo/data/ItemFileReadStore",
"dijit/tree/ForestStoreModel",
"dijit/Tree"
], function(declare, lang, registry, msgbus, dtopic, itemfilereadstore, djforestmodel, djtree) {
return declare("App.Application", [msgbus], {
paneContainer: NULL,
treeContainer: NULL,
menuStoreUrl: '/path/to/url-list',
_widgetInUse: undefined,
defaultPaneProps: {},
loading: false, // ismple mutex
constructor: function(args) {
lang.mixin(this, args);
if(!this.treeContainer || !this.paneContainer) {
console.error("Dont know where to place components")
}
this.defaultPaneProps = {
id: 'mainContentPane'
}
this.buildRendering();
},
buildRendering: function() {
this.menustore = new itemfilereadstore({
id: 'appMenuStore',
url:this.menuStoreUrl
});
this.menumodel = new djforestmodel({
id: 'appMenuModel',
store: this.menustore
});
this.menu = new djtree( {
model: this.menumodel,
showRoot: false,
autoExpand: true,
onClick: lang.hitch(this, this.paneRequested) // passes the item
})
// NEEDS a construct ID HERE
this.menu.placeAt(this.treeContainer)
},
paneRequested: function(item) {
if(this.loading || !item) {
console.warn("No pane to load, give me a menustore item");
return false;
}
if(!this._widgetInUse || !this._widgetInUse.isDirty()) {
dtopic.publish("notify/progress/loading");
this.loading = true;
}
if(typeof this._widgetInUse != "undefined") {
if(!this._widgetInUse.unload()) {
// bail out if widget says 'no' (isDirty)
return false;
}
this._widgetInUse.destroyRecursive();
delete this._widgetInUse;
}
var self = this,
modules = [this.menustore.getValue(item, 'view')];
require(modules, function(viewPane) {
self._widgetInUse = new viewPane(self.defaultProps);
// NEEDS a construct ID HERE
self._widgetInUse.placeAt(this.paneContainer)
self._widgetInUse.ready.then(function() {
self.paneLoaded();
})
});
return true;
},
paneLoaded: function() {
// hide ajax icons
dtopic.publish("notify/progress/done");
// assert widget has started
this._widgetInUse.startup();
this.loading = false;
}
})
})
AbstractView in file 'AppPackagePath/view/AbstractView.js':
define(["dojo/_base/declare",
"dojo/_base/Deferred",
"dojo/_base/lang",
"dijit/registry",
"dijit/layout/ContentPane"], function(declare, deferred, lang, registry, contentpane) {
return declare("App.view.AbstractView", [contentpane], {
observers: [], // all programmatic events handles should be stored for d/c on unload
parseOnLoad: false,
constructor: function(args) {
lang.mixin(this, args)
// setup ready.then resolve
this.ready = new deferred();
// once ready, create
this.ready.then(lang.hitch(this, this.postCreate));
// the above is actually not nescessary, since we could simply use onLoad in contentpane
if(typeof this.content != "undefined") {
this.set("content", this.content);
this.onLoad();
} else if(typeof 'href' == "undefined") {
console.warn("No contents nor href set in construct");
}
},
startup : function startup() {
this.inherited(arguments);
},
// if you override this, make sure to this.inherited(arguments);
onLoad: function() {
dojo.parser.parse(this.contentNode);
// alert the application, that loading is done
this.ready.resolve(null);
// and call render
this.render();
},
render: function() {
console.info('no custom rendering performed in ' + this.declaredClass)
},
isDirty: function() { return false; },
unload: function() {
dojo.forEach(this.observers, dojo.disconnect);
return true;
},
addObserver: function() {
// simple passthrough, adding the connect to handles
var handle = dojo.connect.call(dojo.window.get(dojo.doc),
arguments[0], arguments[1], arguments[2]);
this.observers.push(handle);
}
});
});
View implementation sample in file 'AppPackagePath/view/MyForm.js':
define(["dojo/_base/declare",
"dojo/_base/lang",
"App/view/AbstractView",
// the contentpane href will pull in some html
// in the html can be markup, which will be renderered when ready
// pull in requirements here
"dijit/form/Form", // markup require
"dijit/form/Button" // markup require
], function(declare, lang, baseinterface) {
return declare("App.view.MyForm", [baseinterface], {
// using an external HTML file
href: 'dojoform.html',
_isDirty : false,
isDirty: function() {
return this._isDirty;
},
render: function() {
var self = this;
this.formWidget = dijit.byId('embeddedForm') // hook up with loaded markup
// observer for children
dojo.forEach(this.formWidget._getDescendantFormWidgets(), function(widget){
if(! lang.isFunction(widget.onChange) )
console.log('unable to observe ' + widget.id);
self.addObserver(widget, 'onChange', function() {
self._isDirty = true;
});
});
//
},
// #override
unload: function() {
if(this.isDirty()) {
var go = confirm("Sure you wish to leave page before save?")
if(!go) return false;
}
return this.inherited(arguments);
}
})
});