Angular 1.5 component updating parent controller through ngRoute - angularjs

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
}
]);

Related

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.

Call function in ng-view controller from directive

I have a directive as given below,
app.directive('metadataSelectPicker', ['ManagementService', '$timeout', '$location', function (ManagementService, $timeout, $location) {
return {
restrict: 'E',
replace: true,
templateUrl: 'Views/NgTemplates/DirectiveTemplates/MetaDataSelectPicker.html',
link: function (scope, elem, attr) {
var promise = ManagementService.getMetaDataList();
promise.then(function (response) {
if (response.data.length > 0) {
scope.metaDataList = response.data;
$timeout(function () {
$('.selectpicker').on('change', function () {
$('.selectpicker').selectpicker('refresh');
$timeout(function () {
if (scope.selectedMetaData == 'Class') {
$location.path('/class');
} else {
$location.path('/metadata');
}
});
});
});
}
}, function () {
alert('Data fetching failed.');
});
}
}
}]);
The MetaDataSelectPicker.html is given below:
<div id="metaDataSelectPicker">
<div class="sel_allcustom_name">
<select class="selectpicker show-tick form-control" ng-model="selectedMetaData">
<option ng-repeat="metaData in metaDataList" value="{{metaData.ValueText}}">{{metaData.DisplayText}}</option>
</select>
</div>
<div class="clearfix"></div>
I'm using this directive is in a page with controller named homeController. In this page I used ng-view tag to load different contents. The following code is used to redirect various pages in to ng-view tag.
if (scope.selectedMetaData == 'Class') {
$location.path('/class');
} else {
$location.path('/metadata');
}
In the routeProvider configuration, I have specified different controller for each ng-view path. Initally, I'm not loading any view to the ng-view. But I'm redirecting the ng-view page while changing the options in the select control. In the controller of /metadata page, I have a function and assigned to a scope variable. I need to call that function while changing the select option and if scope.selectedMetaData != 'Class'. I need to do it as follows,
if (scope.selectedMetaData == 'Class') {
$location.path('/class');
} else {
$location.path('/metadata');
scope.doSomething();
}
But while changing the option, the corresponding controller may not be loaded. So the scope.doSomething(); will not work. I don't like to use broadcast event. I need to tract the ng-view page load and perform the function after that. How can I do that? Or suggest me any other better method. Please help me.
You can use https://github.com/angular-ui/ui-router for routing and then you can use one of state change events ref. https://github.com/angular-ui/ui-router/wiki

service only works after `$rootScope.$appy()` applied

I am loading the template from angular-service but that's not updating the template unless i use the $rootScope.$appy(). but my question is, doing this way this the correct approach to update the templates?
here is my code :
var app = angular.module('plunker', []);
app.service('modalService', function( $rootScope ) {
this.hide = function () {
this.show = false;
}
this.showIt = function () {
this.show = true;
}
this.setCategory = function ( category ) {
return this.showPath = category+'.html'
}
this.showCategory = function (category) {
this.setCategory( category )
$rootScope.$apply(); //is this correct?
}
})
app.controller('header', function($scope) {
$scope.view = "home view";
});
app.controller('home', function($scope, modalService) {
$scope.name = 'World';
$scope.service = modalService;
});
//header directive
app.directive('headerDir', function( modalService) {
return {
restrict : "E",
replace:true,
templateUrl:'header.html',
scope:{},
link : function (scope, element, attrs) {
element.on('click', '.edit', function () {
modalService.showIt();
modalService.showCategory('edit');
});
element.on('click', '.service', function () {
modalService.showIt();
modalService.showCategory('service');
})
}
}
});
app.directive('popUpDir', function () {
return {
replace:true,
restrict:"E",
templateUrl : "popup.html"
}
})
Any one please advice me if i am wrong here? or can any one show me the correct way to do this?
click on links on top to get appropriate template to load. and click on the background screen to close.
Live Demo
If you don't use Angular's error handling, and you know your changes shouldn't propagate to any other scopes (root, controllers or directives), and you need to optimize for performance, you could call $digest on specifically your controller's $scope. This way the dirty-checking doesn't propagate. Otherwise, if you don't want errors to be caught by Angular, but need the dirty-checking to propagate to other controllers/directives/rootScope, you can, instead of wrapping with $apply, just calling $rootScope.$apply() after you made your changes.
Refer this link also Angular - Websocket and $rootScope.apply()
Use ng-click for handling the click events.
Template:
<div ng-repeat="item in items">
<div ng-click="showEdit(item)">Edit</div>
<div ng-click="delete(item)">Edit</div>
</div>
Controller:
....
$scope.showEdit = function(item){
....
}
$scope.delete = function(item){
....
}
If you use jquery or any other external library and modify the $scope, angular has no way of knowing if something has changed. Instead if you use ng-click, you let angular track/detect change after you ng-click handler completes.
Also it is the angular way of doing it. Use jquery only if there is no other way to save the world.

Modifying directive property from outside

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(){
...
}
};
}

Unable to call Angular directive method

I've got an Angular view thusly:
<div ng-include="'components/navbar/navbar.html'" class="ui centered grid" id="navbar" onload="setDropdown()"></div>
<div class="sixteen wide centered column full-height ui grid" style="margin-top:160px">
<!-- other stuff -->
<import-elements></import-elements>
</div>
This is controlled by UI-Router, which is assigning the controller, just FYI.
The controller for this view looks like this:
angular.module('pcfApp')
.controller('ImportElementsCtrl', function($scope, $http, $location, $stateParams, $timeout, Framework, OfficialFramework) {
$scope.loadOfficialFrameworks();
// other stuff here
});
The <import-elements> directive, looks like this:
angular.module('pcfApp').directive('importElements', function($state, $stateParams, $timeout, $window, Framework, OfficialFramework) {
var link = function(scope, el, attrs) {
scope.loadOfficialFrameworks = function() {
OfficialFramework.query(function(data) {
scope.officialFrameworks = data;
$(".ui.dropdown").dropdown({
onChange: function(value, text, $item) {
loadSections($item.attr("data-id"));
}
});
window.setTimeout(function() {
$(".ui.dropdown").dropdown('set selected', data[0]._id);
}, 0);
});
}
return {
link: link,
replace: true,
templateUrl: "app/importElements/components/import_elements_component.html"
}
});
I was under the impression that I'd be able to call the directive's loadOfficialFrameworks() method from my controller in this way (since I'm not specifying isolate scope), but I'm getting a method undefined error on the controller. What am I missing here?
The problem is that your controller function runs before your link function runs, so loadOfficialFrameworks is not available yet when you try to call it.
Try this:
angular.module('pcfApp')
.controller('ImportElementsCtrl', function($scope, $http, $location, $stateParams, $timeout, Framework, OfficialFramework) {
//this will fail because loadOfficialFrameworks doesn't exist yet.
//$scope.loadOfficialFrameworks();
//wait until the directive's link function adds loadOfficialFrameworks to $scope
var disconnectWatch = $scope.$watch('loadOfficialFrameworks', function (loadOfficialFrameworks) {
if (loadOfficialFrameworks !== undefined) {
disconnectWatch();
//execute the function now that we know it has finally been added to scope
$scope.loadOfficialFrameworks();
}
});
});
Here's a fiddle with this example in action: http://jsfiddle.net/81bcofgy/
The directive scope and controller scope are two differents object
you should use in CTRL
$scope.$broadcast('loadOfficialFrameworks_event');
//And in the directive
scope.$on('loadOfficialFrameworks_event', function(){
scope.loadOfficialFrameworks();
})

Resources