Directive does not update on service model change - angularjs

I have a service that contains my application's model:
angular.module('adminClient').factory('myApi', function () {
var model = {};
model.myObject = {
label: 'New York City'
};
return {
model: function () {
return model;
}
};
});
A partial/controller has access to the model and can set a new myObject:
angular.module('adminClient').controller('MySelectNetworkCtrl', function ($scope, myApi) {
$scope.myObject = myApi.model().myObject;
});
In addition to this, I have a directive that should display the label contained in myObject:
angular.module('nav').directive('myTopBar', function(myApi) {
return {
restrict: 'E',
replace: true,
scope: {},
templateUrl: 'nav/directive/my-top-bar/my-top-bar.html',
link: function(scope, element, attrs, fn) {
scope.myObject = myApi.model().myObject;
}
};
});
And here is the HTML:
<div id="my-top-bar">
<span ng-bind="myObject"></span>
</div>
When I run the application, the label is displayed fine (New York City), but as soon as the controller changes the myObject, the label in the directive remains unchanged. I can also see this in the Chrome Angular scope inspector.
Any idea how I can make the directive display the current value even after changed by the controller?

It all depends upon how the controller is updating the model object. Since your code
scope.myObject = myApi.model().myObject;
gets hold of this specific model object it works fine as long as model object myObject properties are changed.
But if the controller does
myApi.model().myObject= {}; //or any new object
Now the model returned by service has different myObject and the directive one is different. Therefore changes do not work.
Instead in directive do:
scope.myObject = myApi.model(); // bind to model
And update the bindings in the directive template accordingly. See if it works

Check this plunkr
If you want to modify your myObject inside your controller, and also want this change to be reflected inside your my-top-bar directive which then updates the directive's scope from myApi service, then you should do it like this.
Update the myObject in your controller and then also update it in your service through a setter method.
// Doing this will update your scope.myObject but
// will not change the moObject in your service
$scope.myObject = {
label: 'updated label'
};
// This will update your service, and anyone making a myApi.myApi.model() after this will get an updated myObject
myApi.setModel($scope.myObject);
// Notify directive of the change
$scope.$broadcast('updateDirective');
And in the factory:
angular.module('plunker').factory('myApi', function() {
var model = {};
model.myObject = {
label: 'New York City'
};
return {
model: function() {
return model;
},
setModel: function(newModel) {
model.myObject = newModel;
}
};
});
Finally, the directive:
angular.module('plunker').directive('myTopBar', function(myApi) {
return {
restrict: 'E',
replace: true,
scope: {},
templateUrl: 'top-bar.html',
link: function(scope, element, attrs, fn) {
console.log('in directive link', myApi.model().myObject);
scope.myObject = myApi.model().myObject;
},
controller: function($scope) {
$scope.$on('updateDirective', function() {
console.log('updating the scope of directive so that ng-bind works');
$scope.$apply(function() {
$scope.myObject = myApi.model().myObject;
console.log($scope.myObject);
});
});
}
};
});

Related

Unable to call Angular Directiive Method on Button Click

