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));
});
}
}
Related
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
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'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.)
New to Angular and, so far, I'm loving it but the learning curve seems pretty steep. What I want to do is wrap up a bit of simple business logic and build some re-usable DOM components to template common areas of my system. Specifically I am writing a simple survey application that has different question types. My goal is to get to the point that while I am in an ng-repeat block I can do something like this:
<div ng-repeat="question in questions">
<show-question></show-question>
</div>
Ideally I want to wrap all of the logic into that one statement to switch on question type and then pull from templateUrl for different HTML sets. So if a question.type = "text" it would pull the templateUrl of "templates/textQuestion.html" and be able to inject scope into that template file as it produces the DOM element.
Big question is, am I going about this correctly AT ALL? Is a directive the way to go, should I even try to do this all in one directive/tag? I am open to being schooled on this!
Small question is, if I am going the right direction, what is the correct implementation?
I have already tried putting some logic inside my directives like IF and SWITCH, but that doesn't appear to be valid.
Any and all (constructive) help is welcome.
Thanks all!
It's called a directive. There's a complete guide here: http://docs.angularjs.org/guide/directive
It allows you to make custom attributes, elements, CSS classes, and comments that turn into components.
I wouldn't pull separate templates for each question type, I'd use a different directive for each question type. Then you can switch between them using a parent directive.
Here is what a directive that loads different directives might look like:
app.directive('question', function($compile){
"use strict";
return{
restrict: 'E',
replace: true,
link: function(scope, element, attrs){
var render = function(){
var template = "";
switch(attrs.type){
case "truefalse":
template = '<truefalse></truefalse>';
break;
case "multiplechoice":
template = '<multiplechoice></multiplechoice>';
break;
case "essay":
template = '<essay></essay>';
break;
}
element.html(template);
$compile(element.contents())(scope);
}
attrs.$observe('type', function(value) {
render();
});
render();
}
};
});
Now you could use this as such:
<question ng-repeat="question in questions" type="question.type" ></question>
Assuming you had a directive for each type of question, you'd get different directives rendered. This is sort of similar to using ng-if or different templates or whatever but I like it more because I also get re-usable one-off components.
So if your scope variable questions has all the info for each question like
$scope.questions = [
{ type: 'input',
prompt: 'name'
}
];
Then you might have some html that looks like
<div question="question" ng-repeat="question in questions"></div>
And have a directive that looks something like
app.directive('question', function() {
return {
scope: {
question: '=' // Creates 2 way data binding with the object you passed in as attribute by the same name
},
link: function($scope, $element, $attrs) {
$scope.question; // This will be equal the object you passed in
// { type: 'input', prompt: 'name' }
// You can modify the dom or whatever here
}
};
});
If you want to have different prepared templates then you can inject the $templateCache into your directive
app.directive('question', function($templateCache) {
and then call them in your link function
link: function($scope, $element, $attrs) {
var template = $templateCache.get('path/to/template.html');
// and append it to the element
$element.append(template);
}
You'll have to play around with it a bit, but that's half the fun. Good luck!
I have been thinking about directives in Angularjs like user controls in ASP.Net, and perhaps I have it wrong.
A user control lets you encapsulate a bunch of functionality into a widget that can be dropped into any page anywhere. The parent page doesn't have to provide anything to the widget. I am having trouble getting directives to do anything close to that. Suppose that I have an app where, once the user has logged in I hang onto the first/last name of the user in a global variable somewhere. Now, I want to create a directive called 'loggedinuser' and drop it into any page I want. It will render a simple div with the name of the logged in user pulled from that global variable. How do I do that without having to have the controller pass that information into the directive? I want the usage of the directive in my view to look as simple as
<loggedinuser/>
Is this possible?
I guess you can roughly sum up what a directive is as "something that encapsulates a bunch of functionality into a widget that can be dropped into any page anywhere", but there's more to it than that. A directive is a way to extend HTML by creating new tags, allowing you to write more expressive markup. For instance, instead of writing a <div> and a bunch of <li> tags in order to create a rating control, you could wrap it up with a new <rating> tag. Or, instead of lots of <div>s, and <span>s and whatnot to create a tabbed interface, you could implement a pair of directives, say, <tab> and <tab-page>, and use them like this:
<tab>
<tab-page title="Tab 1"> tab content goes here </tab-page>
<tab-page title="Tab 2"> tab content goes here </tab-page>
</tab>
That's the truly power of directives, to enhance HTML. And that doesn't mean that you should only create "generic" directives; you can and should make components specific to your application. So, back to your question, you could implement a <loggedinuser> tag to display the name of the logged user without requiring a controller to provide it with the information. But you definitely shouldn't rely on a global variable for that. The Angular way to do it would be make use of a service to store that information, and inject it into the directive:
app.controller('MainCtrl', function($scope, userInfo) {
$scope.logIn = function() {
userInfo.logIn('Walter White');
};
$scope.logOut = function() {
userInfo.logOut();
};
});
app.service('userInfo', function() {
this.username = ''
this.logIn = function(username) {
this.username = username;
};
this.logOut = function() {
this.username = '';
};
});
app.directive('loggedinUser', function(userInfo) {
return {
restrict: 'E',
scope: true,
template: '<h1>{{ userInfo.username }}</h1>',
controller: function($scope) {
$scope.userInfo = userInfo;
}
};
});
Plunker here.
The Angular dev guide on directives is a must-go place if you want to start creating powerful, reusable directives.