Modifying directive property from outside - angularjs

So, I have a question that I am unsure of how to even ask. I have a property that I am setting in a directive's controller by an auth0 library (code to follow). I need to modify that property from another app controller.
Specifically, the use case is around being logged in/logged out. When the user is logged in, and they click the logout button, I can set the value of the property, not a problem. But when they are not logged in, and they log in, I can't set that property in the directive from the login controller.
Directive:
angular
.module('app')
.directive('loginLogout', loginLogout);
function loginLogout() {
var directive = {
...
scope: {
loggedin: '='
},
controller: loginLogoutController,
controllerAs: 'vm',
bindToController: true
};
return directive;
function loginLogoutController(auth,store,$location,toastr,$parse ) {
var vm = this;
vm.logout = logUserOut;
vm.loggedin = auth.isAuthenticated;
function logUserOut() {
auth.signout();
...
vm.loggedin = false;
}
}
}
Login Controller:
(abbreviated)
function LoginController(auth, store, $location, toastr) {
var vm = this;
vm.login = function () {
auth.signin({}, loginSuccess, loginFailure);
function loginSuccess(profile, token){
...
// ========== I want to set the value of vm.loggedin from the directive here.
}
function loginFailure(){
...
}
};
}
I have tried things like $parse, and setting tinkering with the isolated scope on the directive config. No luck. Any help is appreciated.

