Component doesn't react on scope change from its controller - angularjs

I have two simple components which communicate using require. The problem is that variable changed in the function is not reflected in the component view. See the example below.
<wizard-element data-active="true">First
<wizard-test></wizard-test>
</wizard-element>
<wizard-element>Second
<wizard-test></wizard-test>
</wizard-element>
The components are simple and by clicking deactivate the wizard-element should be invisible, but it doesn't.
Wizard-element is a wrapper component responsible for showing and hiding. The wizard-test has buttons to show or hide and communicate with wizard-element by require.
component('wizardElement',{
transclude: true,
controller: ['$scope',function($scope){
this.activate = function(){
console.log('show');
this.active = true;
}
this.disactivate = function(){
console.log('hide');
$scope.active = false;
}
}],
bindings: {
'active': '=',
'step': '<'
},
template: '<div ng-show="$ctrl.active" ng-transclude></div>'
})
.component('wizardTest',{
controller: ['$scope',function($scope){
this.activate = function(){
this.wizardElement.activate();
}
this.disactivate = function(){
this.wizardElement.disactivate();
}
}],
require: {
'wizardElement' : '^^wizardElement'
},
template: '<button ng-click="$ctrl.activate()">Activate</button><button ng-click="$ctrl.disactivate()">Disactivate</button>'
});
Link to Plunker https://plnkr.co/edit/A5Hl1qYDhaMTgvvuIS11?p=preview

You are mixing $scope and this in your activate() and disactivate() functions.
try changing
this.disactivate = function() {
console.log('hide');
$scope.active = false;
}
To
this.disactivate = function() {
console.log('hide');
this.active = false;
}

Plunker example works after previous suggestion but my app doesn't even though it's as I think the same. Methods activate and disactivate are invoked but doesn't change the view. I also dump {{$ctrl}} in the wizard-element template to see the scope, but the active value doesn't change.
<wizard>
<wizard-element data-step="1" data-active="true" >First Step</wizard-element>
<wizard-element data-step="2" >Second step</wizard-element>
<button class="btn border-only center-block margin-top" wizard-next-control>Next</button>
And my JS:
authApp.component('wizardElement',{
transclude: true,
controller: ['$scope','$timeout',function($scope,$timeout){
this.active = this.active || false;
this.$onInit = function() {
this.wizardCtrl.addElement(this, this.step);
}
this.activate = function(){
console.log('show', this.step);
this.active = true;
}
this.disactivate = function(){
console.log('hide', this.step);
this.active = false;
}
}],
require:{
'wizardCtrl' : '^^wizard',
},
bindings: {
'step': '<'
},
template: '<span>val:{{$ctrl}}</span><div ng-show="$ctrl.active"><ng-transclude></ng-transclude></div>'});

Related

Two Way binding with isolated scope not working in AngularJs

I have written a directive for simple dropdown. On click of one value, I am calling a function and updating the value.
If I log 'scope.$parent.selectedItem' , I am able to see the value. But that is not updated in parent controller.
This is Directive code
app.directive('buttonDropdown', [function() {
var templateString =
'<div class="dropdown-button">'+
'<button ng-click="toggleDropDown()" class="dropbtn">{{title}}</button>'+
'<div id="myDropdown" ng-if="showButonDropDown" class="dropdown-content">'+
'<a ng-repeat="item in dropdownItems" ng-click="selectItem(item)">{{item.name}}</a>'+
'</div>'+
'</div>';
return {
restrict: 'EA',
scope: {
dropdownItems: "=",
selectedOption: '=',
title: '#'
},
template: templateString,
controller: function($scope,$rootScope,$timeout) {
$scope.selectedOption = {};
$scope.showButonDropDown = false;
$scope.toggleDropDown = function() {
$scope.showButonDropDown = !$scope.showButonDropDown;
};
$scope.$watch('dropdownItems', function(newVal,oldval){
if(newVal){
console.log(newVal);
}
});
$scope.selectItem = function(item){
console.log(item);
$scope.selectedOption = item;
}
},
link: function(scope, element) {
scope.dropdownItems = scope.dropdownItems || [];
window.onclick = function (event) {
if (!event.target.matches('.dropbtn')) {
scope.showButonDropDown = false;
}
console.log(scope.$parent);
}
}
}
}]);
This is my HTML
<button-dropdown title="Refer a Friend" dropdown-items='dropDownList' selected-option='selectedItem'></button-dropdown>
This is my controller code
$scope.$watch('selectedItem',function(newVal,oldVal){
if(newVal){
console.log("*** New Val** ");
console.log(newVal);
}
});
I didn't understand one thing.. If I print 'scope.$parent.selectedItem', I could see the value. but it is not updating in the controller. Didn't understand, what am I missing. Can anyone help on this. Thanks in advance.
Try in this way
1. Try to $emit the scope variable in directive.
2. get that in controller by using $on.
Directive:
$scope.$emit('selectedItem',scopeVariable);
Controller:
$scope.$on('selectedItem',function(event,newVal){
if(newVal){
// logic here
}
});

