Prevent ngRepeat flicker with promise in AngularJS - angularjs

I have a collection of objects, say Products, which I can interact with using $resource. On an index page, I'd like to either display the collection, or, in the case the collection is empty, display a helpful message. i.e.
In Controller
$scope.products = Products.query();
In Template
<div ng-repeat="product in products">
...
</div>
<div class="alert" ng-hide="products.length">
<p>Oops, no products!</p>
</div>
This works fine, provided the user isn't staring at the spot where the ng-repeat will occur. If they are, or if there is a delay in the response from the server, they may notice a slight flicker, before the promise is resolved.
Given that, "invoking a $resource object method immediately returns an empty reference" (see here), such a flicker will always in this example. Instead, I find myself writing:
<div class="alert" ng-hide="!products.$resolved || products.length">
<p>Oops, no products!</p>
</div>
Which takes care of the flicker. However, I'm not too keen on letting my view know exactly how the products are obtained. Especially if I change this later on. Is there anything cleaner I could do? I'm aware that a fallback for ng-repeat is in the works (see here), however, just wondering if there's a cleaner solution in the meantime.

You could use the success method to set the object:
Products.query(function(data) {
$scope.products = data;
});
Or use the promise:
Products.query().$promise.then(function(data) {
$scope.products = data;
});
This way, the object doesn't become empty until you get a response.

You can get $promise out of $resource and change displayed information before/after promise is resolved.
Say you have following Products and service to get them.
/* products */
[
{ "id":1, "name":"name1" },
{ "id":2, "name":"name2" },
...
]
/***/
app.factory('Products', function ($resource) {
return $resource('products.json');
});
Then in your controller assign data only after promise is resolved.
app.controller('MainCtrl', function ($scope, Products) {
$scope.initialize = function () {
$scope.products = null;
Products.query().$promise.then(function (data) {
$scope.products = data;
});
};
$scope.initialize();
});
In your HTML template you can take care of the cases like a) not yet resolved b) resolved c) resolved but no data
<body ng-controller="MainCtrl">
<div ng-show="!products">
Getting data... please wait
</div>
<div ng-show="products && products.length === 0">
Oh noes!1 :( No products
</div>
<div ng-show="products">
<span ng-repeat="product in products">
{{ product | json }} <br>
</span>
</div>
<div>
<button type="button" ng-click="initialize()">Refresh</button>
</div>
</body>
Related plunker here http://plnkr.co/edit/Ggzyz9

Related

Cannot bind response object from POST to my view

I've been trying to solve this for hours, and have tried to find a working solution on stack overflow and other sites, but none worked so far.
The Issue
I am building a travelogue web app that allows users to log and view their journeys (e.g. a road trip). At the moment I am implementing the feature that lets users view a particular journey in a separate view which they have selected from a list of journeys. I pass down the id of the selected journey and retrieve an Object from MongoDB. I implemented this using POST. It works in that the _id of the selected journey is passed in the request, then used to identify the document with Model.findById - then the response yields the data. The data is bound to $scope.selection.
But while $scope.selection contains the data (when logged to console), I cannot seem to bind it to the view (called view_journey). Meaning, whenever I want to access, e.g. selection.name in my view_journey.html, the expression or ng-bind is left empty.
app.js
$scope.viewJourneybyId = function(id) {
var selectOne = { _id : id };
$http.post('http://localhost:8080/view_journey', selectOne).
success(function(data) {
$scope.selection = data;
$scope.$apply();
console.log("POST found the right Journey");
console.log($scope.selection);
}).error(function(data) {
console.error("POST encountered an error");
})
}
server.js
app.post("/view_journey", function(request, response, next) {
Journeys.findById(request.body._id, function(error, selection) {
if (error)
response.send(error)
response.json({ message: 'Journey found!', selection });
});
});
index.html
<tr ng-repeat="journey in journeys">
<td>
<a href="#/view_journey" ng-click="viewJourneybyId(journey._id)">
{{journey.name}}</a>
</td>
<td>...</td>
</tr>
view_journey.html
<div class="panel panel-default">
<div class="panel-heading">
<h2 ng-bind="selection.name"></h2>
<!-- For Debugging -->
ID <span ng-bind="selection._id">
</div>
<div class="panel-body">
<table class=table>
<caption>{{selection.desc}}</caption>
...
</table>
</div>
</div>
Feedback
This is my first question on stack overflow, so please also tell me if I phrased my question in a way that could be misunderstood, and whether or not I should supply more details, e.g. console output. Thank you ;)
After fiddling with your code I can confirm that when triggering the route you are getting a new instance of the controller that has a new, clean scope. This is the expected behavior with AngularJS.
You can verify this by adding a simple log message as the first line of your controller:
console.log($scope.selected);
You will notice that it always logs out "undefined" because the variable has never been set (within viewJourneyById). If you leave that logging in and test the code you will see the logging fire in viewJourneyById but then immediately the "undefined" as it loads the view_journey.html template into ng-view and creates the new instance of mainCtrl. The presence of the "undefined" after the new view loads shows that the controller function is being executed again on the route change.
There are a couple of ways to address this. First you could create a factory or service, inject it into your controller, and have it store the data for you. That is actually one of the reasons they exist, to share data between controllers.
Factory:
travelogueApp.factory('myFactory',function() {
return {
selected: null,
journeys: []
};
});
Controller:
travelogueApp.controller('mainCtrl', ['$scope','$http','$location','myFactory', function ($scope, $http, $location, myFactory) {
// put it into the scope so the template can see it.
$scope.factory = myFactory;
// do other stuff
$scope.viewJourneybyId = function(id) {
var selectOne = { _id : id };
$http.post('http://localhost:8080/view_journey', selectOne)
.success(function(data) {
$scope.factory.selection = data;
console.log("POST found the right Journey");
console.log($scope.factory.selection);
})
.error(function(data) {
console.error("POST encountered an error");
})
}
}]); // end controller
Template:
<div class="panel panel-default">
<div class="panel-heading">
<h2>{{factory.selection.name}}</h2>
</div>
<div class="panel-body">
<table class=table>
<caption>{{factory.selection.desc}}</caption>
...
</table>
</div>
</div>
More or less something like that. Another way to do it would be to construct the link with the journey id as part of the query string and then in the controller check for the presence of the journey id and if you find one, look up the journey. This would be a case of firing the route, loading a new instance of the controller and then loading the data once you're on the view_journey page. You can search for query string parameters in the controller like this:
var journey_id = $location.search().id;
Either way works. The factory/service method allows you to minimize web service calls over time by storing some data. However, then you have to start considering data management so you don't have stale data in your app. The query string way would be your quickest way to solve the problem but means that every route transition is going to be waiting a web service call, even if you are just going back and forth between the same two pages.