You could try using $rootScope.$broadcast and $scope.$on for such communication.
You have used controllerAs to avoid injecting $scope. Ofcourse this would need injecting $scope in controller. However using $scope in such specific cases (that is when controllerAs is used) may not be all that bad idea (https://github.com/toddmotto/angularjs-styleguide).
Login Controller:
function LoginController(auth, store, $location, toastr) {
var vm = this;
vm.login = function () {
auth.signin({}, loginSuccess, loginFailure);
function loginSuccess(profile, token){
...
// ========== I want to set the value of vm.loggedin from the directive here.
$rootScope.$broadcast('loginCheck');
}
function loginFailure(){
...
}
};
}
Directive
function loginLogoutController(auth,store,$location,toastr,$parse ) {
var vm = this;
vm.logout = logUserOut;
vm.loggedin = auth.isAuthenticated;
function logUserOut() {
auth.signout();
...
vm.loggedin = false;
}
$scope.$on('loginCheck', function(event, args) {
// Set vm.loggedin
});
}

What i can think of now, You can use angular.js function binding.
.directive('loginLogout', loginLogout);
function loginLogout() {
var directive = {
...
scope: {
loggedin: '=',
confirmAction: '&'
},
controller: loginLogoutController,
controllerAs: 'vm',
bindToController: true
};
<!--In html-->
<login-logout confirm-action="doSomething()"> </login-logout>
function LoginController(auth, store, $location, toastr) {
var vm = this;
vm.login = function () {
auth.signin({}, loginSuccess, loginFailure);
function loginSuccess(profile, token){
...
// call doSomething here
doSomething()
}
function loginFailure(){
...
}
};
}

Related

angularjs 1.7.2 - How to call a directive function from a controller?

How do you call a directive's function from within a controller?
I have seen a number of answers to this question, with the solution similar to the following:
https://lennilobel.wordpress.com/tag/calling-directive-from-controller/
I have implemented it as follows:
Directive
angular.module('myApp').directive('myDirective', ['$log',
function ($log) {
function link(scope, element, attributes) {
//function that a controller can call
scope.myFunc = function () {
//Do Something
};
//if the user has provided an accessor, attach the function
if (scope.accessor) {
scope.accessor.myFunc = scope.myFunc;
}
}
return {
link: link,
restrict: 'E',
templateUrl: 'app/myTemplate.html',
scope: {
accessor: '=',
}
}
}
Controller
angular.module('myApp').controller('myCtrl', ['$log', '$q',
function ($log, $q) {
var vm = this;
// empty object that the directive will attach myFunc to
vm.accessor = {};
$q
.all([
//Service Call
])
.then(function (results) {
//manipulate the results of the service call using the
//directives function
vm.callDirective();
},
function (err) {
$log.debug('$q.all err:', err);
});
vm.callDirective = function () {
if (vm.accessor.myFunc) {
vm.accessor.myFunc();
} else {
$log.error('umm, I don\'t have a function to call');
}
};
}
HTML Template
<div ng-controller="myCtrl">
<myDirective accessor="vm.accessor"></myDirective>
</div>
When I run the code, the directive indicates that accessor is undefined. As a result, accessor, in the controller, doesn't have myFunc defined.
How do I get myFunc to execute?
I am using angular 1.7.2
The controller is compiled (an instance created with the resulting scope) before the directive.
In this scenario, it compiles faster than the directive can set the accessor function.
A quick workaround for this is to set a delay before checking if there is an accessor present using $timeout service.
The key is having a Promise object passed to $q.all. This will cause a small delay and allowing for the directive to be compiled.
For real, you'll be having promises that do some network call passed to $q.all instead of doing this workaround with the $timeout service.
Here is how this will go:
index.html
<div ng-controller="myCtrl as vm">
<my-directive accessor="vm.accessor"></my-directive>
</div>
script.js
const myApp = angular.module('myApp', []);
myApp.directive('myDirective', ['$log', myDirective]);
myApp.controller('myCtrl', ['$scope', '$timeout', '$log', '$q', myCtrl]);
function myCtrl($scope, $timeout, $log, $q) {
const vm = $scope.vm;
// empty object that the directive will attach myFunc to
vm.accessor = {};
vm.callDirective = () => {
if (vm.accessor.myFunc) {
vm.accessor.myFunc();
} else {
$log.error("umm, I don't have a function to call");
}
};
const handleSuccess = results => {
//manipulate the results of the service call using the
//directives function
vm.callDirective();
};
const handleError = err => {
$log.debug('$q.all err:', err);
};
$q.all([
//Service Call
$timeout()
])
.then(handleSuccess)
.catch(handleError);
}
function myDirective($log) {
//function that a controller can call
const myFunc = function() {
//Do Something
$log.info('Calling assessor myFunc');
};
const link = function(scope) {
//if the user has provided an accessor, attach the function
if (scope.accessor) {
scope.accessor.myFunc = myFunc;
}
};
return {
link: link,
restrict: 'E',
templateUrl: 'mydirective.html',
scope: {
accessor: '='
}
};
}

Angular 1.5 component updating parent controller through ngRoute

I'm using ngRoute to create an Angular single-page app. Want to move to a component-based version.
Problem is isolated scopes. I need access to main controller props and methods. Trying to use bindings but does not work. I cannot find the problem with this one. This app works fine without using components. When I try to change the homepage view into a component it crashes. These are the main parts of the code:
framework
<html ng-app="angularModule" >
<body ng-controller="angularController as angCtrl" >
<div ng-show="angCtrl.user.isLoggedIn" >Sign Out</div>
<div ng-hide="angCtrl.user.isLoggedIn" cd-visible="angCtrl.showSignIn">Sign In</div>
<div id="contentLayer" class="contentLayer" ng-view ></div>
homepage template
<h1 class="pageLabel" >HomePage</h1>
<blockquote>This can be anything. No bindings.</blockquote>
angularController
var app = angular.module ('angularModule', ['ngRoute'] );
app.directive ('cdVisible',
function () {
return function (scope, element, attr) {
scope.$watch (attr.cdVisible,
function (visible) {
element.css ('visibility', visible ? 'visible' : 'hidden');
}
);
};
}
);
app.config ( [ '$locationProvider', '$routeProvider',
function config ($locationProvider, $routeProvider) {
$locationProvider.hashPrefix ('!');
$routeProvider
.when ('/sign-in', {
templateUrl: '/ng-sign-in',
controller: signInController
})
... more routes
.when ('/home', {
template: '<home-page showSignIn="angCtrl.showSignIn" menuSelect="angCtrl.menuSelect" ></home-page>'
})
.otherwise ('/home');
}
]);
function homePageController () {
this.menuSelect ('Devices'); // this statement has no effect on angularController.menuSelection chrome shows it as an anonymous function
this.showSignIn = false; // this bombs: Expression 'undefined' in attribute 'showSignIn' used with directive 'homepage' is non-assignable!
}
app.component ('homePage', {
templateUrl: '/ng-homepage',
controller: homePageController,
bindings: {
menuSelect: '&',
showSignIn: '='
}
});
app.controller ('angularController', [ '$http', '$window', '$location',
function ($http, $window, $location) {
var self = this;
this.user = {
"isLoggedIn": false
};
this.showSignIn = true;
this.menuSelection = "";
this.errorMessage = "";
this.menuSelect =
function (selection) {
self.menuSelection = selection;
};
this.setUserData =
function (userData) {
self.user = userData;
};
this.setShowSignIn =
function (show) {
self.showSignIn = show;
};
this.menuSelect ('');
this.getUserData(); // I removed this for this post
}
]);
I added a comment to the spot where it throws an exception. The homepage controller attempts to update the model of the angularController. The first does nothing the second throws an exception. What am I doing wrong?
First of all showSignIn is a primitive, therefore angular will handle it the exact same way as doing showSignIn="7+2". If you need to modify that value inside the component then you should use an object with the showSignIn property.
Now menuSelect is a little tougher, probably Chrome console is showing something like
function (locals) {
return parentGet(scope, locals);
}
This is because you're just passing the reference to angCtrl.menuSelect to the component.
In order to execute the menuSelect function from inside homePageController you'd have to do (in the HTML):
<home-page menu-select="angCtrl.menuSelect(myMsg)"></home-page>
And then call it like this in the component's controller:
this.menuSelect({ myMsg:"Devices" })
The (myMsg) in the HTML is a call to angular to return this reference, then in the execution we pass the parameter { myMsg:"Devices" } to match the parameter in the reference we just did.
You can check this answer that explains it way more detailed.
In the process of reading your answer one mistake jumped at me: the home-page component of the /home route should use kebab case show-sign-in menu-select for attributes not lower-camelCase as was coded initially.
Your suggestions both worked. Thanks. Without components I was using prototypal inheritance to access properties and methods in the parent scope. Due to the nature of javascript prototypal inheritance the only way to modify scalars in the parent scope is to invoke setter methods on the parent scope. Apparently something similar applies here. More about prototypal inheritance in javascript.
This was a proof-of-concept exercise. I wanted to test my ability to update properties and objects in a parent scope from a component as well as execute a method in the parent scope. This example updates a scalar in the parent scope 2 different ways:
stick it into an object, bind to the object and reference it using that object from the component like is done with the loginData.showSignIn boolean
build a setter method in the parent scope and bind to that and invoke the setter from the component like is done with menuSelect (which is actually nothing more than a setter for menuSelection)
and demonstrates invoking a function while doing so.
The final working code is: (homepage template does not change)
framework
<html ng-app="angularModule" >
<body ng-controller="angularController as angCtrl" >
<div ng-show="angCtrl.user.isLoggedIn" >Sign Out</div>
<div ng-hide="angCtrl.user.isLoggedIn" cd-visible="angCtrl.loginData.showSignIn">Sign In</div>
<div id="contentLayer" class="contentLayer" ng-view ></div>
angularController
var app = angular.module ('angularModule', ['ngRoute'] );
app.directive ('cdVisible',
function () {
return function (scope, element, attr) {
scope.$watch (attr.cdVisible,
function (visible) {
element.css ('visibility', visible ? 'visible' : 'hidden');
}
);
};
}
);
app.config ( [ '$locationProvider', '$routeProvider',
function config ($locationProvider, $routeProvider) {
$locationProvider.hashPrefix ('!');
$routeProvider
.when ('/sign-in', {
templateUrl: '/ng-sign-in',
controller: signInController
})
... more routes
.when ('/home', {
template: '<home-page login-data="angCtrl.loginData" menu-select="angCtrl.menuSelect(mySelection)" ></home-page>'
})
.otherwise ('/home');
}
]);
function homePageController () {
this.menuSelect ( { mySelection: 'Overview' });
this.loginData.showSignIn = true;
}
app.component ('homePage', {
templateUrl: '/ng-homepage',
controller: homePageController,
restrict: 'E',
bindings: {
menuSelect: '&',
loginData: '='
}
});
app.controller ('angularController', [ '$http', '$window', '$location',
function ($http, $window, $location) {
var self = this;
this.user = {
"isLoggedIn": false
};
this.loginData = {
"showSignIn": false
};
this.menuSelection = "";
this.errorMessage = "";
this.menuSelect =
function (selection) {
self.menuSelection = selection;
};
this.setUserData =
function (userData) {
self.user = userData;
};
this.setShowSignIn =
function (show) {
self.showSignIn = show;
};
this.menuSelect ('');
this.getUserData(); // I removed this for this post
}
]);

How to reload a directive in AngularJS?

I'm trying to build a header directive that:
If logged in, should display the username and a Log Out button
If logged out, should hide the above-mentioned things
I'm using a custom login service that captures this information, and broadcasts the events login and logout. I'm successfully listening to these events in both the header's controller and directive.
How can I reload the directive on these events?
loginService.js:
angular.module("app")
.service("loginService", ["$http", "$rootScope", function ($http, $rootScope) {
var loggedIn = false,
_username = "";
this.logIn = function (username, password) {
// do some validation...
loggedIn = ...validation was successful;
_username = username;
if (loggedIn) {
$rootScope.$broadcast("login");
}
};
this.getUsername = function () {
return _username;
};
this.isLoggedIn = function () {
return loggedIn;
};
this.logOut = function () {
loggedIn = false;
$rootScope.$broadcast("logout");
};
}]);
headerController.js
angular.module("app")
.controller("headerController", ["loginService", "$rootScope", "$location", function (loginService, $rootScope, $location) {
this.isLoggedIn = loginService.isLoggedIn();
this.username = "";
$rootScope.$on("login", function (event) {
this.isLoggedIn = loginService.isLoggedIn();
this.username = loginService.getUsername();
});
this.logOut = function () {
loginService.logOut();
this.isLoggedIn = loginService.isLoggedIn();
this.username = "";
$location.path("/login"); // redirecting
};
}]);
header.html:
<header ng-controller="headerController as header">
<span ng-if="header.isLoggedIn">{{header.username}} <button ng-click="header.logOut()">Log Out</button></span>
</header>
headerDirective.js
angular.module("app")
.directive("header", function () {
return {
restrict: "A",
transclude: false,
templateUrl: "app/header/header.html",
controller: "headerController",
link: function (scope, element, attrs) {
scope.$on("login", function (event) {
// show the ng-if in header.html??
});
scope.$on("logout", function (event) {
// hide the ng-if in header.html??
});
}
};
});
I'm using this as <div header></div>.
It looks like there are some fundamental issues with the directive that will not allow this to work:
1) Declared as an Attribute Directive:
You've create a header attribute directive: restrict: "A", but you are using it as an element directive: <header ng-controller...</header>. restrict property should be restrict: "E". Or you haven't used the directive as others have commented.
2) Transclude is false
You have set transclude to false but you are attempting to use the directive with contents so transclude should be true.
To solve your issue I would suggest this as a solution:
1. Declare your header directive in its parent container view as just this.
<ian-header></ian-header>
ianHeader.html
<header>
<span ng-if="header.isLoggedIn">{{header.username}} <button ng-click="header.logOut()">Log Out</button></span>
</header>
ianHeader.js
angular.module("app")
.directive("ianHeader", function () {
return {
restrict: "E",
templateUrl: "app/header/ianHeader.html",
link: function (scope, element, attrs) {
scope.header = {isLoggedIn: false};
scope.$on("login", function (event) {
// show the ng-if in header.html??
scope.header.isLoggedIn = true;
});
scope.$on("logout", function (event) {
// hide the ng-if in header.html??
scope.header.isLoggedIn = false;
});
}
};
});
If you would provide a JS snippet would be easier but anyway one approach might be:
angular.module("app")
.directive("header", function () {
return {
restrict: "A",
transclude: false,
templateUrl: "app/header/header.html",
controller: "headerController",
link: function (scope, element, attrs) {
scope.$on("login", function (event)
{
//header should be the controllerAs you declared
//If you would provide JS snippet would be easier to debbug
scope.$parent.header.isLoggedIn= true;
// show the ng-if in header.html??
});
scope.$on("logout", function (event)
{
scope.$parent.header.isLoggedIn = false;
// hide the ng-if in header.html??
});
}
};
});
Your solution is not great, usage of event broadcasting is always prone to errors and generally difficult to test and debug. What you need to do is to create a service which stores the current profile object and store a reference to it in the header directive (and other services which may use/modify the current user).
The Service
'use strict';
(function() {
function AuthService() {
var Auth = {
User: User //call some API for user authentication if using sessions
};
return Auth;
}
angular.module('app')
.factory('Auth', AuthService);
})();
Your header directive
'use strict';
//do not use "header" as the name of directive
angular.module('app')
.directive('navbar', () => ({
templateUrl: 'components/navbar.html',
restrict: 'E',
controller: function(Auth){
this.user = Auth.User;
},
controllerAs: 'navbar'
}));

AngularJS: require with bindToController, required controller is not available?

I have a directive where I user require and bindToController in the definition.
(function(){
'use strict';
angular.module('core')
.directive('formValidationManager', formValidationManager);
function formValidationManager() {
return {
require: {
formCtrl: 'form'
},
restrict: 'A',
controller: 'FormValidationManagerCtrl as fvmCtrl',
priority: -1,
bindToController: true
};
}
}());
According to the angular docs:
If the require property is an object and bindToController is truthy,
then the required controllers are bound to the controller using the
keys of the require property. This binding occurs after all the
controllers have been constructed but before $onInit is called. See
the $compileProvider helper for an example of how this can be used.
So I expect that in my controller:
(function () {
'use strict';
angular
.module('core')
.controller('FormValidationManagerCtrl', FormValidationManagerCtrl);
FormValidationManagerCtrl.$inject = ['$timeout', '$scope'];
function FormValidationManagerCtrl($timeout, $scope) {
var vm = this;
vm.API = {
validate: validate
};
//vm.$onInit = activate;
function activate() {
}
function validate() {
$scope.$broadcast('fvm.validating');
var firstInvalidField = true;
Object.keys(vm.formCtrl).forEach(function (key) {
var prop = vm.formCtrl[key];
if (prop && prop.hasOwnProperty('$setTouched') && prop.$invalid) {
prop.$setTouched();
if (firstInvalidField) {
$timeout(function(){
var el = $('[name="' + prop.$name + '"]')[0];
el.scrollIntoView();
}, 0);
firstInvalidField = false;
}
}
});
return firstInvalidField;
}
}
})();
vm.formCtrl will be populated with the form controller. However, it is undefined. Why is it undefined? Additionally, I tried to access it in the $onInit function but the $onInit function never got called. What am I doing wrong?
I am using the directive like so:
<form novalidate name="editUserForm" form-validation-manager>
I'm not sure if this is your issue, but I think your directive declaration should look like this:
controller: 'FormValidationManagerCtrl',
controllerAs: 'vm',
rather than:
controller: 'FormValidationManagerCtrl as fvmCtrl',
Looks like this is related to the angular version. I was in 1.4.6 which did not support this. I have upgraded to 1.5.0 in which the code in the OP is working great.

How to add a particular function from service to directive in AngularJS

This is a function I have added in localcacheService.
I want to add this function in directive.
LocalCacheServiceClear.prototype.isAvailable = function() {
this.cache.clear();
}
My directive is this:
(function() {
var MainApp = angular.module('MainApp');
MainApp.directive('Logout', function () {
return {
'restrict': 'E',
'templateUrl': 'directives/panels/Logout.html',
'controller': ["$scope","LocalCacheService", function($scope,LocalCacheService) {
console.log("Logout Controller called....");
$scope.Logout = function() {
window.sessionStorage.clear();
console.log('Log out');
}
}]
};
});
}());
How to add this function to this directive?
via Angular Service:
MainAppUserConvMod.service('LocalCacheServiceClear', LocalCacheServiceClear);
and then inject into your directive:
MainAppUserConvMod.directive('Logout', function (LocalCacheServiceClear) {
and use it
LocalCacheServiceClear.isAvailable()

Resources