I'm trying to call directive method on button click from the calling controller.
Here is the directive code:
myApp.directive("helloDirective", function() {
return {
restrict: "E",
template: '<input type="text" data-ng-model="model.msg" />',
scope: {},
bindToController: {
param: "="
},
controller: 'helloDirectiveController',
controllerAs: 'model'
}
})
.controller("helloDirectiveController", function() {
var self = this;
self.actions = {
get: function() {
return self.msg;
},
set: function(msgData) {
self.msg = msgData;
}
});
I have call the get and set method from controller..
myApp.controller("indexController", [function() {
var self = this;
self.helloParam ={};
self.get = function() {
//how to call the Directive get method from here
}
}]);
i tried to create a fiddle here
plnkr
The idea
For me, the cleanest solution (so far) for your problem is a solution used by Angular Material developers (I don't know if they were the authors, but I found it there). I've used it once in my project and it worked like a charm.
The idea is to create a global registry for directives' actions. Directives would be stored there by unique ids. We can also create a service dedicated for each directive, just in case we need some external logic.
The solution
1. Components registry
Firstly, we need a components registry. It can be a really simple service:
angular.module('app');
.service('$componentsRegistry', function() {
var self = this;
var components = {};
self.put = function(id, actions) {
components[id] = actions;
}
self.get = function(id) {
return components[id];
}
})
The components registry has methods for storing and getting components by ids. Of course, there might be much more methods and they might be more complicated, but this is just a simple example.
2. Service for our directive
Let's say we have a simple show-message directive, so we can create a $showMessageService:
angular.module('app')
.service('$showMessageService', ['$componentsRegistry', function($componentsRegistry) {
return function(id) {
return $componentsRegistry.get(id);
}
}])
For now, the only task of the service is to return our directive's actions. But it can be extended in the future, of course.
3. Directive's code
The last thing we need is our show-message directive:
angular.module('app')
.directive('showMessage', function($componentsRegistry) {
return {
restrict: 'E',
scope: {
directiveId: '#' // Unique id is passed from the view
},
template: '<div>{{ text }}</div>',
link: function(scope) {
scope.text = 'TEST TEXT';
// Create actions
scope.actions = {
set: function(value) {
scope.text = value;
}
}
// Store actions in the components registry
$componentsRegistry.put(scope.directiveId, scope.actions);
}
}
})
In a link function, we need to simply register our actions in components registry. We pass the unique id from the view so that developer has control over it inside views/controllers.
Example of usage
And now we can finally use the directive in our application. Here is a simple code which shows how to do that:
View
<div ng-controller="testController as test">
<show-message directive-id="testDirective"></show-message>
<button ng-click="test.changeText()">CHANGE</button>
</div>
Controller
angular.module('app')
.controller('testController', function(['$showMessageService', $showMessageService) {
var self = this;
self.changeText = function() {
$showMessageService('testDirective').set('NEW');
}
}])
I've Change The Directive Code, moved Controller code to link and it's working fine.
testApp.directive("helloDirective", function () {
return {
restrict: "E",
template: '<input type="text" data-ng-model="model.msg" />',
scope: {},
bindToController: {
param: "="
},
link: function (scope, element, attrs) {
var self = scope.model;
var assignMethod = function () {
if (self.param == undefined) {
self.param = {};
}
self.param.actions = {
get: function () {
return self.msg;
},
set: function (msgData) {
self.msg = msgData;
}
};
};
assignMethod();
},
controller: function () { },
controllerAs: 'model'
}
});
now i can call the directive get Method from calling controller like,
self.helloParam = {};
self.click = function () {
alert(self.helloParam.actions.get());
}

Passing an object as attribute to compiled directive on the fly

I have a angular element on the page which needs to communicate with the rest of the non angular page elements.
I am creating directive elements on the fly, and appending it to its target div. I am trying to pass that created directive an object (ajax object), which contains just attributes.
The issue is that I can't figure out how to pass just this ajax object to the directive, as $compile requires a scope. When the http finishes, and because i have to use = in the directive, the directives are being over-ridden.
Please see my plunk: https://plnkr.co/edit/brTWgUWTotI44tECZXlQ ( sorry about the images ). Click the <button> to trigger the directive.
(function() {
'use strict';
var CHANNEL = 'podOverlay';
angular.module('CavernUI', [])
.controller('CavernCtrl', function($scope,getItemService) {
$scope.model = {};
var _pods = $scope.model.pods = {};
function getData(selector) {
$(selector).each(function(i, pod) {
_pods[+pod.dataset.item] = {
$: $(pod)
};
});
Object.keys($scope.model.pods).map(function(key) {
getItemService.getItem(key).success(function(response) {
_pods[key] = angular.extend(_pods[key], response);
$scope.$broadcast(CHANNEL, _pods[key], $scope);
});
})
}
$scope.runPodCheck = function(selector) {
getData(selector);
}
})
.directive('podchecker', function($compile) {
var createOverlay = function(e,data,scope){
scope.data = data;
// can i just pass data rather than scope.data?
// If I pass the scope, then when another $broadcast happens
// the scope updates, wiping out the last scope change.
// Scope here really needs to be a static object that's
// created purely for the hand off. But I don't know if
// that can be done.
angular.element(data.$[0]).empty().append($compile('<overlay data="data"></overlay>')(scope));
}
return {
restrict: 'E',
scope: {
check: '&',
},
templateUrl: 'tpl.html',
link: function(scope,elm,attr){
scope.$on(CHANNEL,createOverlay);
}
};
})
.directive('overlay', function() {
return {
restrict: 'E',
scope: {
o: '=data' // here is the problem.
},
template: '<div class="overlay"><img ng-src="{{o.images.IT[0]}}"/></div>',
link: function(scope, elm, attr) {
}
}
})
.service('getItemService', ['$http', function($http) {
this.getItem = function(itemId) {
return $http({
method: 'GET',
url: 'https://www.aussiebum.com/ajaxproc/item',
params: {
id: itemId,
ajxop: 1
},
});
};
}]);
}());
Edits:
Expected ouput:
I'm not sure this is the best approach, but one way might be to manually create a new scope for each of the overlays.
So changed this:
var createOverlay = function(e,data,scope){
scope.data = data;
angular.element(data.$[0]).empty().append($compile('<overlay data="data"></overlay>')(scope));
}
to this:
var createOverlay = function(e,data,scope){
var overlayScope = scope.$new(false); // use true here for isolate scope, false to inherit from parent
overlayScope.data = data;
angular.element(data.$[0]).empty().append($compile('<overlay data="data"></overlay>')(overlayScope));
}
Updated Plnkr: https://plnkr.co/edit/wBQ1cqVKfSqwqf04SnPP
More info about $new()
Cheers!

Accessing data between controller directive and factory

I have created a global variable in factory. And I have accessed the global variable in my controller but upon changing the value in the directive it is unable to update in the controller.
My directive is
myApp.directive('quiz', function(quizFactory) {
return {
restrict: 'AE',
scope: {},
templateUrl: 'templete.html',
controller: function($scope){
},
link: function(scope, elem, attrs,UserService) {
scope.start = function() {
UserService.var1=true;
}
}
};
My Factory is
myApp.factory('UserService', function() {
return {
var1 : false
};
});
My controller is
myApp.controller('LearnSetQue', ['$scope','UserService',
function($scope,UserService){
$scope.var2=UserService.var1;
$scope.var3=UserService.var1;
}
]);
Here start is button function
<button ng-click="start()">Start</button>
Here upon clicking the start button the var1 should become true var1=true,var2=true and var3=true and how can I update that in the controller.
First in the service, you should return an object with the properties you want to share across your app:
myApp.factory('UserService', function() {
var properties = { var1: false };
return {
getProperties : function() { return properties; }
};
});
Then in the directive
scope.start = function() {
UserService.getProperties().var1=true;
}
And in the controller you should have:
myApp.controller('LearnSetQue', ['$scope','UserService',
function($scope,UserService){
$scope.properties = UserService.getProperties();
]);
And then on the view, just reference the var1 directly
<div>{{ properties.var1 }}</div>

Angular: Evaluating a function on controller scope then passing it to a directive

I'm trying to evaluated a function on the parent controller then send it into a directive. I need the values to be watched by the digest loop and updated when a user updates them.
I've worked through some original issues, but am having trouble with getting the bindings to update.
I have a Controller with an object and a function that checks if an object has values, it returns true or false:
this.foo = {
obj1: {
name: '',
time: 'time2'
},
obj2: {
name: 'name2',
time: 'time2'
}
};
this.isPaneComplete = function(tab) {
var complete = true;
var tab2 = tab.tab;
for (var prop in tab2) {
if (tab2.hasOwnProperty(prop)) {
complete = !!tab2[prop] && complete;
}
}
return complete;
};
I have a directive called MyPane with this scope :
scope: {
completed : '&myPaneComplete'
},
This is my template:
<my-pane my-pane-complete="gigEditCtrl.isPaneComplete({tab : gigEditCtrl.foo.obj1})">
<input type="text" placeholder="2014-12-31" ng-model="gigEditCtrl.foo.obj1.name">
<input type="text" placeholder="2014-12-31" ng-model="gigEditCtrl.foo.obj1.time">
When running the following console.log I get TRUE or FAlSE in my Directive
link: function(scope, element, attrs, tabsCtrl) {
console.log(scope.completed());
},
This all works great. However, when I update the values in the input boxes the controller function isn't run again and the console.log isn't fired. Thoughts?
the solution for your issue is to use $watch in your directive and it will look something like this
testApp.directive('myDir', function(){
return{
scope:{
test: '&'
},
link: function(scope, el, attrs){
scope.$watch(scope.test, function(newValue){
console.log('from dir name = '+ newValue);
});
}
};
});
basically you will you will use $watch to watch changes in the return value of your function.
I've setup a working demo for you here http://plnkr.co/edit/Gk2tILTql8NW1QkvVRRk?p=preview

directive scope variables not accessible in jasmine test

I have a directive like below:
angular.module('buttonModule', []).directive('saveButton', [
function () {
function resetButton(element) {
element.removeClass('btn-primary');
}
return {
restrict: 'E',
replace: 'false',
scope: {
isSave: '='
},
template:
'<button class="btn" href="#" style="margin-right:10px;" ng-disabled="!isSave">' +
'</button>',
link: function (scope, element) {
console.log(scope.isSave);
scope.$watch('isSave', function () {
if (scope.isSave) {
resetButton(scope, element);
}
});
}
};
}
]);
and the jasmine test as below:
describe('Directives: saveButton', function() {
var scope, compile;
beforeEach(module('buttonModule'));
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
compile = $compile;
}));
function createDirective() {
var elem, compiledElem;
elem = angular.element('<save-button></save-button>');
compiledElem = compile(elem)(scope);
scope.$digest();
return compiledElem;
}
it('should set button clean', function() {
var el = createDirective();
el.scope().isSaving = true;
expect(el.hasClass('btn-primary')).toBeFalsy();
});
});
The issue is the value of isSaving is not getting reflected in the directive and hence resetButton function is never called. How do i access the directive scope in my spec and change the variable values. i tried with isolateScope but the same issue persists.
First note that you are calling the resetButton function with two arguments when it only accepts one. I fixed this in my example code. I also added the class btn-primary to the button element to make the passing of the test clearer.
Your directive is setting up two-way databinding between the outer scope and the isolated scope:
scope: {
isDirty: '=',
isSaving: '='
}
You should leverage this to modify the isSaving variable.
Add the is-saving attribute to your element:
elem = '<save-button is-saving="isSaving"></save-button>';
Then modify the isSaving property of the scope that was used when compiling (you also need to trigger the digest loop to make the watcher detect the change):
var el = createDirective();
scope.isSaving = true;
scope.$apply();
expect(el.hasClass('btn-primary')).toBeFalsy();
Demo: http://plnkr.co/edit/Fr08guUMIxTLYTY0wTW3?p=preview
If you don't want to add the is-saving attribute to your element and still want to modify the variable you need to retrieve the isolated scope:
var el = createDirective();
var isolatedScope = el.isolateScope();
isolatedScope.isSaving = true;
isolatedScope.$apply();
expect(el.hasClass('btn-primary')).toBeFalsy();
For this to work however you need to remove the two-way binding to isSaving:
scope: {
isDirty: '='
}
Otherwise it would try to bind to something non-existing as there is no is-saving attribute on the element and you would get the following error:
Expression 'undefined' used with directive 'saveButton' is
non-assignable!
(https://docs.angularjs.org/error/$compile/nonassign?p0=undefined&p1=saveButton)
Demo: http://plnkr.co/edit/Ud6nK2qYxzQMi6fXNw1t?p=preview

Resources