I am creating an Ionic application that is pulling articles from a joomla K2 website. I am using $http and just ending my url off with '?format=json' and that is working perfectly. However the website I am pulling data from updates its articles every few minutes so I need a way for the user to be able to refresh the page. I have implemented Ionics pull to refresh and it is working swell except for the fact that instead of just pulling in new articles it just appends all the articles to my array. Is there anyway to just maybe iterate over current articles timestamps or IDs (I am caching articles in localStorage) to just bring in new articles? My factory looks like this:
.factory('Articles', function ($http) {
var articles = [];
storageKey = "articles";
function _getCache() {
var cache = localStorage.getItem(storageKey );
if (cache)
articles = angular.fromJson(cache);
}
return {
all: function () {
return $http.get("http://jsonp.afeld.me/?url=http://mexamplesite.com/index.php?format=json").then(function (response) {
articles = response.data.items;
console.log(response.data.items);
return articles;
});
},
getNew: function () {
return $http.get("http://jsonp.afeld.me/?url=http://mexamplesite.com/index.php?format=json").then(function (response) {
articles = response.data.items;
return articles;
});
},
get: function (articleId) {
if (!articles.length)
_getCache();
for (var i = 0; i < articles.length; i++) {
if (parseInt(articles[i].id) === parseInt(articleId)) {
return articles[i];
}
}
return null;
}
}
});
and my controller:
.controller('GautengCtrl', function ($scope, $stateParams, $timeout, Articles) {
$scope.articles = [];
Articles.all().then(function(data){
$scope.articles = data;
window.localStorage.setItem("articles", JSON.stringify(data));
},
function(err) {
if(window.localStorage.getItem("articles") !== undefined) {
$scope.articles = JSON.parse(window.localStorage.getItem("articles"));
}
}
);
$scope.doRefresh = function() {
Articles.getNew().then(function(articles){
$scope.articles = articles.concat($scope.articles);
$scope.$broadcast('scroll.refreshComplete');
});
};
})
Use underscore.js to simple filtering functionality.
For example:
get all id's of already loaded items (I believe there is some unique fields like id)
http://underscorejs.org/#pluck
var loadedIds = _.pluck($scope.articles, 'id');
Reject all items, if item.id is already in loadedIds list.
http://underscorejs.org/#reject
http://underscorejs.org/#contains
var newItems = _.reject(articles, function(item){
return _.contains(loadedIds, item.id);
});
Join new items and existings:
$scope.articles = newItems.concat($scope.articles);
or
http://underscorejs.org/#union
$scope.articles = _.union(newItems, $scope.articles);
Actually _.union() can manage and remove duplicates, but I would go for manual filtering using item.id.
Related
First of all, let me mention that I am new to AngularJS, and programming aswell.
My situation is as follows:
I am dealing with over 50k entries that I pull from a SQL database.
I have to show those entries on a web platform after a search/filter is applied on those entries.
So I did some research on this and came to the conclussion that the way to go is:
SQL>PHP>JSON>ANGULARJS.
I got all this down , the thing is that ng-repeat sends everything to the browser and what I want is to filter the results and THEN print them with ng-repeat.
I have tried to implement a filter of sorts but I can't seem to solve it.
Code looks like this:
js:
var app = angular.module('myApp' ,[]);
app.controller('MainCtrl', function($scope, $http)
{
$http.get("http:source.php")
.then(function(response)
{
$scope.materiale = response.data.records;
});
});
app.filter('filtruCautare', function () {
return function (items, filtru) {
var filtered = [];
var filtruMatch = new RegExp(filtru, 'i');
for (var i = 0; i < items.length; i++) {
var item = items[i];
if (filtruMatch.test(item.MATKL) || filtruMatch.test(item.MAKTX)) {
filtered.push(item);
} else if (filtruMatch.test(item.MATNR)) {
filtered.push(item);
}
}
return filtered;
};
});
html:
<input ng-model='filtru' type="text" placeholder="...">
<tbody ng-repeat="material in materiale | filtruCautare:filtru">
I've put this together from several blogs, Stackoverflow posts/questions and other several sources...
I think that I need to somehow filter the 50k entries, store them inside a scope and use the ng-repeat to print the results from there, it's just that I'm having trouble creating a scope that pulls info from the filter.
Any help is appreciated!
Simply change your filter in order to return an empty array if the search filter is not defined (or is an empty string).
In this way you will start to see results only when you start filtering:
app.filter('filtruCautare', function () {
return function (items, filtru) {
if (!filtru) {
return [];
}
var filtered = [];
var filtruMatch = new RegExp(filtru, 'i');
for (var i = 0; i < items.length; i++) {
var item = items[i];
if (filtruMatch.test(item.MATKL) || filtruMatch.test(item.MAKTX)) {
filtered.push(item);
} else if (filtruMatch.test(item.MATNR)) {
filtered.push(item);
}
}
return filtered;
};
});
I've been learning AngularJS for a while now and am finally getting my head around how it works after being a back-end developer for years.
However, I'm having an enormous amount of trouble understanding how unit testing works with Karma + Jasmine.
Every article I read either stops at testing a controller $scope variable for a value or dives so far into the deep end I get lost in the first paragraph.
I'm hoping someone can write a demo test for this controller so I can get my head around how to test controller functions with private variables etc.
financeApp.controller('navController', ['$scope', '$resource', '$cookies', '$location', function ($scope, $resource, $cookies, $location) {
// Set default values
$scope.resultList = [];
$scope.cookieExp = moment().add(3, 'months').toDate();
$scope.dataLoaded = true;
$scope.codesList = [];
// Update watchlist item stock prices
$scope.updateWatchItem = function (items) {
sqlstring = items.join("\",\"");
var financeAPI = $resource('https://query.yahooapis.com/v1/public/yql', {callback: "JSON_CALLBACK" }, {get: {method: "JSONP"}});
financeAPI.get({q: decodeURIComponent('select%20*%20from%20yahoo.finance.quote%20where%20symbol%20in%20(%22' + sqlstring + '%22)'),
format: 'json', env: decodeURIComponent('store%3A%2F%2Fdatatables.org%2Falltableswithkeys')})
.$promise.then(function (response) {
var quotes = response.query.results.quote;
quotes = Array.isArray(quotes) ? quotes : [quotes];
quotes.forEach(function (quote) {
$scope.createWatchItem(quote);
});
}, function (error) {
alert("ERROR: There was an issue accessing the finance API service.");
});
};
// Add a new watchlist item (triggered on button click)
$scope.newWatchItem = function () {
var newcode = $scope.asxcodeinput;
if (newcode == null) {
alert('Please enter a valid ASX equities code...');
return;
}
else if ($scope.codesList.indexOf(newcode + '.AX') > -1) {
alert('You are already tracking ' + newcode.toUpperCase() + '!');
return;
}
$scope.dataLoaded = false;
var financeAPI = $resource('https://query.yahooapis.com/v1/public/yql', {callback: "JSON_CALLBACK" }, {get: {method: "JSONP"}});
financeAPI.get({q: decodeURIComponent('select%20*%20from%20yahoo.finance.quote%20where%20symbol%20in%20(%22' + newcode + '.AX%22)'),
format: 'json', env: decodeURIComponent('store%3A%2F%2Fdatatables.org%2Falltableswithkeys')})
.$promise.then(function (response) {
$scope.dataLoaded = true;
var quote = response.query.results.quote;
if(quote.StockExchange != null) {
$scope.createWatchItem(quote);
$cookies.putObject('codesCookie', $scope.codesList, {expires: $scope.cookieExp});
$location.path('/' + (quote.Symbol).split('.')[0].toUpperCase());
}
else {
alert("Woops! Looks like that stock doesn't exist :(");
}
}, function (error) {
alert("ERROR: There was an issue accessing the finance API service.");
});
$scope.asxcodeinput = "";
};
// Delete a watchlist item (triggered on delete icon click)
$scope.deleteWatchlistItem = function (asxcode) {
$scope.resultList.forEach(function (result, key) {
if(result.Symbol == asxcode) {
$scope.resultList.splice(key, 1);
}
});
$scope.codesList.forEach(function (code, key) {
if(code == asxcode) {
$scope.codesList.splice(key, 1);
}
});
$cookies.putObject('codesCookie', $scope.codesList, {expires: $scope.cookieExp});
$location.path('/');
};
// Add new watchlist item to lists of watched items
$scope.createWatchItem = function (quote) {
$scope.resultList.push(quote);
$scope.codesList.push(quote.Symbol);
};
// Get current page for navigation menu CSS
$scope.isActive = function (location) {
return location === $location.path();
};
// If the cookie is set and not empty, populate the watchlist items with the cookie contents
if($cookies.getObject('codesCookie') && $cookies.getObject('codesCookie').length > 0) {
$scope.updateWatchItem($cookies.getObject('codesCookie'));
}
}]);
Also, if anyone can recommend an easy to read article on unit testing in AngularJS I'd appreciate it.
That is a big lump to start testing with. I suggest looking at the tutorial page REST and Custom Services on the angular site and put the resource stuff in a service.
I suggest viewing some good videos on jasmine at https://www.youtube.com/channel/UC4Avh_hoUNIJ0WL2XpcLkog
I do recommend you view up to and including the one on spies.
So I have a small angular app that takes in a search query, sends it to an elasticsearch node I've got set up, and then displays the result set on screen.
My problem is that when I make a new query, the results gets appended to the end of the current result set. What I would like it to do is to erase whatever is currently on the page, and reload it with only the new data, much like how searching for something on Google returns a completely new set of results.
Is there any way to do this? Code below for reference.
// this is the controller that displays the reuslts.
var displayController = function($scope, $rootScope, $window, notifyingService) {
var dataReady = function(event, data) {
$scope.resultSet = notifyingService.getData();
}
$rootScope.$on('data-ready', dataReady)
}
app.controller("displayController", ["$scope", "$rootScope", "$window", "notifyingService", displayController]);
// this is the service that's responsible for setting the data
var notifyingService = function($http, $rootScope) {
var svc = {
_data: [],
setData: setData,
getData: getData
};
function getData() {
return svc._data;
}
function setData(data) {
var base_obj = data.hits.hits
console.log("Setting data to passed in data.");
console.log('length of dataset: ' + base_obj.length);
for(var i = 0; i < base_obj.length; i++){
svc._data.push(base_obj[i]._source);
}
$rootScope.$broadcast('data-ready', svc._data);
}
return svc;
};
app.factory("notifyingService", ["$http", "$rootScope", notifyingService]);
In setData just before the loop re-initialize svc._data
svc._data = [];
Clear you svc._data before you start adding the new query.
function setData(data) {
var base_obj = data.hits.hits;
svc._data = [];//reset your array before you populate it again.
for(var i = 0; i < base_obj.length; i++){
svc._data.push(base_obj[i]._source);
}
$rootScope.$broadcast('data-ready', svc._data);
}
Solved: The below code now works for anyone who needs it.
Create an Angular factory that queries a database and returns query results.
Pass those results of the query into the $scope of my controller
Render the results of the $scope variable in my view (html)
Challenge:
Because this is for a node-webkit (nwjs) app, I am trying to do this without using express, setting up api endpoints, and using the $http service to return the query results. I feel like there should be a more eloquent way to do this by directly passing the data from nodejs to an angular controller. Below is what I've attempted but it hasn't worked.
Database: Below code is for a nedb database.
My Updated Controllers
app.controller('homeCtrl', function($scope,homeFactory){
homeFactory.getTitles().then(function(data){
$scope.movieTitles = data;
});
});
My Updated Factory:
app.factory('homeFactory', function($http,$q) {
return {
getTitles: function () {
var deferred = $q.defer();
var allTitles = [];
db.find({}, function (err, data) {
for (var i = 0, len = data.length; i < len; i++) {
allTitles.push(data[i].title)
}
deferred.resolve(allTitles);
});
return deferred.promise;
}
}
});
HTML
<script>
var Datastore = require('nedb');
var db = new Datastore({ filename: './model/movies.db', autoload: true });
</script>
<--shows up as an empty array-->
<p ng-repeat="movie in movieTitles">{{movie}}</p>
So the problem with your original homeFactory is that db.find happens asynchronously. Although you can use it just fine with callbacks, I think it is better practice to wrap these with $q.
app.service('db', function ($q) {
// ...
this.find = function (query) {
var deferred = $q.defer();
db.find(query, function (err, docs) {
if (err) {
deferred.reject(err);
} else {
deferred.resolve(docs);
}
});
return deferred.promise;
};
// ...
});
Once you begin wrapping non-Angular code like this, then it makes the interface consistent.
app.service('homeFactory', function (db) {
this.getAllTitles = function () {
return db.find({}).then(function (docs) {
return docs.map(function (doc) {
return doc.title;
});
});
};
});
And the controller would look like:
app.controller('homeCtrl', function ($scope, homeFactory) {
homeFactory.getAllTitles().then(function (titles) {
$scope.movieTitles = titles;
});
});
Partial Answer: I was able to get it working by moving all the code into the controller. Anyone now how to refactor the below to use a factory to clean up the code?
Updated Controller ($scope.movietitles is updating in views with the correct data):
$scope.movieTitles = [];
$scope.getMovieTitles = db.find({},function(err,data){
var arr = [];
for (var i = 0, len = data.length; i < len; i++){
arr.push(data[i].title)
}
$scope.movieTitles = arr;
$scope.$apply();
});
What I have is simple CRUD operation. Items are listed on page, when user clicks button add, modal pops up, user enters data, and data is saved and should automatically (without refresh)be added to the list on page.
Service:
getAllIncluding: function(controllerAction, including) {
var query = breeze.EntityQuery.from(controllerAction).expand(including);
return manager.executeQuery(query).fail(getFailed);
},
addExerciseAndCategories: function(data, initialValues) {
var addedExercise = manager.createEntity("Exercise", initialValues);
_.forEach(data, function(item) {
manager.createEntity("ExerciseAndCategory", { ExerciseId: addedExercise._backingStore.ExerciseId, CategoryId: item.CategoryId });
});
saveChanges().fail(addFailed);
function addFailed() {
removeItem(items, item);
}
},
Controller:
$scope.getAllExercisesAndCategories = function() {
adminCrudService.getAllIncluding("ExercisesAndCategories", "Exercise,ExerciseCategory")
.then(querySucceeded)
.fail(queryFailed);
};
function querySucceeded(data) {
$scope.queryItems = adminCrudService.querySucceeded(data);
var exerciseIds = _($scope.queryItems).pluck('ExerciseId').uniq().valueOf();
$scope.exerciseAndCategories = [];
var createItem = function (id, exercise) {
return {
ExerciseId: id,
Exercise : exercise,
ExerciseCategories: []
};
};
// cycle through ids
_.forEach(exerciseIds, function (id) {
// get all the queryItems that match
var temp = _.where($scope.queryItems, {
'ExerciseId': id
});
// go to the next if nothing was found.
if (!temp.length) return;
// create a new (clean) item
var newItem = createItem(temp[0].ExerciseId, temp[0].Exercise);
// loop through the queryItems that matched
_.forEach(temp, function (i) {
// if the category has not been added , add it.
if (_.indexOf(newItem.ExerciseCategories, i.ExerciseCategory) < 0) {
newItem.ExerciseCategories.push(i.ExerciseCategory);
}
});
// Add the item to the collection
$scope.items.push(newItem);
});
$scope.$apply();
}
Here is how I add new data from controller:
adminCrudService.addExerciseAndCategories($scope.selectedCategories, { Name: $scope.NewName, Description: $scope.NewDesc });
So my question is, why list isn't updated in real time (when I hit save I must refresh page).
EDIT
Here is my querySuceeded
querySucceeded: function (data) {
items = [];
data.results.forEach(function(item) {
items.push(item);
});
return items;
}
EDIT 2
I believe I've narrowed my problem !
So PW Kad lost two hours with me trying to help me to fix this thing (ad I thank him very very very much for that), but unfortunately with no success. We mostly tried to fix my service, so when I returned to my PC, I've again tried to fix it. I believe my service is fine. (I've made some changes as Kad suggested in his answer).
I believe problem is in controller, I've logged $scope.items, and when I add new item they don't change, after that I've logged $scope.queryItems, and I've noticed that they change after adding new item (without refresh ofc.). So probably problem will be solved by somehow $watching $scope.queryItems after loading initial data, but at the moment I'm not quite sure how to do this.
Alright, I am going to post an answer that should guide you on how to tackle your issue. The issue does not appear to be with Breeze, nor with Angular, but the manner in which you have married the two up. I say this because it is important to understand what you are doing in order to understand the debug process.
Creating an entity adds it to the cache with an entityState of isAdded - that is a true statement, don't think otherwise.
Now for your code...
You don't have to chain your query execution with a promise, but in your case you are returning the data to your controller, and then passing it right back into some function in your service, which wasn't listed in your question. I added a function to replicate what yours probably looks like.
getAllIncluding: function(controllerAction, including) {
var query = breeze.EntityQuery.from(controllerAction).expand(including);
return manager.executeQuery(query).then(querySucceeded).fail(getFailed);
function querySucceeded(data) {
return data.results;
}
},
Now in your controller simply handle the results -
$scope.getAllExercisesAndCategories = function() {
adminCrudService.getAllIncluding("ExercisesAndCategories", "Exercise,ExerciseCategory")
.then(querySucceeded)
.fail(queryFailed);
};
function querySucceeded(data) {
// Set your object directly to the data.results, because that is what we are returning from the service
$scope.queryItems = data;
$scope.exerciseAndCategories = [];
Last, let's add the properties we create the entity and see if that gives Angular a chance to bind up properly -
_.forEach(data, function(item) {
var e = manager.createEntity("ExerciseAndCategory");
e.Exercise = addedExercise; e.Category: item.Category;
});
So I've managed to solve my problem ! Not sure if this is right solution but it works now.
I've moved everything to my service, which now looks like this:
function addCategoriesToExercise(tempdata) {
var dataToReturn = [];
var exerciseIds = _(tempdata).pluck('ExerciseId').uniq().valueOf();
var createItem = function (id, exercise) {
return {
ExerciseId: id,
Exercise: exercise,
ExerciseCategories: []
};
};
// cycle through ids
_.forEach(exerciseIds, function (id) {
// get all the queryItems that match
var temp = _.where(tempdata, {
'ExerciseId': id
});
// go to the next if nothing was found.
if (!temp.length) return;
// create a new (clean) item
var newItem = createItem(temp[0].ExerciseId, temp[0].Exercise);
// loop through the queryItems that matched
_.forEach(temp, function (i) {
// if the category has not been added , add it.
if (_.indexOf(newItem.ExerciseCategories, i.ExerciseCategory) < 0) {
newItem.ExerciseCategories.push(i.ExerciseCategory);
}
});
// Add the item to the collection
dataToReturn.push(newItem);
});
return dataToReturn;
}
addExerciseAndCategories: function (data, initialValues) {
newItems = [];
var addedExercise = manager.createEntity("Exercise", initialValues);
_.forEach(data, function (item) {
var entity = manager.createEntity("ExerciseAndCategory", { ExerciseId: addedExercise._backingStore.ExerciseId, CategoryId: item.CategoryId });
items.push(entity);
newItems.push(entity);
});
saveChanges().fail(addFailed);
var itemsToAdd = addCategoriesToExercise(newItems);
_.forEach(itemsToAdd, function (item) {
exerciseAndCategories.push(item);
});
function addFailed() {
removeItem(items, item);
}
}
getAllExercisesAndCategories: function () {
var query = breeze.EntityQuery.from("ExercisesAndCategories").expand("Exercise,ExerciseCategory");
return manager.executeQuery(query).then(getSuceeded).fail(getFailed);
},
function getSuceeded(data) {
items = [];
data.results.forEach(function (item) {
items.push(item);
});
exerciseAndCategories = addCategoriesToExercise(items);
return exerciseAndCategories;
}
And in controller I have only this:
$scope.getAllExercisesAndCategories = function () {
adminExerciseService.getAllExercisesAndCategories()
.then(querySucceeded)
.fail(queryFailed);
};
function querySucceeded(data) {
$scope.items = data;
$scope.$apply();
}