I've been with Angularjs a few days and I'm struggling with a few aspects of it. I'll do my best to try and explain what the issue is, and I'd really appreciate any help anyone can give me about it.
My situation (simplified) is this:
I have a service which loads some info from a json and stores it in an object. It also have some functions to be used for other controllers to retrieve that information.
var particServices = angular.module('particServices', []);
particServices.service('particSrv', function() {
var data = {};
this.updateData = function(scope) {
data = // http call, saves in data
}
this.getName = function(code) {
return data.name;
}
});
I have an html page assisted by a controller, which uses a directive board (no params, really simple). This is the controller:
var bControllers = angular.module('bControllers', []);
bControllers.controller('bController', ['$scope', 'particSrv', function ($scope, particSrv) {
$scope.getName = function(code) {
return particSrv.getName(code);
};
particSrv.updateData($scope);
}]);
As you can see, the controller makes the call to initialize the object in the service. As this is a singleton, I understand once that info is loaded no other call needs to be make to updateData and that info is available to others using the getters in the service (getName in this case).
I have a really simple directive board (which I simplified here), which uses another directive bio.
angular.module('tsDirectives', [])
.directive('board', ['dataSrv', 'particSrv', function(dataSrv, particSrv) {
return {
restrict: 'E',
replace: true,
scope: true,
controller: function($scope) {
$scope.getName = function(code) {
return particSrv.getName(code);
};
dataSrv.updateData($scope, 'board', 'U');
},
templateUrl: '<div class="board"><div bio class="name" partic="getName(code)"/></div></div>'
};
}]);
And this is the bio directive:
angular.module('gDirectives', [])
.directive('bio', function() {
return {
scope: {
partic: '&'
},
controller: function($scope) {
$scope.name = $scope.partic({code: $scope.athid});
},
template: '<a ng-href="PROFILE.html">{{name}}</a>'
};
})
Now, what I expected is that in the bio directive the info retrieved from party was displayed, but apparently this directive is processed before the partic is initialized in the main controller.
I was under the impression that even though this information was still not loaded when the directive is processed, as soon as the service finishes and the info is ready, automagically it would appear in my directive, but that does not seem to work like that. I've been reading about $watch and $digest, but I fail to see why (and if) I would need to call them manually to fix this.
Any hint will be much appreciated. I could provide more technical details if needed.
Directive will initialise when app is loaded and user opens the page where that directive is, if you have some property that is set later (from api for example), it will update that property in directive but that directive will not be reinitialised ($scope.partic({code: $scope.athid}) wont be called).
If you want for directive to wait for initialisation you should use ng-if. Something like this:
<div data-directive-name data-some-property="someProperty" data-ng-if="someProperty"></div>
In this case directive will be initialised when (if) you have some value in $scope.someProperty. But this is not very good if you can have false values for someProperty.
In that case you would need to use some kind of loaded flag.
You have not included "particServices" as a dependency in other modules which use the services of "particServices". Your modules should look like:
var bControllers = angular.module('bControllers', ['particServices']);
angular.module('tsDirectives', ['particServices']);
angular.module('gDirectives', ['particServices']);
Related
Ok, I'm trying to test the outcome of a function that updates the DOM>
I have a directive that loads a template via url.
Then a controller calls a factory method to update the html table with data.
I have the tests showing that I can get the data that is all good.
but how can I test that the updates to the table have taken place?
I am using NodeJS with Karma and Jasmine.
I have followed tutorials on how to load in templates, and I have that working, I can load and access the templates in my test fine.
but when I run the method to update the table, the tests fail.
I'll give an scaled down example of what I'm trying to do. Note, this is just demo code, Not a working app.
Template.
<table><tr><td class="cell1"></td></tr></table>
Directive.
dataTable.directive('dataTable', function () {
return {
restrict: 'E',
templateUrl: 'path/to/template/dataTable.html'
};
});
Controller
dataTable.controller('dataTableController', ['$scope', 'dataTableFactory',
function ($scope, dataTableFactory){
$scope.updateTable = function(){
dataTableFactory.loadData();
// code to load data from dataTableFactory here! //
dataTableFactory.updateTable();
}
}])
Factory
dataTable.factory('dataTableFactory',['$document',function($document){
var _tableData;
return(
"tableData": _tableData,
loadData: function(){
// code to get data and populate _tableData.
}
updateTable: function(){
$document.find('.cell1').append(this.tableData.data);
}
)
}])
Unit Test
describe('dataTable Tests', function () {
var scope, element, $compile, mDataTableFactory, controller, tableData, doc, factory;
beforeEach(module('dataTable'));
beforeEach(module('app.templates')); // setup via ng-html2js
beforeEach(inject(function (_$rootScope_, _$compile_,_$controller_,_dataTableFactory_) {
scope = _$rootScope_.$new();
doc = _$compile_('<flood-guidance></flood-guidance>')(scope);
factory = _dataTableFactory_;
controller = _$controller_('dataTableController', {
$scope: scope,
$element: doc,
dataTableFactory: factory
});
scope.$digest();
}));
it("Template should contain the cell cell1", function(){
expect(doc.find('.cell1').contents().length).toBe(0);
expect(doc.find('.cell1').html()).toBeDefined();
});
// Passes fine, so I know the template loads ok.
it('Should show data in cell1',function(){
factory.tableData = {data: 'someData'};
scope.updateTable();
expect(doc.find('.cell1').contents().length).toBe(1);
expect(doc.find('.cell1').html()).toBe('SomeData');
});
});
});
Test Ouptut
Expected 0 to be 1. Expected '' to be 'someData'.
If I put the updateTable code in to the controller and call the update function there, the test passes, but I'd like to have this in a factory, how can I make this test pass (the app runs and works as expected, I just can't get a working test).
I understand this kind of testing is more focused on the UI and not exactly 'Unit Testing' but is it possible to do this?
So essentially updateTable cannot find the changes performed by factory.tableData. I guess the problem may be due to the way how your factory exposes the _tableData property.
Could you try to modify your factory like this:
dataTable.factory('dataTableFactory',['$document',function($document){
var _tableData;
return(
getTableData: function() { return _tableData; },
setTableData: function(newVal) { _tableData = newVal; },
loadData: function(){
// code to get data and populate _tableData.
}
updateTable: function(){
$document.find('.cell1').append(this.tableData.data);
}
)
}])
and then of course use the setter/getter accordingly. See if it works this way.
OK so I'm still not sure if I fully get your intention but here is a fiddle with my refactored example.
http://jsfiddle.net/ene4jebb/1/
First of all the factory shouldn't touch the DOM, that's the directives responsibility. Thus my rework passes the cellvalue (new scope property) to the directive, which renders it. Now when you call setTableData (which will change _tableData.data) and since in test environment call the $digest loop yourself, the directive will automatically redraw the new stuff.
Controller is kept thin as possible thus only providing a scope property to the factory.
As said not sure if you were after this, but hope it helps. If there are any questions just ask.
I've got a potentially really dumb question, but how do I modify variables up in $rootScope in Angular? I've got a slide-in sidebar that I want to change the content on whenever someone clicks on a thumbnail, and I figured the easiest way to handle where the data in the sidebar comes from/the sidebar visibility would either be in global values, or in $rootScope. I'm trying to keep everything as simple as possible, but I just don't know how to handle modifying global variables.
My angular code surrounding this is:
app.run(function($rootScope) {
$rootScope.currentUrl = { value: 'visual/design/1/' };
$rootScope.detail_visible = { value: true };
});
app.controller('navController', ['$scope', '$rootScope',
function ($scope, $rootScope) {
$scope.isDetail = $rootScope.detail_visible.value;
$scope.url = $rootScope.currentUrl.value;
$scope.hide = function($rootScope) {
$rootScope.detail_visible.value = false;
};
}]);
and the connecting HTML is
<div id="detail_box" ng-class="{d_show: isDetail, d_hide: !isDetail}">
<div ng-include="url + 'detail.html'"></div>
</div>
In essence, I'm trying to make it so that when you click on a thumbnail, it changes the currentUrl value from 'visual/design/1/' to whatever they've clicked on (like, 'music/solo/2' or whatever) then changes the value of detail_visible to false, so that the classes on my sidebar switch and I get a nice little slide-in, with fresh content loaded via ng-include which I kind of love a thousand times more than I thought I would. I've been banging my head against this for about three hours now, breaking everything else on this app whenever I get the chance. What am I screwing up here? Alternatively, is there a better way of doing this?
My reason for using global variables is that I have multiple thumbnails in multiple controllers, and I want each one to be able to dynamically change the URL in my ng-include.
For your question, you change the $rootScope variable simple by referencing it with
$rootScope.detail_visible.value = newValue;
but you dont need to inject $rootScope to your function:
$scope.hide = function() { //without $rootScope
$rootScope.detail_visible.value = false;
};
But, I would suggest you to implement a service and not to pollute the rootscope for such task.
https://docs.angularjs.org/guide/services
Object properties of scopes are inherited -- in your controller, you should be able to modify $scope.detail_visible.value and see it affect the $rootScope. You still have to initialize it on the $rootScope in .run() though.
app.run(function($rootScope) {
$rootScope.currentUrl = { value: 'visual/design/1/' };
$rootScope.detail_visible = { value: true };
});
app.controller('navController', ['$scope', function ($scope, $rootScope) {
$scope.hide = function() { // don't need to pass an argument
$scope.detail_visible.value = false;
};
}]);
view:
<div id="detail_box" ng-class="{d_show: currentUrl.value, d_hide: !currentUrl.value}">
<div ng-include="currentUrl.value + 'detail.html'"></div>
</div>
I'm looking for a way to access both controllers inside the directive.
Currently it's not working and I don't know why or if it's even possible.
If I use both require and controller option, inside the link function ctrl property refers to whatever I requested via the require option.
I can't think of a way to access the controller inside the link function when the require option is present.
It seems that these two properties are mutually exclusive ?
angular.module('moduleName').directive('directiveName', [function () {
return {
controller: 'MediaController',
require:'ngController',
link: function (scope, element, attributes, ctrl) {
// I need access to both controllers here
}
}
}]);
If you want both controllers, then require both controllers:
angular.module('moduleName').directive('directiveName', [function () {
return {
controller: MediaController,
require:['directiveName', 'ngController'],
In this case ctrl is an array containing the two controllers.
Without really knowing why you need to access both controllers, I can only offer minimal advice here. My suggestion would be to create a service to handle cross controller needs. Services are singletons and they support data binding. Services are my preference for cross controller work every day. For example:
App.controller('Ctrl1', function Ctrl1 ($scope, TestService) {
$scope.someValue = TestService.getValue();
});
App.controller('Ctrl2', function Ctrl2 ($scope, TestService) {
$scope.someValue = TestService.getValue();
});
App.factory('TestService', function() {
var myVal = "I Bound";
return {
getValue: function() {
return myVal;
}
}
});
This method allows you to abstract a controllers need to directly access another controller. Your services can be pulled into these directives or other services too. I hope this helps a bit.
Thanks,
Jordan
I have a service that watches something on the rootscope and does something in response. Nobody else requires that service as a dependency. How do I tell the service to start doing its thing?
This is how I do it for now.
angular.module('app')
.service('myService', function ($rootScope) {
return function () {
$rootScope.$on("...", function () {
//do stuff
});
};
})
.run(function (myService) {
myService();
});
Is there a cleaner way to do this?
I've implemented similar things, but using a directive, and not a service:
app.directive('myDirective', function() {
return {
controller: function($scope) {
$scope.$on('myEvent', function () {
// Do stuff
});
}
}
});
And then used this in the root element of my application, which can, for example, be body:
<body ng-app="app" my-directive>
The benefit of using directives in this way over services is that it makes it a touch less global / final. For example, if you end up wanting some scopes to not be able to $emit events to this handler, then you could move the myDirective to another element. Or you could maybe down the road you'll want a part of your app to respond slightly differently to myEvent, so you can add another instance of myDirective, maybe passing it some options via attributes. On the whole it leaves it a lot more open to adding complex behaviour.
I've spent the night on trying to figure this out and have finally decided to give up and ask for help.
I'm building a web-app with AngularJS that is designed to work with flakey connections (mobiles).
I'm trying to implement the functionality for a user to add a object (whether that's an appointment, book, etc is irrelevant) to the server.
Service that handles syncing objects with the server:
angular.module('App')
.service('syncUp', function syncUp($http, $q, app) {
this.addObject = function addObject(object) {
var deferred = $q.defer();
app.inSync = false;
var httpConfig = {
method: 'POST',
url: 'http://myurl.dev/app_dev.php/api/add-object',
data: object
}
function persist() { setTimeout(function() {
$http(httpConfig).
success(function(data, status) {
app.inSync = true;
deferred.resolve(data.id);
}).
error(function(data, status) {
app.inSync = false;
persist();
});
}, 3000);
};
persist();
return deferred.promise;
}
});
'app' service that the status bar is bound to:
'use strict';
angular.module('App')
.service('app', function app($http, $q) {
this.inSync = true;
});
Template binding to the 'app' service inSync property:
<div class="status" ng-class="{'insync':inSync}"></div>
Specific object service that sends data from the controller to the syncUp service:
this.addBook = function(book)
{
var tempId = syncUp.generateUid();
this.books[tempId] = book;
this.books[tempId].tempId = tempId;
syncUp.addObject({
'type': 'book',
'data': this.books[tempId]
}).then(function(newId) {
booksRef[newId] = book;
delete booksRef[tempId];
}, function() {});
}
Everything is working as it should (data is being persisted to the server and the ID is being returned and replacing the tempId just fine. The problem is, when the inSync key on the 'app' service is updated, the class isn't added/removed from the div as it should be with ng-class in the template. If I load another route, that will force iterate through whatever internal cycle angular is doing and update the class on the template.
I've tried all manner of $apply() solutions, moving where the app.inSync key is set back to true, looping a function watching it. It's being set in all the right places (from debugging I know it's set back to true correctly), I just can't figure out how to make the change appear on the UI.
I tried:
$rootScope.$apply(function() {
app.inSync = true;
});
Which gave me an error (already running a digest, or something).
So I tried the 'safeApply' version that has been circulated on many answers/blogs, which didn't throw the error, but didn't work either.
As far as I can figure out, the UI should be updated when promises are resolved (both the http and my syncUp.addObject promise are resolved, so I'm not sure why it's not working.
Any ideas? I need to keep the current implementation of promises to be able to set the returned ID from the server on the added object, to avoid a circular-dependency issue between the syncUp and object angular services.
Edit:
And the status bar directive:
angular.module('App')
.directive('navigation', function (app) {
return {
templateUrl: '/app/views/navigation.html',
restrict: 'E',
link: function (scope, element, attrs) {
scope.inSync = app.inSync;
}
}
});
References you make in templates refer to objects on the current $scope. Services do usually not create or add anything to the $scope, so putting properties on a service, will not make them available to the template. To get stuff on the $scope, you need to use a controller. You can use the ng-controller directive to reference a controller, you'll find examples of this in the first AngularJS tutorials.
What you should do is create a controller and have it listen for events from the service. Here's an example of how to do that.
That's the nice way; You might also be able to get away with it by putting the inSync = true on the $rootScope as such;
service('syncUp', function syncUp($http, $q, app, $rootScope) {
// (...)
$rootScope.inSync = true;
It looks like you're hoping to see bindings operating between a service ('app') and a template. It's hard to tell if we're not seeing the entire picture. Going on that assumption, you need to refactor so that you are setting up bindings on a controller.
I would expect the controller setup to look something like this:
angular.module('App')
.controller('app', function app($http, $q, $scope) {
$scope.inSync = true;
});
Now you will have two-way binding hooked-up on the 'inSync' property.
Otherwise, your template looks fine.
If I'm off base, please update your question with more context, or better yet make a fiddle to boil down the problem.