I 'm trying to handle data that is coming from my database in a directive , however these data are being pulled by a controller and being assigned to scope like this:
Calendar Controller:
'use strict';
var CalendarController = ['$scope', 'EventModel', function(scope, EventModel) {
scope.retrieve = (function() {
EventModel.Model.find()
.then(function(result) {
scope.events = result;
}, function() {
});
}());
}];
adminApp.controller('CalendarController', CalendarController);
Calendar Directive:
'use strict';
var calendarDirective = [function($scope) {
var Calendar = {
init: function(events) {
console.log(events);
}
};
return {
link: function(scope) {
Calendar.init(scope.events);
}
};
}];
adminApp.directive('calendarDirective', calendarDirective);
But the data is undefined in the directive, and in the controller the data appears to be ok.
Thanks!
This is a common error for people starting out with AngularJS. This is a load order issue. The events scope variable is not defined when the directive link function is executed. One solution is to use a watch on the variable passed into the directive and load once it is defined.
return {
link: function(scope) {
scope.$watch('events', function() {
if(scope.events === undefined) return;
Calendar.init(scope.events);
});
}
};
In the above scenario controller and directives have isolated scope. One solution will be to use factory/service to communicate with the server and inject the service to controller and directive. Since service/factory is singleton you can cache the data & share is between controllers & directives.
try this way.
Directive html
<calendar-directive objEvent="events" ></calendar-directive >
JS :
'use strict';
var calendarDirective = [function($scope) {
return {
scope : { objEvent : '=objEvent'}
template: '<div>{{ objEvent }}</div>',
};
}];
Use angular isolated scopes.
Related
I will explain what exactly I'm trying to do before explaining the issue. I have a Directive which holds a form, and I need to access that form from the parent element (where the Directive is used) when clicking on a submit button to check fi the form is valid.
To do this, I am trying to use $scope.$parent[$attrs.directiveName] = this; and then binding some methods to the the Directive such as this.isValid which will be exposed and executable in the parent.
This works fine when running locally, but when minifying and building my code (Yeoman angular-fullstack) I will get an error for aProvider being unknown which I traced back to a $scopeProvider error in the Controller.
I've had similar issues in the past, and my first thought was that I need to specifically say $inject for $scope so that the name isn't lost. But alas.....no luck.
Is something glaringly obvious that I am doing wrong?
Any help appreciated.
(function() {
'use strict';
angular
.module('myApp')
.directive('formDirective', formDirective);
function formDirective() {
var directive = {
templateUrl: 'path/to/template.html',
restrict: 'EA',
scope: {
user: '='
},
controller: controller
};
return directive;
controller.$inject = ['$scope', '$attrs', 'myService'];
function controller($scope, $attrs, myService) {
$scope.myService = myService;
// Exposes the Directive Controller on the parent Scope with name Directive's name
$scope.$parent[$attrs.directiveName] = this;
this.isValid = function() {
return $scope.myForm.$valid;
};
this.setDirty = function() {
Object.keys($scope.myForm).forEach(function(key) {
if (!key.match(/\$/)) {
$scope.myForm[key].$setDirty();
$scope.myForm[key].$setTouched();
}
});
$scope.myForm.$setDirty();
};
}
}
})();
Change the directive to a component and implement a clear interface.
Parent Container (parent.html):
<form-component some-input="importantInfo" on-update="someFunction(data)">
</form-component>
Parent controller (parent.js):
//...
$scope.importantInfo = {data: 'data...'};
$scope.someFunction = function (data) {
//do stuff with the data
}
//..
form-component.js:
angular.module('app')
.component('formComponent', {
template:'<template-etc>',
controller: Controller,
controllerAs: 'ctrl',
bindings: {
onUpdate: '&',
someInput: '<'
}
});
function Controller() {
var ctrl = this;
ctrl.someFormThing = function (value) {
ctrl.onUpdate({data: value})
}
}
So if an event in your form triggers the function ctrl.someFormThing(data). This can be passed up to the parent by calling ctrl.onUpdate().
I am having a few problems accessing my controller on a directive that I am trying to unit test with jasmine and karma testrunner. The directive looks like this:
directive
angular.module('Common.accountSearch',['ngRoute'])
.directive('accountSearch', [function() {
return {
controllerAs: 'ctrl',
controller: function ($scope, $element, $routeParams, $http) {
this.setAccount = function () {
var response = { AccountId : $scope.ctrl.searchedAccount.AccountId }
$scope.callback(response)
}
this.getAccounts = function(searchText){
return $http.get('/api/CRMAccounts', {
params: {
retrievalLimit: 10,
search: searchText
}
}).then(function(response){
return response.data;
});
}
},
scope : {
config : '=',
values : '=',
callback : '='
},
templateUrl : '/common/components/account-search/account-search.html',
restrict : 'EAC'
}
}]);
This here is the test case file so far I believe all is in order and correct (I hope):
test case file:
describe("Account search directive logic tests", function (){
var element,$scope,scope,controller,template
beforeEach(module("Common.accountSearch"))
beforeEach(inject( function (_$compile_, _$rootScope_,_$controller_,$templateCache) {
template = $templateCache.get("components/account-search/account-search.html")
$compile = _$compile_;
$rootScope = _$rootScope_;
$controller = _$controller_;
scope = $rootScope.$new();
element = $compile(template)(scope)
ctrl = element.controller
scope.$digest();
// httpBackend = _$httpBackend_;
}));
it(" sets the account and calls back.", inject(function () {
console.log(ctrl)
expect(ctrl).toBeDefined()
}));
//httpBackend.flush()
});
I have managed to print the controller of the directive ( I think) to the console which returns the following ambiguous message:
LOG: function (arg1, arg2) { ... }
I cannot access any of the functions or properties on the directive as they are all returning "undefined", what am I doing wrong?
Controllers for directives are actually fully injectable - instead of providing a constructor, you can just refer to the controller by name. See the directive definition object docs for Angular here: https://docs.angularjs.org/api/ng/service/$compile#directive-definition-object
In your case where you want to unit test the controller you'd just do it like this:
common.accountSearch.js
angular.module('Common.accountSearch', [])
.directive('accountSearch', [function () {
return {
controller: 'accountSearchCtrl',
scope: {
config : '=',
values : '=',
callback : '='
},
templateUrl : '/common/components/account-search/account-search.html',
restrict: 'EAC'
}
}])
.controller('accountSearchCtrl', ['$scope', function ($scope) {
$scope.setAccount = function () {
var response = {
AccountId: $scope.ctrl.searchedAccount.AccountId
};
$scope.callback(response);
}
$scope.getAccounts = function (searchText) {
// Code goes here...
}
}]);
common.accountSearch-spec.js
describe("Account search directive logic tests", function () {
var controller, scope;
beforeEach(module("Common.accountSearch"));
beforeEach(inject(function (_$controller_, _$rootScope_) {
$rootScope = _$rootScope_;
scope = $rootScope.$new();
controller = _$controller_('accountSearchCtrl', { '$scope': scope });
}));
it(" sets the account and calls back.", function () {
expect(controller).toBeDefined();
});
});
This way you can just inject your controller directly into your jasmine tests like any of your other controllers.
Hope this helps.
So close!
element.controller is a function and needs to be passed the name of the directive which you're attempting to get the controller for. In this case it would be
ctrl = element.controller("accountSearch");
element.controller is an additional method to AngularJS jqLite, so when you log it you see jqLite method .toString(). You should call it and get a directive controller. Element controller manual
I have a simple controller that works just fine:
app.controller('IndexController', ['$scope', obj.indexPage]);
var obj = {};
obj.indexPage = function ($scope) { // do controller stuff };
I also have an event function that i want to use to load/create/instantiate this controller:
// some callback, doesn't really matter
app.onPage('index', function () {
// load and run controller logic in here
app.controller('IndexController', ['$scope', obj.indexPage]);
}, obj);
there are some issues, like Argument 'IndexController' is not a function, got undefined
Any ideas?
my solution:
app.controller('IndexController', ['$scope', function ($scope) {
var obj = {};
obj.indexPage = function (data) {
// do controller stuff
};
app.onPage('index', function (data) {
obj.indexPage(data);
}, obj);
});
Due to how the angular module system works, you can't instantiate controllers asynchronously like that. You can however, use the $controller service to create controllers on the fly. The same technique below is often used in unit testing.
For example:
angular.module('app', [])
.controller('MyCtrl', function($rootScope, CtrlFactory){
var dynamicCtrl = CtrlFactory.create({$scope: $rootScope.$new()});
console.log(dynamicCtrl.method()); //-> 123
})
.factory('CtrlFactory', function($controller) {
return {
create: function(locals) {
return $controller(
//this is the constructor of the new controller
function($scope){
console.log('Dynamic controller', $scope);
this.method = function() { return 123; };
},
//these are the injected deps
locals
);
}
};
})
For some example usage in a unit testing context, see: https://docs.angularjs.org/guide/controller.
I'll add that you may want to reconsider your reasons for doing this--I can't say I've seen $controller used outside testing.
app.onPage('index', function () {
app.controller('IndexController', obj.indexPage); // this would load the controller to the module
$controller('IndexController', { $scope: $scope }); // This would instantiate the controller, NOTE: $controller service should be injected
}, obj);
I am struggling with data binding in AngularJs.
I have the following piece of markup in .html file that includes the custom directive:
<my-directive ng-repeat="i in object" attr-1="{{i.some_variable}}"></my-directive>
Note: 'some-variable' is being updated every 10 seconds(based on the associate collection and passed to template through controller).
The directive's code includes:
myApp.directive('myDirective', function () {
scope: {
'attr-1': '=attr1'
which throws this exception because of the brackets in attr-1(see html code above).
It works though if I use read-only access(note at sign below):
myApp.directive('myDirective', function () {
scope: {
'attr-1': '#attr1'
I use scope.attr-1 in directive's HTML to show its value.
The problem is that with read-only access UI is not reflecting the change in attribute change.
I've found solution with $parse or $eval(couldn't make them work tho). Is there a better one there?
You'll need only two-way binding and I think $parse or $eval is not needed.
Please have a look at the demo below or in this fiddle.
It uses $interval to simulate your updating but the update can also come from other sources e.g. web socket or ajax request.
I'm using controllerAs and bindToController syntax (AngularJs version 1.4 or newer required) but the same is also possible with just an isolated scope. See guide in angular docs.
The $watch in the controller of the directive is only to show how the directive can detect that the data have changed.
angular.module('demoApp', [])
.controller('MainController', MainController)
.directive('myDirective', myDirective);
function MainController($interval) {
var self = this,
refreshTime = 1000; //interval time in ms
activate();
function activate() {
this.data = 0;
$interval(updateView, refreshTime);
}
function updateView() {
self.data = Math.round(Math.random()*100, 0);
}
}
function myDirective() {
return {
restrict: 'E',
scope: {
},
bindToController: {
data: '='
},
template: '<div><p>directive data: {{directiveCtrl.data}}</p></div>',
controller: function($scope) {
$scope.$watch('directiveCtrl.data', function(newValue) {
console.log('data changed', newValue);
});
},
controllerAs: 'directiveCtrl'
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.js"></script>
<div ng-app="demoApp" ng-controller="MainController as ctrl">
model value in ctrl. {{ctrl.data}}
<my-directive data="ctrl.data"></my-directive>
</div>
I've come to the following solution(in case somebody runs into the the same problem):
// Directive's code
myApp.directive('myDir', function () { return {
restrict: 'E',
templateUrl: function () {
return 'my-dir.html';
},
scope: {
'id': '#arId',
'x': '#arX',
'y': '#arY',
//....
},
link: function ($scope, element, attrs) {
// *** SOLUTION ***
attrs.$observe('arId', function (id) {
$scope.id = id;
});
//...
}
Update: somebody sent me this answer, they have the same problem and came up with a very similar if not exact same solution:
Using a directive inside an ng-repeat, and a mysterious power of scope '#'
It is useful to read because they explain what's the idea behind it.
I'm struggling to figure out how to do this. Hope anyone can help :)
I have multiple controllers in my Angular app. Like titleCtrl and SettingsCtrl
I have a service which holds a variable like this:
var myVar = {title: 'test', settings: {color: 'black', font: 'verdana'}};
I'm making a $http.get request to update the "myVar" variable from the server.
The question is, how do I update the $scope.title in titleCtrl and $scope.settings in SettingsCtrl AFTER the http request has finished? I know how to do it in a single controller, but how do I update the $scopes in multiple controllers?
Use a watch on that variable in the service. When its updated, then update your values in controller scope. Here's an example:
Inside your controller, you can watch a var myVar on YourService and when it changes, update a variable called myVarInController with the value it changed to.
$scope.$watch(
// This function returns the value being watched.
function() {
return YourService.myVar;
},
// This is the change listener, called when the value returned above changes
function(newValue, oldValue) {
if ( newValue !== oldValue ) {
$scope.myVarInController = newValue;
}
}
);
Just in you service create a object when you get data from you server copy it to that object, so all your controllers can reference to that object.
Please see here http://plnkr.co/edit/j25GJLTHlzTEVS8HNqcA?p=preview
JS:
var app = angular.module('plunker', []);
app.service('dataSer', function($http) {
var obj = {};
getData = function() {
$http.get("test.json").then(function(response) {
angular.copy(response.data, obj);
});
}
return {
obj: obj,
getData: getData
};
});
app.controller('MainCtrl', function($scope, dataSer) {
$scope.data = dataSer;
$scope.get = function() {
$scope.data.getData()
}
});
app.controller('SecondCtrl', function($scope, dataSer) {
$scope.data = dataSer;
});
HTML:
<div ng-controller="MainCtrl">
<button ng-click="get()">get data</button>
<p>Fist Controller:
<br/>{{ data.obj.title}}</p>
</div>
<div ng-controller="SecondCtrl">
<p>Second Controller:
<br/>{{data.obj.settings}}</p>
</div>
Use both factory and service to pass value to two controllers. This is the only way to pass value
angular.module('mulipleCtrlApp', [])
.service('shareService', function () {
return {
title: 'test',
settings: {
color: 'black',
font: 'verdana'
}
};
})
.controller('titleCtrl', function ($scope, shareService) {
$scope.myVar = shareService;
$scope.testchange = function () {
$scope.myVar.title = 'Completed test';
};
})
.controller('settingCtrl', function ($scope, shareService) {
$scope.myVar = shareService;
});
Egghead Link
Jsfiddler Link example
Make your service return promise object.
Then in controller you can define a success call back to fetch title in one and settings in
another controller once the promise is resolved.
Code to use promises
In your service class:
var factory = {};
var factory.fetchData = function () {
return $http({method: 'GET', url: '/someUrl'});
}
return factory;
In controller 1:
$scope.getData = function(){
factory.fetchData().success(response){
$scope.title = response.title;
}
}
Similarly you can update controller 2, to set settings data.
I've found a better and easier maintainable solution in my opinion. Simply do the following to achieve to-way data-binding between one (or more) controller(s) with a service:
Lets assume you fetch (i.e. $http) and store data in your service (serviceName) in the variable serviceData.
In your controller reference the service like this to achieve to-way data-binding:
$scope.data = serviceName
In your view/html bind to the data properties like this:
<input ng-model="data.serviceData.title">
Thats it! :) When your serviceData variable updates the view/scope does as well. This will work with multiple controllers.