I'm building an app in AngularJS that uses LeafletJS for interacting with a map, offering different possible interactions separated across what I call phases. For each of those phases there is a UIRouter state, with its controller and template.
I'm currently providing the leaflet functionality through a service. The idea was for that service to initialise a Leaflet map and provide some limited access to the state's controller. Those controllers would thus call service functions such as setupMarkersInteractions to setup callbacks that enable marker placement on the map, for example.
However, I'm running into a problem when initialising the map through Leaflet's leaflet.map() function, namely: Error: Map container not found. This is related to Leaflet's inability to find the HTML element with which the map should be associated.
Currently, I'm kinda doing this:
function mapService() {
var map;
return {
initializeMap : initializeMap,
setupMarkersInteractions : setupMarkersInteractions
};
function initializeMap() {
map = leaflet.map('map');
}
function setupMarkersInteractions() {
map.on('click', markerPlacementCallback);
}
}
The initializeMap function tells leaflet.map to look for a HTML element with id='map', which is declared on the state's template.
Now, for the actual question, is this related to some kind of AngularJS services' inability to access the HTML template? I couldn't find anything on the matter, but I thought that it would make sense for services to not directly access the view...
If it is, what kind of workaround should I explore? I've looked into leaflet-directive, but it doesn't seem to offer the possibility to add and remove custom callbacks with the flexibility I would like to (things get complex when I add free draw functionality with Leaflet-Freedraw, for example).
I considered using leaflet.map directly with an HTMLElement argument for the element but still I couldn't make it work - although there is a probability that I'm not passing what is supposed to.
What's happening is that at the moment L.Map tries to access the DOM from your service, the template is available yet. Normally services get loaded and injected into controllers, controllers initialize their scopes, after that the templates get initialized and added to DOM. You'll see if you'll put a large timeout on your map initialization that it will find it's DOM element. But that's a very ugly hack. In Angular you should use a directive to add logic to DOM elements.
For example, a template: <leaflet></leaflet> and it's very basic directive:
angular.module('app').directive('leaflet', [
function () {
return {
replace: true,
template: '<div></div>',
link: function (scope, element, attributes) {
L.map(element[0]);
}
};
}
]);
You can hook that up to your service and pass the element to your initialization method:
angular.module('app').directive('leaflet', [
'mapService'
function (mapService) {
return {
replace: true,
template: '<div></div>',
link: function (scope, element, attributes) {
mapService.initializeMap(element[0]);
}
};
}
]);
That way the initializeMap method will only be called once the actual DOM element is available. But it presents you with another problem. At the moment your controller(s) are initialized, your service is not ready yet. You can solve this by using a promise:
angular.module('app').factory('leaflet', [
'$q',
function ($q) {
var deferred = $q.defer();
return {
map: deferred.promise,
resolve: function (element) {
deferred.resolve(new L.Map(element));
}
}
}
]);
angular.module('app').directive('leaflet', [
'leaflet',
function (leaflet) {
return {
replace: true,
template: '<div></div>',
link: function (scope, element, attributes) {
leaflet.resolve(element[0]);
}
};
}
]);
If you want to use the map instance in your controller you can now wait untill it's resolved:
angular.module('app').controller('rootController', [
'$scope', 'leaflet',
function ($scope, leaflet) {
leaflet.map.then(function (map) {
var tileLayer = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap',
maxZoom: 18
}).addTo(map);
map.setView([0, 0], 1);
L.marker([0, 0]).addTo(map);
});
}
]);
Here's an example of the concept on Plunker: http://plnkr.co/edit/DoJpGqtR7TWmKAeBZiYJ?p=preview
Related
I'm very much trying to get my head around angularJS and directives still.
I have an existing REST service that outputs JSON data as follows (formatted for readability):
{"ApplicationType":
["Max","Maya","AfterEffects","Nuke","WebClient","Other"],
"FeatureCategory":
["General","Animation","Compositing","Management","Other"],
"FeatureStatus":
["Completed","WIP","NotStarted","Cancelled","Rejected","PendingReview"],
"BugStatus":
["Solved","FixInProgress","NotStarted","Dismissed","PendingReview"]}
I then have a service (which appears to be working correctly) to retrieve that data that I wish to inject into my directive.
(function () {
'use strict';
var enumService = angular.module('enumService', ['ngResource']);
enumService.factory('Enums', ['$resource',
function ($resource) {
return $resource('/api/Enums', {}, {
query: { method: 'GET', cache: false, params: {}, isArray: false }
});
}
]); })();
My intentions are to use the data from the json response to bind to html selector 'options' for the purposes of keeping the data consistent between the code behind REST service and the angular ( ie. the json data is describing strongly typed model data from c# eg. Enum.GetNames(typeof(ApplicationType)) )
projMgrApp.directive('enumOptions', ['Enums',
function (Enums) {
return {
restrict: 'EA',
template: '<option ng-repeat="op in options">{{op}}</option>',
scope: {
key: '#'
},
controller: function($scope) { },
link: function (scope, element, attrs) {
scope.options = Enums.query(function (result) { scope.options = result[scope.key]; });
}
};
}
]);
the intended usage would be to use as follows:
<label for="Application" class="control-label col-med-3 col">Application:</label>
<select id="Application" class="form-control col-med-3 col pull-right">
<enum-options key="ApplicationType"></enum-options>
</select>
which would then produce all of the options consistent with my c# enums.
In this case it appears the directive is never being called when the tag is used.
Note. I assume the factory is working fine, as i can inject it into a separate controller and it works as anticipated.
1) I guess projMgrApp is the main module. Have you included enumService as dependency to this module?
angular.module('projMgrApp',['enumServices'])
Otherwise your main module won't be aware of the existence of your service.
2) Are you aware how the directives are declared and used. When you declare
projMgrApp.directive('EnumOptions', ['Enums', function (Enums) {}])
actually should be used in the html code as:
<enum-options></enum-options>
I m not quite sure about the name but it should start with lowercase letter like enumOptions
3) I don't know why you use this key as attribute. You don't process it at all. scope.key won't work. You either have to parse the attributes in link function (link: function(scope, element, attributes)) or create isolated scope for the directive.
Add as property of the return object the following:
scope : {
key:'#' //here depends if you want to take it as a string or you will set a scope variable.
}
After you have this , you can use it in the link function as you did (scope.key).
Edit:
Here is a working version similar (optimized no to use http calls) to what you want to achieve. Tell me if I'm missing anything.
Working example
If you get the baddir error try to rename your directive name to enumOptions according to the doc (don't forget the injection):
This error occurs when the name of a directive is not valid.
Directives must start with a lowercase character and must not contain leading or trailing whitespaces.
Thanks to Tek's suggestions I was able to get this fully functioning as intended. Which means now my angularJS + HTML select tags/directives are completely bound to my APIs enums. I know down the line that I am going to be needing to adjust add to these occasionally based on user feedback, plus I use these enums to represent strongly typed data all over the app. So it will be a big time saver and help reduce code repetition. Thanks to everyone that helped!
The service and directive that I used is copied below, in case other people starting out with Angular run into similar issues or have similar requirements.
Service:
(function () {
'use strict';
var enumService = angular.module('enumService', [])
.service('Enums', ['$http', '$q', function ($http, $q) {
return {
fetch: function () {
var defer = $q.defer();
var promise = $http.get("/api/Enums").then(function (result) {
defer.resolve(result);
});
return defer.promise;
}
}
}]);})();
Directive:
angular.module('projMgrApp').directive('enumOptions', ['Enums', function (Enums) {
return {
restrict: "EA",
scope: {
key: "#"
},
template: "<select><option ng-repeat='enum in enumIds' ng-bind='enum'></option><select>",
link: function (scope) {
Enums.fetch().then(function (result) {
scope.enumIds = result.data[scope.key];
});
}
};
}]);
I have this directive that is getting more and more complicated. So I decided to split it up into parts.
The directive itself loaded a garment SVG graphic, when the SVG loaded it then ran a configure method which would apply a design, applied picked colours (or database colours if editing) and other bits and pieces.
As I said, it was all in one directive, but I have now decided to separate the logic out.
So I created my first directive:
.directive('configurator', function () {
// Swap around the front or back of the garment
var changeView = function (element, orientation) {
// If we are viewing the front
if (orientation) {
// We are viewing the front
element.addClass('front').removeClass('back');
} else {
// Otherwise, we are viewing the back
element.addClass('back').removeClass('front');
}
};
return {
restrict: 'A',
scope: {
garment: '=',
onComplete: '&'
},
require: ['configuratorDesigns'],
transclude: true,
templateUrl: '/assets/tpl/directives/kit.html',
link: function (scope, element, attrs, controllers) {
// Configure our private properties
var readonly = attrs.hasOwnProperty('readonly') || false;
// Configure our scope properties
scope.viewFront = true;
scope.controls = attrs.hasOwnProperty('controls') || false;
scope.svgPath = 'assets/garments/' + scope.garment.slug + '.svg';
// Apply the front class to our element
element.addClass('front').removeClass('back');
// Swaps the design from front to back and visa versa
scope.rotate = function () {
// Change the orientation
scope.viewFront = !scope.viewFront;
// Change our view
changeView(element, scope.viewFront);
};
// Executes after the svg has loaded
scope.loaded = function () {
// Call the callback function
scope.onComplete();
};
}
};
})
This is pretty simple in design, it gets the garment and finds the right SVG file and loads it in using ng-transclude.
Once the file has loaded a callback function is invoked, this just tells the view that it is on that it has finished loading.
There are a few other bits and pieces that you should be able to work out (changing views, etc).
In this example I am only requiring one other directive, but in the project there are 3 required directives, but to avoid complications, one will suffice to demonstrate my problem.
My second directive is what is needed to apply the design. It looks like this:
.directive('configuratorDesigns', function () {
return {
restrict: 'A',
controller: 'ConfiguratorDesignsDirectiveController',
link: function (scope, element, attrs, controller) {
// Get our private properties
var garment = scope.$eval(attrs.garment),
designs = scope.$eval(attrs.configuratorDesigns);
// Set our controller designs array
controller.designs = designs;
// If our design has been set, watch it for changes
scope.$watch(function () {
// Return our design
return garment.design;
}, function (design) {
// If we have a design
if (design) {
// Change our design
controller.showDesign(element, garment);
}
});
}
}
})
The controller for this directive just loops through the SVG and finds the design that matches the garment design object. If it finds it, it just hides the others and shows that one.
The problem I have is that this directive is unaware of the SVG loading or not. In the "parent" directive I have the scope.loaded function which is executed when the SVG has finished loading.
The "parent" directive's template looks like this:
<div ng-transclude></div>
<div ng-include="svgPath" onload="loaded()"></div>
<span class="glyphicon glyphicon-refresh"></span>
So my question is this:
How can I get the required directives to be aware of the SVG loaded state?
If I understand your question correctly, $rootScope.broadcast should help you out. Just broadcast when the loading is complete. Publish a message from the directive you are loading the image. On the directive which needs to know when the loading is complete, listen for the message.
I have a very particular scenario, and I can't identify the cause of it.
Brief overview of my situation:
I'm developing a small little application, as part of my learning TypeScript, and using AngularJS seemed to be a challenge almost everyone new to the TS system faces, so why not.
I have an app.ts file, that basically sets up required global variables and initializes the data-ng-app="" part required by AngularJS.
The app.ts file:
module Learning.AngularTS.Core
{
"use strict";
// ... environmental variables left out
export var AngularTSApp: ng.IModule = angular.module("angularTSApp", []);
export var ngCompileProvider: ng.ICompileProvider = null;
AngularTSApp.config(function (
$compileProvider: ng.ICompileProvider,
$controllerProvider: ng.IControllerProvider): void
{
(<any>$controllerProvider).allowGlobals();
ngCompileProvider = $compileProvider;
});
}
Nothing special, a module just creates a 'namespaced' reference for me, and in here I export some variables, but the one of interest is the AngularTSApp variable, which needs it's configuration properties set.
In the .config() I'm attaching the reference to the Angular's CompileProvider to the variable ngCompileProvider, as per the code I posted.
The particular 'issue', I'm experiencing is when I'm trying to attach a Directive to the page, where the .config() only gets called after the directive, and I don't know why.
The directive:
module Learning.AngularTS.Directives
{
"use strict";
Learning.AngularTS.Core.ngCompileProvider.directive.apply(null,
["MyTestDirective",
["$parse", "$compile",
function ($parse: ng.IParseService,
$compile: ng.ICompileService): ng.IDirective
{
return <ng.IDirective>{
restrict: "E",
replace: false,
transclude: true,
templateUrl: "",
scope: {
value: "=",
edit: "#"
},
link: function ($scope: any, $rootElement: ng.IRootElementService, $attributes: ng.IAttributes): any
{
return null;
}
}
}]]);
}
On face value, this looks fine, when the JS file loads, Learning.AngularTS.Core.ngCompileProvider is null, because it has not been initialized as per the .config() section in app.ts.
Is there something wrong with my approach, and if so, is there a better way to do this?
I'm a pretty experienced Wicket developer, but curious about AngularJS, so I decided to give it a try. Most things are pretty straightforward when coming from Java (DI etc).
However, I was not able to find a (Object Oriented) strategy for layout of components in my page. In Wicket, you can pack view and behaviour together in a Component, for example a Panel. You can create multiple subclasses for a certain component, and decide which subclass to use in Java.
In my case, I develop a (abstract) Game which has a Board component. Based on the type of game, a different Board will be renderend. Other components, like the title bar, scoring, end-of-game animation etc. will be the same for all games. In Wicket, this would look something like this:
Board b = myGame.newBoardPanel(id);
add(b);
Where the game can provide an abstract method to provide a Board.
I tried looking at views, but it seems hard to combine multiple different dynamic components in them. In my case, next to the Board, I have an AnswerPanel which can also differ from game to game, but for some games, it will be the same.
My next move was to use directives; but it seems AngularJS is not designed to choose layout from the controller, as it is not easy to change (in my case) the templateURL to implement different layout.
TL;DR
What is the Angular way to implement different components in a OO-style?
In angular you would use directives to reuse elements in the game.
Directives and element can have a controller associated with them. The controllers in angular are expected to be functions, which internally are instantiated using the new keyword, so you can use classes. And if you use classes you can use inheritance to share logic between your controllers.
You can modify the contents of a directive HTML node ($element), but you need to load and compile the template manually.
<!-- Use directives to reuse elements -->
<div ng-controller="GameController as gameCtrl">
<title-bar title="gameCtrl.title"></title-bar>
<answer-panel answers="gameCtrl.answers"></answer-panel>
<!-- Pass data into your directives via attributes, from the controller -->
<board type="gameCtrl.boardType"></board>
</div>
ES6
// controllers can be classes, as such they can be extended via mixins
// or OO extend.
class GameController {
constructor($location, gameRepository) {
var ctrl = this;
var params = $location.search();
gameRepository.get(params.id).then(function (game) {
// store the data in the controller instead of the $scope,
// this gives you context to where your data is coming from in
// the templates
ctrl.title = game.title;
ctrl.answers = game.answers
ctrl.boardType = game.boardType;
});
}
}
The title-bar directive would be very simple:
function titleBarDirective() {
return {
restrict: 'E',
// this is a convenience for angular to associate the evaluation of the
// element attribute 'title' to the $scope.title
scope: {
title: '='
},
template: '<span ng-bind="title"></span>'
};
}
For the board, you can include your custom $compiled template, this might not work exactly but I hope it gives you a good starting point.
function boardDirective() {
return {
restrict: 'E',
scope: {
type: '='
},
controller: BoardController,
controllerAs: 'boardCtrl'
};
}
class BoardController {
constructor($scope, $element, $compile, $templateRequest) {
var ctrl = this;
ctrl.$scope = $scope;
ctrl.$element = $element;
ctrl.$compile = $compile;
ctrl.$templateRequest = $templateRequest;
// check for changes to the "type", because in this example the
// type is coming from the gameRepository, so this directive could
// be initialized before a request with the game type comes
// from the server.
$scope.$watch('type', function (newType) {
if (!newType) return;
ctrl._loadBoard(newType);
});
}
_loadBoard(type) {
var ctrl = this;
// manually request the template
ctrl.$templateRequest('/templates/board/' + type + '.html')
.then(function (template) {
// manually compile the template
var boardTpl = ctrl.$compile(template):
// manually add the link of the template with a scope to
// the directive element
ctrl.$element.append(boardTpl(ctrl.$scope));
});
}
}
I've created a small app that has 2 directives: One that adds a google map div and initialize it, and the second, which shows layers that contain markers. The directives are independent, but I want them to communicate: The layers directive is independent, yet it needs to use the google maps directive to add markers to the map. I used $broadcast through $rootScope to communicate between the directives.
The directives are defined as follows:
angular.module('googleMapModule', [])
.directive('myGoogleMap', function(){
template: '<div id="map" />',
controller: function($scope){
// some initializations
// Listen for event fired when markers are added
$scope.$on('addMarkersEvent', function(e, data){
// do something
}
}
}
angular.module('layersDirective', [])
.directive('myLayers', function() {
templateUrl: 'someLayersHtml.html',
controller: function($http, $scope, $rootScope){
// Get layers of markers, etc.
// On specific layer click, get markers and:
$rootScope.broadcast('addMarkersEvent', {
data: myMarkers
});
}
});
After this long prologue here are my questions:
How should the connection between the two directives be implemented? Is it correct to use $rootScope and $broadcast or should there be a dependency between the myLayers directive and the myGoogleMap directive?
Furthermore, I've read about when to use controller, link and compile, yet I don't know the right way to use them here. My guess is that myGoogleMap should define its API in a controller and that myLayers should be dependent on myGoogleMap.
The example I wrote here works fine in my application. I'm looking for guidance on how to do it right and to understand what I did wrong here.
Thanks, Rotem
There are some ways for directives to cooperate/communicate
If one is a sibling or child of the other, you can use require. E.g. for this template:
<dir1>
<dir2></dir2>
</dir1>
Use this code:
app.directive('dir1', function() {
return {
...
controller: function() {
// define api here
}
};
});
app.directive('dir2', function() {
return {
...
require: '^dir1',
link: function(scope, elem, attrs, dir1Controller) {
// use dir1 api here
}
};
});
A service, used by both directives to communicate. This is easy and works well if the directives can only be instantiated once per view.
Using $broadcast/$emit on the $rootScope (there is a slight difference: $broadcast will "flood" the scope hierarchy, possibly affecting performance; $emit will only call listeners on the $rootScope, but this means you have to do $rootScope.$on() and then remember to deregister the listener when the current scope is destroyed - which means more code). This approach is good for decoupling components. It may become tricky in debugging, i.e. to find where that event came from (as with all event-based systems).
Other
controller, link and compile
Very short:
Use the controller to define the API of a directive, and preferably to define most its logic. Remember the element, the attributes and any transclude function are available to the controller as $element, $attrs and $transclude respectively. So the controller can, in most cases, replace the link function. Also remember that, unlike the link function, the controller is elligible for dependency injection. (However you can still do dependency injection at the directive level, so, after all, the link function can also access dependencies.)
Use the link function to access required controllers (see case 1 above). Or, if you are feeling lazy, to define the directive's logic. I think the controller is cleaner though.
Use the compile function... rarely :) When you need very special transformations to the template (repetition is the first thing that comes to mind - see ng-repeat) or other mystical stuff. I use directives all the time, about 1% of them needs a compile function.
My guess is that myGoogleMap should define its API in the controller and that myLayers should be dependent on myGoogleMap
The question is how will you communicate this API using events? You probably need only to create an API in a custom event object. The listeners of your event will be using that custom event. If so, the controller does not really need to define an API.
As a bottom line, I am perfectly OK with using events in your case.
Generally communication between directives should be handled via controllers and using the require property on the directive definition object.
If we re-work your first directive we can simply add that method to the controller:
directive('myGoogleMap', function () {
return {
template: '<div id="map" />',
controller: function ($scope) {
var _this = this;
//Part of this directives API
_this.addMarkers = function(arg){
//Do stuff
}
}
};
});
Now we can require this controller in another directive, but one little known features is that you can actually require an array of directives. One of those directives can even be yourself.
All of them will be passed, in order, as an array to your link function:
directive('myLayers', function () {
return {
templateUrl: 'someLayersHtml.html',
controller: function ($http, $scope, $rootScore) {
// Some get layers of markers functionality
},
// Require ourselves
// The '?' makes it optional
require: ['myLayers', '?myGoogleMap'],
link: function(scope, elem, attrs, ctrls){
var myLayersCtrl = ctrls[0];
var myGoogleMapCtrl = ctrls[1];
//something happens
if(myGoogleMapCtrl) {
myGoogleMapCtrl.addMarkers(markers);
}
}
};
});
Now you can communicate explicitly opt-in by using the ? which makes the controller optional.
In order for that to work, you have to define both directives in the same module, i.e.:
var module = angular.module('myModule');
module.directive('myGoogleMap', function(){
template: '<div id="map" />',
controller: function($scope){
// some initializations
// Listen to event for adding markers
$scope.$on('addMarkersEvent', function(e, data){
// do something
}
}
}
module.directive('myLayers', function() {
templateUrl: 'someLayersHtml.html',
controller: function($http, $scope, $rootScore){
// Some get layers of markers functionality
// On specific layer click, get markers and:
$rootScope.broadcast('addMarkersEvent', {
data: myMarkers
});
}
});
Read more here.
EDIT:
Sorry i didn't understand your question, but according to your comment, quoting from the AngularJs Best Practices:
Only use .$broadcast(), .$emit() and .$on() for atomic events
that are relevant globally across the entire app (such as a user
authenticating or the app closing). If you want events specific to
modules, services or widgets you should consider Services, Directive
Controllers, or 3rd Party Libs
$scope.$watch() should replace the need for events
Injecting services and calling methods directly is also
useful for direct communication
Directives are able to directly communicate with each other through directive-controllers
You have already highlight one may for the directives to communicate using rootscope.
Another way directive can communicate if they are defined on the same html hierarchy is by use directive controller function. You have highlighted that too in your question. The way it would be done is (assuming myGoogleMap is defined on parent html), the two directive definitions become:
angular.module('googleMapModule', [])
.directive('myGoogleMap', function () {
template: '<div id="map" />',
controller: function ($scope) {
this.addMarkersEvent = function (data) {}
// some initializations
}
angular.module('layersDirective', [])
.directive('myLayers', function ($http, $rootScope) {
templateUrl: 'someLayersHtml.html',
require: '^myGoogleMap',
link: function (scope, element, attrs, myGoogleMapController) {
$scope.doWork = function () {
myGoogleMapController.addMarkersEvent(data);
}
}
});
Here you use the require property on the child directive. Also instead of using child controller all the functionality in the child directive is now added to link function. This is because the child link function has access to parent directive controller.
Just a side note, add both directives to a single module (Update: Actually you can have the directives in different modules, as long as there are being referenced in the main app module.)