what is the way to iterate the $resource.get other than doing $promise.then

Once I receive the document say $scope.var = $resource.get(/*one record*/).. I would need to read the received nested object structure (which is now in $scope.var) in order to display each and every field.
I am not able to access the key-value pairs that are present in the $scope.var. So I found a way in doing this using $scope.var.$promise.then(callback) but the format of the $scope.var is changed.
for example:
before parsing:
When I console to see what is in $scope.var, it shows as -
Resource: {$Promise: Promise, $resolved: false}
/*key-value pairs of the object*/
after parsing using $promise.then:
Here the console says
Object {_id: "...", ...}
/*key-value pairs of the object*/
Because of the above format difference I am facing the problem when trying to $update using $resource. Which says $update is not a function.
Is there any other way to access key-value pairs from the $resource.get other than using $promise.then?
Edit: Here is my original code:
Contact.service.js
'use strict';
angular.module('contacts').factory('Contact', ['$resource', function($resource) {
console.log('Iam cliked');
return $resource('/api/contacts/:id', {id: '#_id'}, {
update: {
method: 'PUT'
}
});
}]);
EditController.js
'use strict';
angular.module('contacts').controller('EditController', ['$scope', '$stateParams' ,'$location', '$rootScope', 'Contact',
function($scope, $stateParams, $location, $rootScope, Contact) {
$rootScope.PAGE = 'edit';
$scope.contact = {};
$scope.singleContact = Contact.get({ id: $stateParams.id});
console.log($scope.singleContact);
$scope.singleContact.$promise.then(function(data){
angular.forEach(data, function(value, key) {
if(key !== 'additions')
$scope.contact[key] = value;
else {
angular.forEach(data.additions, function(value, key) {
$scope.contact[key] = value;
});
}
});
console.log($scope.contact);
});
/*parsing my received nested json doument to have all straight key-value pairs and binding this $scope.contact to form-field directive 'record=contact'
I am successful doing this and able to display in web page but when I try to $update any of the field in the webpage it doesnt update. Because it is not being Resource object but it is just the object
please see the images attached[![enter image description here][1]][1]*/
$scope.delete = function(){
$scope.contact.$delete();
$location.url('/contacts');
};
}
]);
Edit.html
<section data-ng-controller='EditController'>
<h2>{{contact.firstName}} {{contact.lastName}}</h2>
<form name='editContact' class='form-horizontal'>
<form-field record='contact' recordType='contactType' field='firstName' required='true'></form-field>
<form-field record='contact' recordType='contactType' field='lastName' required='true'></form-field>
<form-field record='contact' recordType='contactType' field='{{k}}' ng-repeat=' (k, v) in contact | contactDisplayFilter: "firstName" | contactDisplayFilter: "lastName" | contactDisplayFilter: "__v" | contactDisplayFilter: "_id" | contactDisplayFilter: "userName"'></form-field>
<new-field record='contact'></new-field>
<div class='row form-group'>
<div class='col-sm-offset-2'>
<button class='btn btn-danger' ng-click='delete()'>Delete Contact</button>
</div>
</div>
</form>
form-field.html
<div class='row form-group' ng-form='{{field}}' ng-class="{'has-error': {{field}}.$dirty && {{field}}.$invalid}">
<label class='col-sm-2 control-label'>{{field | labelCase}} <span ng-if='required'>*</span></label>
<div class='col-sm-6' ng-switch='required'>
<input ng-switch-when='true' ng-model='record[field]' type='{{recordType[field]}}' class='form-control' required ng-change='update()' ng-blur='blurUpdate()' />
<div class='input-group' ng-switch-default>
<input ng-model='record[field]' type='{{recordType[field]}}' class='form-control' ng-change='update()' ng-blur='blurUpdate()' />
<span class='input-group-btn'>
<button class='btn btn-default' ng-click='remove(field)'>
<span class='glyphicon glyphicon-remove-circle'></span>
</button>
</span>
</div>
</div>
<div class='col-sm-4 has-error' ng-show='{{field}}.$dirty && {{field}}.$invalid' ng-messages='{{field}}.$error'>
<p class='control-label' ng-message='required'>{{field | labelCase}} is required.</p>
<p class='control-label' ng-repeat='(k, v) in types' ng-message='{{k}}'>{{field | labelCase}} {{v[1]}}</p>
</div>
function in form-field.js directive
$scope.blurUpdate = function(){
if($scope.live!== 'false'){
console.log($scope.record);
$scope.record.$update(function(updatedRecord){
$scope.record = updatedRecord;
});
}
};
So in the above $update gives error. saying not a function.
I want the same format in $scope.singleContact and $scope.contact. How can I achieve this?
I don't know if I understand but you want something like this:
Contact.get({id: $stateParams.id}, function (contact){
$scope.var = contact;
});
right?
It will return te promise and you will be able to use the data before loading the templates
Okay, a couple things:
Promises will not return values synchronously
You have to use a then function call. The line
$scope.singleContact = Contact.get({ id: $stateParams.id});
will not put your data into the $scope.singleContact variable immediately because that line returns a Promise and goes onto the next line without waiting for the web request to return. That's how Promises work. If you aren't familiar with them, it might not be a bad idea to read up on them. A consequence of this is that everything inside your controller (or wherever this code is) that needs a working value in $scope.singleContact needs to be inside of a then block or in some way guaranteed to be run after the promise is resolved.
So I think you want (as #EDDIE VALVERDE said):
Contact.get({ id: $stateParams.id}, function(data) {
$scope.singleContact = data;
$scope.contact = data;
});
I'm not sure what the rest of the code is doing but it looks to me like it is trying to do a deep copy of $scope.singleContact. My guess is that you're just doing this cause the promise stuff wasn't working and you can remove it...
Let me know if I missed something.
Ah, now I think I understand. Okay, for 1 I would not add $resource objects to your scope. IMO the scope is intended to be a Model in the MVC paradigm. A $resource is kindof like a model, but it's performing network calls and it's got a bunch of stuff other than data. If you put $resource in your model you remove any clear layer where you can manipulate and transform the data as it comes back from the server. And I'm sure there are other reasons. So I would not add $resource objects like Contact directly to the scope. That being said, I think you want to add a new update function, like you did with delete, only something like this:
$scope.update= function(args){
//make network call and update contact
Contact.$update({/*data from args?*/});
//get the data i just saved
Contact.$get({/* id from args */}, function(result) {
$scope.contact = result;
});
//other stuff?
};
That's my first thought, anyway...

nested sortable angular repeat not populating with $http

I'm an angular newby, and I'm trying to produce a sortable array within another sortable array using ng-sortable. When I get the data directly from a javascript file it works fine, but when I use $http to retrieve the same data, only the non-repeating data displays - I don't get any error, but the repeaters don't display at all.
Controller with test data (which works):
angular.module('demoApp').controller('TestController', function ($scope, TestService, TestDataFactory) {
$scope.testData = TestService.getTestData(TestDataFactory.Data);
});
Controller with $http:
angular.module('demoApp').controller('TestController', function ($scope, $http, TestService) {
$http.get('test/Index')
.success(function (data) {
$scope.testData = TestService.getTestData(data);
})
.error(function (data) {
alert("Error getting test data");
});
});
View:
<h1 class="dataname">{{data.name}}</h1>
<div ng-model="testData" id="test" ng-controller="TestController">
<div as-sortable="sectionSortOptions" ng-model="testData.sections">
<div class="section" ng-repeat="section in testData.sections" as-sortable-item ng-include="'test/Section'">
</div>
</div>
</div>
Test/Index returns a string of the same format as TestDataFactory.Data, and the object returned by TestService.getTestData() (which is just a data formatter) in each case is identical.
Am I missing something?
Edit:
Seems my problem is to do with the fact that my ng-include has another ng-sortable within it - here's a flattened view of the whole thing:
<h1 class="dataName">{{testData.name}}</h1>
<div as-sortable="sectionSortOptions" ng-model="testData.sections">
<div class="section" ng-repeat="section in testData.sections" as-sortable-item>
<span class="section-sortOrder" as-sortable-item-handle>Section {{section.sortOrder}}</span>
<div as-sortable="itemSortOptions" ng-model="section.items">
<div class="item" ng-repeat="item in section.items" as-sortable-item>
<div as-sortable-item-handle>
<span class="itemName">{{item.name}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
If I comment out the the line:
<div as-sortable="sectionSortOptions" ng-model="documentPack.sections">
And the associated as-sortable-item-handle (plus the closing div tag) then I get the sortable items in the sections (but not, obviously, the section-level sortability).
Plunker here: http://plnkr.co/edit/CNLVmlGjPvkcFKRo7SjN?p=preview - uses $timeout to simulate $http ($timeout caused the same problem) but now working as it uses latest ng-sortable (see answer).
I think this is your problem:
.success(function (data) {
$scope.testData = TestService.getTestData(data);
})
It should be
.success(function(data){
$scope.testData = data;
})
The success function is called when the promise is resolved, so if you do
$scope.testData = TestService.getTestData(data);
You are probably doing another remote call just as the first one finishes, and your page will never load anything.
Also, I suggest you read the documentation for ngResource as it will probably be useful in whatever you're doing.
It's not a good practice to use $http calls in your controllers, as you're coupling them with your remote calls, server addresses, and lots of other stuff that should be in another configuration files.
OK, seems the problem was with the version of ng-sortable I had - version number was labeled 1.2.2 (same as current version), but it didn't have the latest fixes (since 15th June - so, all of 15 days ago). Not sure how unlucky I am to have hit the (probably tiny) window when this problem was there, but I'll post this as answer just in case someone else hits similar behaviour with an early-June-2015 version.

How to call $scope.$apply() using "controller as" syntax

I am trying to limit my use of $scope in my controllers as much as possible and replace it with the Controller as syntax.
My current problem is that i'm not sure how to call $scope.$apply() in my controller without using $scope.
Edit: I am using TypeScript 1.4 in conjunction with angular
I have this function
setWordLists() {
this.fetchInProgress = true;
var campaignId = this.campaignFactory.currentId();
var videoId = this.videoFactory.currentId();
if (!campaignId || !videoId) {
return;
}
this.wordsToTrackFactory.doGetWordsToTrackModel(campaignId, videoId)
.then((response) => {
this.fetchInProgress = false;
this.wordList = (response) ? response.data.WordList : [];
this.notUsedWordList = (response) ? response.data.NotUsedWords : [];
});
}
being called from
$scope.$on("video-switch",() => {
this.setWordLists();
});
And it's (the arrays wordList and notUsedWordList)
is not being updated in my view:
<div class="wordListWellWrapper row" ng-repeat="words in wordTrack.wordList">
<div class="col-md-5 wordListWell form-control" ng-class="(words.IsPositive)? 'posWordWell': 'negWordWell' ">
<strong class="wordListWord">{{words.Word}}</strong>
<div class="wordListIcon">
<div class="whiteFaceIcon" ng-class="(words.IsPositive)? 'happyWhiteIcon': 'sadWhiteIcon' "></div>
</div>
</div>
<div class="col-md-2">
<span aria-hidden="true" class="glyphicon-remove glyphicon" ng-click="wordTrack.removeWord(words.Word)"></span>
</div>
</div>
Along the same lines of $apply, is there another way of calling $scope.$on using Controller as?
Thanks!
To answer the question at hand here, you can use $scope() methods in a controller when using the controller-as syntax, as long as you pass $scope as a parameter to the function. However, one of the main benefits of using the controller-as syntax is not using $scope, which creates a quandary.
As was discussed in the comments, a new question will be formulated by the poster to review the specific code requiring $scope in the first place, with some recommendations for re-structuring if possible.

AngularJS - Using a Service as a Model, ng-repeat not updating

I'm creating an ajax search page which will consist of a search input box, series of filter drop-downs and then a UL where the results are displayed.
As the filters part of the search will be in a separate place on the page, I thought it would be a good idea to create a Service which deals with coordinating the inputs and the ajax requests to a search server-side. This can then be called by a couple of separate Controllers (one for searchbox and results, and one for filters).
The main thing I'm struggling with is getting results to refresh when the ajax is called. If I put the ajax directly in the SearchCtrl Controller, it works fine, but when I move the ajax out to a Service it stops updating the results when the find method on the Service is called.
I'm sure it's something simple I've missed, but I can't seem to see it.
Markup:
<div ng-app="jobs">
<div data-ng-controller="SearchCtrl">
<div class="search">
<h2>Search</h2>
<div class="box"><input type="text" id="search" maxlength="75" data-ng-model="search_term" data-ng-change="doSearch()" placeholder="Type to start your search..." /></div>
</div>
<div class="search-summary">
<p><span class="field">You searched for:</span> {{ search_term }}</p>
</div>
<div class="results">
<ul>
<li data-ng-repeat="item in searchService.results">
<h3>{{ item.title }}</h3>
</li>
</ul>
</div>
</div>
</div>
AngularJS:
var app = angular.module('jobs', []);
app.factory('searchService', function($http) {
var results = [];
function find(term) {
$http.get('/json/search').success(function(data) {
results = data.results;
});
}
//public API
return {
results: results,
find: find
};
});
app.controller("SearchCtrl", ['$scope', '$http', 'searchService', function($scope, $http, searchService) {
$scope.search_term = '';
$scope.searchService = searchService;
$scope.doSearch = function(){
$scope.searchService.find($scope.search_term);
};
$scope.searchService.find();
}]);
Here is a rough JSFiddle, I've commented out the ajax and I'm just updating the results variable manually as an example. For brevity I've not included the filter drop-downs.
http://jsfiddle.net/XTQSu/1/
I'm very new to AngularJS, so if I'm going about it in totally the wrong way, please tell me so :)
In your HTML, you need to reference a property defined on your controller's $scope. One way to do that is to bind $scope.searchService.results to searchService.results once in your controller:
$scope.searchService.results = searchService.results;
Now this line will work:
<li data-ng-repeat="item in searchService.results">
In your service, use angular.copy() rather than assigning a new array reference to results, otherwise your controller's $scope will lose its data-binding:
var new_results = [{ 'title': 'title 3' },
{ 'title': 'title 4' }];
angular.copy(new_results, results);
Fiddle. In the fiddle, I commented out the initial call to find(), so you can see an update happen when you type something into the search box.
The problem is that you're never updating your results within your scope. There are many approaches to do this, but based on your current code, you could first modify your find function to return the results:
function find(term) {
$http.get('/json/search').success(function(data) {
var results = data.results;
});
//also notice that you're not using the variable 'term'
//to perform a query in your webservice
return results;
}
You're using a module pattern in your 'public API' so your searchService returns the find function and an array of results, but you'd want to make the function find to be the responsible for actually returning the results.
Setting that aside, whenever you call doSearch() in your scope, you'd want to update the current results for those returned by your searchService
$scope.doSearch = function(){
$scope.searchService.results = searchService.find($scope.search_term);
};
I updated your jsfiddle with my ideas, is not functional but i added some commments and logs to help you debug this issue. http://jsfiddle.net/XTQSu/3/

Resources