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.
Related
I'm new in angular and i'm looking for the best way to do what I want.
In my main page I have 2 directives, one is used to display a button (and maybe other stuff). And another used to display a kind of dialog box/menu.
Each directive has its own controller.
I want to show or hide the second directive when I click on the button in the first one.
I don't really know what are goods or wrong approaches. Should I use a service injected in both controller and set a variable with ng-show in the second directive? This solution doesn't really hide the directive because I need a div inside the directive to hide its content and isn't too much to use a service only for one boolean?
Should I use a kind of global variable (rootscope?) or inject the first controller inside the second one?
Or maybe use a third controller in my main page (used with a service?) or use only one controller for both directive?
Basically without directive I would probably used only one main controller for my whole page and set a variable.
In fact the first directive is just a kind of button used to display "something", and the second directive just a kind of popup waiting a boolean to be displayed. That's why I finally used a service containing a boolean with a getter and a setter to avoid any interaction beetween both controller.
My both controller use this service, the first one to set the value when we click on the element and the second controller provide just a visibility on the getter for my ng-show.
I don't know if it is the best way to do but I am satisfied for now.
Small example here (without directive but with same logic) :
http://codepen.io/dufaux/pen/dXMrPm
angular.module('myModule', []);
angular.module("myModule")
.controller("ButtonCtrl", buttonCtrl)
.controller("PopUpCtrl", popUpCtrl)
.service("DisplayerService", displayerService);
//ButtonCtrl
buttonCtrl.$inject = ["DisplayerService", "$scope"];
function buttonCtrl(DisplayerService, $scope) {
var vm = this;
vm.display = function(){
DisplayerService.setDisplay(!DisplayerService.getDisplay());
}
}
//PopUpCtrl
popUpCtrl.$inject = ["DisplayerService"];
function popUpCtrl(DisplayerService) {
var vm = this;
vm.displayable = function(){
return DisplayerService.getDisplay();
}
}
//Service
function displayerService(){
var vm = this;
vm.display = false;
vm.setDisplay = function(value){
vm.display = value;
}
vm.getDisplay = function(){
return vm.display;
}
}
--
<body data-ng-app="myModule">
<div data-ng-controller="ButtonCtrl as btnCtrl" >
<button data-ng-click="btnCtrl.display()">
display
</button>
</div>
[...]
<div data-ng-controller="PopUpCtrl as popUpCtrl" >
<div data-ng-show="popUpCtrl.displayable()">
hello world
</div>
</div>
</body>
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 am trying to create a developer console in my Angular app and I have the following problem.
On every page of my application I want my developer console to be available (like a floating thing in a corner of the screen). I have HTML template and I am switching content with ui-view. My controllers can include consoleService and then call something like myConsole.log(...). This function should add message to some array and then display it on web. For my console I have directive with HTML template, where I want to use ng-repeat to display all messages.
And now my problem. How should I design this? I was thinking like this:
First solution
Well, I can create global JS variable, store all messages there and then use <script> and write em out in my directive template.
WRONG! Not respecting Angular-way of doing things
Second solution
Ok, I`ll use $scope and put my messages there, and then I can use that magical Angular data bind to just let it work on its own. Ok, but here I have a problem, how can I inject $scope into my service?
WRONG! Injecting $scope into service is nonsense, because it would not make any sense, and again, it kinda oppose the Angular-way.
Third solution
Fine, lets store my messages on console service, since all services in Angular are singletons it should work pretty fine, but... there is no $scope inside a service, so I need to return this array of messages to a controller and then assign it to $scope in controller.
??? What do you think? My problem with this solution is that in every controller I need to assign message array to $scope and I just dont want to do that, since I need this console everywhere, on every page.
I hope that my explanation of what I want to do is clear, so I am just hoping for a hint, or advice on how to design it in Angular.
Use the third solution. Your service will have an array with all the messages and each controller can use one of it's functions (service.log(...)) which will basically just add one message to the service list.
Then on your directive, you just assign the getMessages to the directive scope:
var app = angular.module('plunker', []);
app.service('log', function(){
var messages = [];
return {
getMessages: function() { return messages; },
logMessage: function(msg) { messages.push(msg); }
}
})
app.controller('MainCtrl', function($scope, log) {
$scope.msg = '';
$scope.addMessage = function() {
log.logMessage($scope.msg);
};
});
app.directive('console', function(log) {
return {
restrict: "E",
scope: {},
replace: true,
template: "<div><div ng-repeat='msg in messages'>{{msg}}</div></div>",
link: function(scope, el, attr) {
scope.messages = log.getMessages();
}
};
});
plunker: http://plnkr.co/edit/Q2g3jBfrlgGQROsTz4Nn?p=preview
What I am trying to do is after clicking on a buddy in the buddy list, load a chat dialog template HTML file without disturbing other elements in current DOM just like chatting in facebook.
My problem is that after loading the html template file the scope variables such as {{contact.jid}} are not properly rendered, and the controller for the dialog is not even called.
How can I force a rerender or a call on the controller so that those variables are properly renderred? Or should I not use the jQuery.load function to do this? I can't figure out any other way.
Thank you.
Code of the controllers:
// Controller for the chat dialog
ctrl.controller('ChatCtrl',function($scope){
$scope.test = "Test"
});
// Controller for the buddy list
// $scope.toggleDialog is called when a buddy is clicked
ctrl.controller('ContactCtrl',function($scope){
$scope.contacts = window.contactList;
$scope.toggleDialog = function(to){
$.("#chatarea").load("chatdialog.html")
};
});
The controller function of chat dialog is not called after loading chatdialog.html which has an attribute of ng-controller, so the {{test}} variable is not set.
You need to wrap your content that will be compiled inside a directive.
This directive receives the variables and the HTML that will be compiled.
You can wrap this directive in the element that you like:
http://plnkr.co/edit/RYVCrlDmgVXT3LszQ9vn?p=preview
For example, given the following html and variables:
// In the controller
$scope.controllerHtml = '<span>{{variables.myVariable}}</span>';
$scope.controllerVariables = {myVariable: 'hello world'};
<!--In the HTML-->
<div compile-me html-to-bind="controllerHtml" variables="controllerVariables"></div>
You will get the following:
<div>
<span>hello world</span>
</div>
You are loading the external HTML via jQuery and Angular has no way of knowing how to use it. There are two ways to solve this issue:
use ngInclude to load the template from the server, angular will load it from the server and compile it for you to use.
continue to use jQuery load the HTML from the server and use the $compile service to teach angular how to use it
I would highly suggest using method #1 to load your external template.
I suppose the:
$.("#chatarea").load("chatdialog.html")
is the jQuery .load, or something similar. I would get the template via ngInclude, checking if test is setted or not; html:
<div id="chatarea" ng-if="test">
<div ng-include="'chatdialog.html'"/>
</div>
controller:
ctrl.controller('ContactCtrl',function($scope){
$scope.contacts = window.contactList;
$scope.test = '';
var callbackFunction = function(data) {
$scope.test = data.test;
};
$scope.toggleDialog = function(to){
AjaxRequestToGetBuddyInfoAndMessages(to, callbackFunction);
};
});
Obviously test will be a more complex object, so the ngIf test will be different (and you will need to take into account the fact that:
$scope.test = data.test
if they are objects, they will lose the reference and have an unwanted behaviour).
I am using Angularjs in a project.
For login logout I am setting a scope variable like below:
$scope.showButton = MyAuthService.isAuthenticated();
In markup its like
<li ng-show="showLogout">Logout</li>
When I logout it redirect to the login page but logout menu doesn't disappear.
Also tried like this:
$scope.showButton = MyAuthService.isAuthenticated();
In markup:
<li ng-class=" showLogout ? 'showLogout' : 'hideLogOut' ">Logout</li>
Seems scope change is not reflecting in my view, but when I reload page "logout menu" disappears as expected.
I also tried with directives like below:
MyApp.directive('logoutbutton', function(MyAuthService) {
return {
restrict: 'A',
link: function(scope, element, attrs, controller) {
attrs.$observe('logoutbutton', function() {
updateCSS();
});
function updateCSS() {
if (MyAuthService.isAuthorized()) {
element.css('display', 'inline');
} else {
element.css('display', 'none');
}
}
}
}
});
No luck with that too.
How can I hide it when the logout is successful and also after successful login how can I show "logout button"?
Setup a watch on MyAuthService.isAuthenticated() and when that fires, set your scope variable to the result of that service call. In your first example, the scope variable is only getting set once when the controller is initialized (I am assuming that's where it is being run). You can set the watch up in the controller or, if you want to use a directive, in the directive link function.
Something like this:
$scope.$watch(MyAuthService.isAuthenticated, function(newVal, oldVal){
$scope.showButton = newVal;
});
Edit: After read the MarkRajcok comment I realized that this solution is coupling view from business logic layer, also it exposes the service variable to be changed outside the service logic, this is undesirable and error prone, so the $scope.$watch solution proposed by BoxerBucks it's probably better, sorry.
You can use $scope.$watch as in the BoxerBucks answer, but I think that using watchers isn't proper for services, because usually you want to access services variables in differents controllers expecting that when you change that services variables, all the controllers that inject that service will be automatically updated, so I believe that this is a good way to solve your problem:
In your MyAuthServices do this:
app.service('MyAuthService', function(...){
var MyAuthServiceObj = this;
this.authenticated=false; // this is a boolean that will be modified by the following methods:
// I supose that you have methods similar to these ones
this.authenticateUser(...){
...
// At some point you set the authenticated var to true
MyAuthServiceObj.authenticated = true;
}
this.logout(){
....
// At some point you set the authenticated var to false
MyAuthServiceObj.authenticated = false;
}
});
Then in your controller do this:
$scope.myAuthService = MyAuthService;
finally in your html:
ng-show="myAuthService.authenticated"
And this should work without using a watcher like in BoxerBucks answer.
Check this excellent video about AngularJS providers, to understand how to use services properly.