directive doesn't refresh itself on scope change

I have a directive to display a gravatar like follows :
angular.module('ngGravatar').directive('gravatar', function(){
return {
restrict: 'E',
template: '<img ng-src={{gravatarUrl}}>',
scope: {email: '='},
controller: function($scope, md5){
var url = 'http://www.gravatar.com/avatar/';
$scope.gravatarUrl = url + md5.createHash($scope.email || '');
}
};
});
I use it in my view like this
<gravatar email="vm.email"></gravatar>
When the view loads, vm.email gets updated asynchronously, and when its value updates, the gravatar directive won't update itself and stays with the default logo...
How can I make it update itself ? With $scope.$watch ? I thought the two way data binding took care of that.
Is there something I missed out on here ?
Try using $scope.$watch to process changes.
angular.module('ngGravatar').directive('gravatar', function()
{
return {
restrict: 'E',
template: '<img ng-src={{gravatarUrl}}>',
scope: { email: '='},
controller: function($scope, md5){
$scope.$watch('email', function(email) {
if (email)
{
var url = 'http://www.gravatar.com/avatar/';
$scope.gravatarUrl = url + md5.createHash(email || '');
}
});
}
};
});
Your directive doesn't refresh after the asynchronous data arrives because it simply doesn't know about its arrival.
It could only possibly know the changes going on in it's controller and not the updates happening in a parent controller.
You can set up a watch on the variable to make your directive update its contents when the appropriate view model changes.
Check the below code snippet which demonstrates that external changes can be tracked by using watchers and internal changes are tracked automatically and the directive updates its contents through data binding features.
angular
.module('demo', [])
.controller('DefaultController', DefaultController)
.directive('gravatar', gravatar);
DefaultController.$inject = ['$timeout'];
function DefaultController($timeout) {
var vm = this;
$timeout(function() {
vm.gravatarName = 'gravatar.png';
}, 500);
}
function gravatar() {
var directive = {
restrict: 'E',
scope: {
name: '='
},
template: '<img ng-src="{{vm.source}}"/>',
controller: GravatarController,
controllerAs: 'vm',
bindToController: true,
};
return directive;
}
GravatarController.$inject = ['$scope', '$timeout'];
function GravatarController($scope, $timeout) {
var vm = this;
var URL = 'https://d13yacurqjgara.cloudfront.net/users/4085/screenshots/2072398/';
// external changes need to be explicitly watched using watchers
$scope.$watch('vm.name', function(newValue, oldValue) {
if (newValue) {
vm.source = URL + vm.name;
}
});
$timeout(function() {
// internal changes are automatically watched
vm.source = 'https://pbs.twimg.com/profile_images/453956388851445761/8BKnRUXg.png';
}, 2000);
}
img {
height: 250px;
width: 250px;
border: 1px solid #E6E6E6;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.9/angular.min.js"></script>
<div ng-app="demo">
<div ng-controller="DefaultController as vm">
<gravatar name="vm.gravatarName"></gravatar>
</div>
</div>

How to make the two way binding in AngularJS Isolated scope?

directive('confButton', function () {
return {
restrict: 'EA',
replace: false,
scope: {
modalbtntext: '#',
btntext: '#',
iconclass: '#',
btnclass:'#',
callback: '&',
disabled: '='
},
templateUrl : '/app/scripts/mod/Directives/ConfirmationDirective/ConfrimationDirect.html',
controller: ['$scope', '$element', '$attrs', '$transclude', 'modalService',
function ($scope, $element, $attsr, $transclude, modalService) {
$scope.open = function () {
console.log($scope.disabled);
var bodyMessage = '';
if ($scope.modalbtntext.toLowerCase() == "edit") {
bodyMessage = "Are you sure you want to edit this ?"
}
else{
bodyMessage = 'Are you sure you want to delete this customer?'
}
var modalOptions = {
closeButtonText: 'Cancel',
actionButtonText: $scope.modalbtntext,
headerText: 'Please Confirm your Request',
bodyText: bodyMessage
};
modalService.showModal({}, modalOptions).then(function (result) {
$scope.callback();
});
}
}]
}
});
and this is my Tempalte
<button class="{{btnclass}}" ng-disabled="{{disabled}}" ng-click="open()"><i class="{{iconclass}}"></i> {{btntext}} </button>
here is the implementation of the directive
<conf-button modalbtntext="Delete"
disabled="gridOptions.selectedItems.length == 0"
btntext="Delete Selected"
btnclass="btn btn-default hidden-xs"
iconclass ="glyphicon glyphicon-trash"
callback="deleteCity()">
</conf-button>
The point is I want to implement the two way binding in the button .. it's disabled by default as no item is selected .. but when I choose any item it still disabled. how can I achieve the two way binding in this case ?
To make 2 way binding to work you need to bind it to an object. What happens is that you bind the value to a primitive during the expression evaluation so the binding doesn't update.
You can use $watch to update a certain value.
.controller('someCtrl', function($scope){
$scope.gridOption.selectedItems = [];
$scope.$watch(function(){
//Watch for these changes
return $scope.gridOptions.selectedItems.length == 0;
}, function(newVal){
//Change will trigger this callback function with the new value
$scope.btnDisabled = newVal;
});
});
HTML markup:
<conf-button modalbtntext="Delete"
disabled="btnDisabled"
btntext="Delete Selected"
btnclass="btn btn-default hidden-xs"
iconclass ="glyphicon glyphicon-trash"
callback="deleteCity()">
</conf-button>

Bind a callback to directive through template tag in Angular

I am trying to bind a callback to a component through a template. The template contains instance of another directive. This just doesn't seems to work. I'm invoking the directive from a modal, not sure if this can cause a problem. I tried many of the solution suggested in previous questions and still no luck. I ran it with a debugger, and the '$ctrl.onSelectionChanged' is defined to be as it should:
function (locals) { return parentGet(scope, locals); }
My code:
my-component.js:
The inner-directive as no reference to the callback, should it have?
angular.module('myModule')
.component('myComponent', {
template: '<div class="container-fluid"> <inner-directive><button class="btn btn-default center-block" ng-click="$ctrl.onSelectionChange({items_list: $ctrl.selectedItems})">Button</button> </inner-directive> </div>',
bindings: {
$router: '<',
onSelectionChange: '&'
},
controller: MyComponentController
});
/* #ngInject */
function MyComponentController(MyService, $filter, $log, $q) {
var $ctrl = this;
$ctrl.$routerOnActivate = function () {
};
$ctrl.selectedItems = [];
}
calling-component-controller.js:
function CallingComponentCtrl(toastr, $scope, $uibModal, $log) {
var $ctrl = this;
$ctrl.loadDone = false;
$ctrl.grid = {
enableSorting: true,
data: [],
columnDefs: [
{name: 'id'},
{name: 'name'},
{name: 'description'}
],
enableRowSelection: true,
enableRowHeaderSelection: false,
multiSelect: false,
noUnselect: true,
onRegisterApi: function (gridApi) {
$ctrl.gridApi = gridApi;
}
};
this.$onInit = function () {
if (angular.isUndefined($ctrl.abc)) {
return;
}
syncData();
$ctrl.loadDone = true;
};
this.$onChanges = function () {
// TODO
};
function syncData(){
$ctrl.grid.data = $ctrl.abc;
}
$ctrl.myFoo = function(items_list) {
alert("This is never happening");
};
$ctrl.onPress = function (event) {
var modalInstance = $uibModal.open({
template: '<my-component on-selection-change="$ctrl.myFoo(items_list)"></my-component>',
windowClass: 'modal-window'
});
};
}
Any thoughts?
Use the $compile service
link: function(scope, element) {
var template = $compile('<div class="container-fluid"> <inner-directive><button class="btn btn-default center-block" ng-click="$ctrl.onSelectionChange({items_list: $ctrl.selectedItems})">Button</button> </inner-directive> </div>')(scope);
element.append(template);
}
Remember to inject the compile service to the directive function
Trying changing your child component to this:
.component('myComponent', {
template: '<div class="container-fluid"> <inner-directive><button class="btn btn-default center-block" ng-click="$ctrl.onSelectionChange({items_list: $ctrl.selectedItems})">Button</button> </inner-directive> </div>',
bindings: {
$router: '<'
},
require: {
parent: '^^CallingComponent'
},
controller: MyComponentController
});
With require you inherit the parent controller.
Then in the init function you can make the call:
function MyComponentController(MyService, $filter, $log, $q) {
this.$onInit = function() {
this.parent.myFoo(items_list);
}
var $ctrl = this;
$ctrl.$routerOnActivate = function () {};
$ctrl.selectedItems = [];
}
--Old answer
Try changing the template to:
<my-component on-selection-change="$ctrl.myFoo(items_list)"></my-component>
You're calling it from the $scope when it's declared as a controller function.
Well, I found the problem. While invoking the modal, I used a template used a component in this template. To the component, I passed a callback that is defined in the '$ctrl'. The problem was that the modal defined its own scope and couldn't reached this $ctrl. So I defined a controller to the modal, and called through it the function I needed. This is my solution, I highlighted the changes and adds:
calling-component-controller.js:
function CallingComponentCtrl(toastr, $scope, $uibModal, $log) {
var $ctrl = this;
....
$ctrl.myFoo = function(items_list) {
alert("This is never happening");
};
$ctrl.onPress = function (event) {
var modalInstance = $uibModal.open({
template: '<my-component on-selection-change="$ctrl.myNewFoo(items_list)"></my-component>',
**controllerAs: '$ctrl',**
windowClass: 'modal-window',
**controller: function($uibModalInstance){
var $ctrl = this;
$ctrl.myNewFoo= function(items_list) {
$uibModalInstance.close(items_list);
};
}**
});
**modalInstance.result.then(function(items_list) {
$ctrl.myFoo(items_list);
});**
};
}

How to make directive update when the collection sent to it is updated?

I have send a directive a collection, but when I update the collection in the parent scope, the directive doesn't update the collection:
http://jsfiddle.net/edwardtanguay/kj4oj1aa/9/
<div ng-controller="mainController">
<div item-menu datasource="customers" add="addCustomer()"></div>
<button ng-click="addCustomer()">add customer</button> Number added: {{numberAdded}}
<ul>
<li ng-repeat="customer in customers">{{customer.lastName}}</li>
</ul>
</div>
I supposed I need a $watch somewhere in my directive but where and how to implement it?
directive:
.directive('itemMenu',function() {
var controller = function($scope) {
var vm = this;
function init() {
vm.items = angular.copy($scope.datasource);
}
init();
};
return {
restrict: 'A',
scope: {
datasource: '=',
add: '&'
},
controller: controller,
controllerAs: 'vm',
bindToController: true,
templateUrl: 'itemMenuTemplate',
link: function(scope, element, attrs) {
scope.getTemplateUrl = function(item) {
switch(item.kind) {
case 'external':
return 'itemMenuTemplateExternal';
case 'internal':
return 'itemMenuTemplateInternal';
default:
return 'itemMenuTemplateUndefined';
}
};
},
};
Because you are using angular.copy, it won't update when the $scope.datasource is being updated.
You need to put a $watch on it with the parameter true at the end for deep watching. I assume this is your scenario (this is considered pretty expensive in terms of performance, so read up on $watch to see if you should be using some other method like $watchCollection).
var controller = function($scope) {
var vm = this;
$scope.$watch('datasource', function(newVal) {
copy(newVal);
}, true);
function copy(object) {
vm.items = angular.copy(object);
}
function init() {
//add other init code here
copy(vm.datasource);
}
init();
};

Resources