Chained promises and prototype `this` - javascript

I'm having a hard time to get promises to work with the right this scope inside prototypes.
Here is my code:
'use strict';
angular.module('testApp').factory('UrlSearchApi',
function($resource, URL_SEARCH_API, PAGE_SIZE, $q){
var resource = $resource(URL_SEARCH_API);
resource.Scroll = function () {
return this.reset();
};
resource.Scroll.prototype.reset = function () {
this.visibleItems = [];
this.allItems = [];
this.busy = null;
return this;
};
resource.Scroll.prototype.fetch = function(query){
var params = {};
if(query) { params.q = query; }
return resource.query(params).$promise;
};
resource.Scroll.prototype.loadAllItems = function (results) {
var d = $q.defer();
angular.forEach(results, function (result, i) {
this.allItems.push(result);
if(i === results.length - 1 ) { d.resolve(); }
}, this);
return d.promise;
};
resource.Scroll.prototype.loadVisibleItems = function () {
var length = this.visibleItems.length,
offset = parseInt(length / PAGE_SIZE),
start = PAGE_SIZE * offset,
end = start + PAGE_SIZE,
subset = this.allItems.slice(start, end),
d = $q.defer();
angular.forEach(subset, function (item, i) {
this.visibleItems.push(item);
if(i === subset.length - 1 ) { d.resolve(); }
}, this);
return d.promise;
};
resource.Scroll.prototype.nextPage = function (query) {
if(this.busy) { return; }
console.log('nextPage ', query);
var tasks = [],
that = this;
if(!this.allItems.length) {
this.reset();
this.busy = true;
return this.fetch(query)
.then(this.loadAllItems)
.then(this.loadVisibleItems)
.finally(function () {
this.busy = false;
});
} else {
this.busy = true;
return this.loadVisibleItems().finally(function () {
this.busy = false;
});
}
};
return resource;
});
Whenever I run the tests I get
describe('#nextPage', function () {
var scroll;
describe('when there is NO search term (show all)', function () {
beforeEach(function (done) {
scroll = new UrlSearchApi.Scroll();
$httpBackend.expectGET('/policy/search')
.respond(200, arrayGenerator(123));
scroll.nextPage().then(done);
$httpBackend.flush();
$rootScope.$apply();
});
it('should load all the items in all items variable', function () {
expect(scroll.allItems.length).toBe(123);
});
});
});
I get the following error:
TypeError: 'undefined' is not an object (evaluating 'this.allItems')
I now that $q in strict mode sets the this inside then to undefined. I tried using bind(this) in multiple places but not luck... Any ideas?

I've already answered a question like this here.
Just let me know in comments if you still have questions.
Upd. Try to update your resource.Scroll.prototype.nextPage method like this:
if(!this.allItems.length) {
this.reset();
this.busy = true;
return this.fetch(query)
.then(this.loadAllItems.bind(this)) //bind here
.then(this.loadVisibleItems.bind(this)) // here
.finally(function () {
this.busy = false;
}.bind(this)); //and here
But keep in mind - when you pass a function as a callback to a then or to forEach e.t.c it'll lose this context. So, use bind exactly when you pass the function which uses this syntax as a callback.

Related

How to disable method call if instance of object doesn't exist (javascript)

Is there a way to not call a method from the function object (instance of it) if that instance doesn't exist?
This is my function for sticky sidebar and it has 2 methods;
init() and updateSticky();
function stickySideBar(element, options = {}) {
var _this = this;
_this.init = function () {}
_this.updateSticky = function (timeout) {
//this is actually just a debouncer that calls init method
}
}
I want to use this updateSticky on window resize
$(window).on("resize", function () {
newsletterBlog.updateSticky();
sideBarBlog.updateSticky();
if ($(productsSticky.element).length > 0) {
productsSticky.updateSticky();
}
});
now I use if loop to check if an element exists but I would like to do that inside of the instance of that function
if i dont have if loop i get "Uncaught TypeError: e.updateSticky is not a function".
cheers
EDIT
here is the function
function stickySideBar(element, options = {}) {
var _this = this;
console.log("_this :>> ", _this);
//declared element
_this.debouncer;
_this.element = element;
if ($(_this.element).length === 0) {
return;
}
// declared options
_this.parentElementClass = options.parentElementClass;
_this.wrapClass = options.wrapClass;
_this.activeStickyClass = options.activeStickyClass;
_this.top = options.top;
_this.width = options.width;
_this.activeBottomClass = options.activeBottomClass;
_this.disableOnMobile = options.disableOnMobile ? options.disableOnMobile : true;
_this.breakpoint = 992;
_this.init = function () {
};
_this.updateSticky = function (timeout) {
if ($(_this.element).length === 0) return;
var timeoutVal = timeout ? timeout : 100;
clearTimeout(_this.debouncer);
_this.debouncer = setTimeout(function () {
_this.init();
}, timeoutVal);
};
return _this.init();
}

setTimeout and clearTimeout interruption

I'm trying to make like a drop alert queue.
So far i've achieved it. The problem comes when I try to implement interruptions.
Let's say that an offline event fires one alert, and before it end's it's timeout, online event fires. I want it to kill offline alert, and show online alert instead.
The thing is that timeout's aren't working as I expect them to, in that case.
Here's the Angular service that handles the alerts:
.factory('AlertDrop', ['$rootScope', '$q', function ($rootScope, $q) {
var timeout = 2000;
$rootScope.DropAlerts = [];
var hideonclick = false;
var listenerOn = false;
var t = this;
this.currentAlert = {};
var getColor = function (clase) {
switch (clase) {
case 'error':
return 'rgba(201,48,44,0.9)';
break;
case 'warning':
return 'rgba(240,173,78,0.9)'
break;
case 'success':
return 'rgba(68,157,68,0.9)'
break;
case 'info':
return 'rgba(49, 176, 213,0.9)'
break;
default:
console.log('Código de color incorrecto. Cargo color por defecto.');
return 'rgba(152,26,77,0.9)';
break;
}
};
this.requestTypes = [
{
type: 'offline',
class: 'error'
},
{
type: 'online',
class: 'success'
}
];
this.bgcolor = 'rgba(201,48,44,0.9)';
this.showAlert = function (params) {
this.currentAlert = params;
delete t.ctout;
t.ctout = {};
var deferred = $q.defer();
if (params.timeout) {
this.timeout = params.timeout
}
if (params.hideonclick) {
hideonclick = params.hideonclick;
}
if (params.class != '')
this.bgcolor = getColor(params.class);
if (params.class != '') {
$('.alertDrop').attr('style', 'background: ' + this.bgcolor + '; top:44px;');
} else {
$('.alertDrop').attr('style', 'top:44px;');
}
if (hideonclick) {
addListener(this.bgcolor);
}
$('.alertDrop').html(params.message);
t.ctout = setTimeout(function () {
timeoutDone();
}, this.timeout);
};
this.hideAlert = function (color) {
$('.alertDrop').css('top', '0');
rmListener();
};
var rmListener = function () {
if (listenerOn) {
$('.alertDrop').unbind('click');
}
};
var addListener = function (color) {
if (!listenerOn) {
// $('.alertDrop').on('click', t.hideAlert(color));
}
};
var timeoutDone = function(){
var deferred = $q.defer();
t.hideAlert(this.bgcolor);
var tt = setTimeout(function(){
t.changeStatus(false);
deferred.resolve(true);
},2000); // 2 segundos, tiempo establecido en style.css
return deferred.promise;
};
var removeOnlines = function(){
$rootScope.DropAlerts = $rootScope.DropAlerts.filter(function(el){
return el.type != 'online';
});
};
var removeOfflines = function(){
$rootScope.DropAlerts = $rootScope.DropAlerts.filter(function(el){
return el.type != 'offline'
});
};
var interrupcion = function(params){
var load = true;
if(params.type == 'offline'){
removeOnlines(params);
load = false;
}else if(params.type == 'online'){
removeOfflines(params);
load = false;
}
if(load){
$rootScope.DropAlerts.push(params);
}else{
clearTimeout(t.ctout);
timeoutDone().then(function(){
t.showAlert(params);
});
}
};
this.checkPush = function(params){
var deferred = $q.defer();
if( $rootScope.AlertDropActive ){
if( params.type == this.currentAlert.type ){
clearTimeout(this.ctout);
setTimeout(function(){
timeoutDone();
},this.timeout)
}else{
interrupcion(params);
}
deferred.resolve(true);
}else{
$rootScope.DropAlerts.push(params);
deferred.resolve(true);
}
return deferred.promise;
};
this.pushAlert = function (params) {
this.checkPush(params);
if (!$rootScope.AlertDropActive) {
this.changeStatus(true);
var alert = $rootScope.DropAlerts.shift();
this.showAlert(alert);
}
};
this.changeStatus = function(v){
$rootScope.AlertDropActive = v;
if (!$rootScope.AlertDropActive && ( $rootScope.DropAlerts.length > 0 )) {
this.changeStatus(true);
var alert = $rootScope.DropAlerts.shift();
this.showAlert(alert);
}
};
return {
showAlert: this.showAlert,
hideAlert: this.hideAlert,
pushAlert: this.pushAlert,
changeStatus: this.changeStatus,
currentAlert: this.currentAlert,
checkPush: this.checkPush
};
}]);
So, when I fire interrupcion function, only in the case that connection is reestablished or lost, I need to stop the current timeout and, once the alert is hidden, show the new event.
I'm displaying this on a div, with a transition of 2 seconds.
Any ideas?
Actually the error wasn't on the code i posted, but when I called pushAlert.
I added a timeout in order to try to simulate fast event changes, and that was messing around with the timeouts.

Get public methods/functions for object

I've this code:
var Helper = function () {
this.prototype = {
loadScripts: function (scripts, path) {
scripts = $.map(scripts, function (scr) {
return $.getScript((path || '') + scr);
});
scripts.push($.Deferred(function (deferred) {
$(deferred.resolve);
}));
return $.when.apply($, scripts);
}
};
return {
loadScripts: loadScripts,
};
};
But I cannot access the method loadScripts with Helper.loadScripts() like here:
// Getting TypeError: FraggyHelper.loadScripts is not a function
Helper.loadScripts(['foo.js', 'bar.js'], '/path/to/');
Why is that? And how can I get access to this function?
try this
var Helper = {
loadScripts: function (scripts, path) {
scripts = $.map(scripts, function (scr) {
return $.getScript((path || '') + scr);
});
scripts.push($.Deferred(function (deferred) {
$(deferred.resolve);
}));
return $.when.apply($, scripts);
}
};
Change it to:
var Helper = function () {};
Helper.prototype.loadScripts = function (scripts, path) {
scripts = $.map(scripts, function (scr) {
return $.getScript((path || '') + scr);
});
scripts.push($.Deferred(function (deferred) {
$(deferred.resolve);
}));
return $.when.apply($, scripts);
};
// Getting TypeError: FraggyHelper.loadScripts is not a function
new Helper().loadScripts(['foo.js', 'bar.js'], '/path/to/');
var Helper = (function () {
var loadScripts = function (scripts, path) {
scripts = $.map(scripts, function (scr) {
return $.getScript((path || '') + scr);
});
scripts.push($.Deferred(function (deferred) {
$(deferred.resolve);
}));
return $.when.apply($, scripts);
};
return {
loadScripts: loadScripts,
} })();
You can do IFFE like above

Ionic infinite scroll

I am using wordpress as a backend for an app and I want to use infinite scroll but I am having trouble concatenating articles.
I am calling the service using a factory:
.factory('Worlds', function ($http) {
var worlds = [];
storageKey = "worlds";
function _getCache() {
var cache = localStorage.getItem(storageKey );
if (cache)
worlds = angular.fromJson(cache);
}
return {
all: function () {
return $http.get("http://www.examplesite.com/tna_wp/wp-json/posts?filter[category_name]=international&filter[posts_per_page]=10").then(function (response) {
worlds = response.data;
console.log(response.data);
return worlds;
});
},
GetNewPosts: function () {
return $http.get("http://www.examplesite.com/tna_wp/wp-json/posts?filter[category_name]=international&filter[posts_per_page]=2").then(function (response) {
worlds = response.data;
console.log(response.data);
return worlds;
});
},
get: function (worldId) {
if (!worlds.length)
_getCache();
for (var i = 0; i < worlds.length; i++) {
if (parseInt(worlds[i].ID) === parseInt(worldId)) {
return worlds[i];
}
}
return null;
}
}
})
and my controller looks like this:
.controller('WorldCtrl', function ($scope, $stateParams, $timeout, _, Worlds) {
$scope.worlds = [];
Worlds.all().then(function (data){
$scope.worlds = data;
window.localStorage.setItem("worlds", JSON.stringify(data));
},
function (err) {
if(window.localStorage.getItem("worlds") !== undefined) {
$scope.worlds = JSON.parse(window.localStorage.getItem("worlds"));
}
}
);
$scope.loadMore = function() {
Worlds.GetNewPosts().then(function (worlds){
var loadedIdss = _.pluck($scope.worlds, 'id');
var newItemss = _.reject(worlds, function (item){
return _.contains(loadedIdss, item.id);
});
$scope.worlds = newItemss.concat($scope.worlds);
$scope.$broadcast('scroll.infiniteScrollComplete');
});
};
})
I am trying to use underscore to ignore the posts that are already loaded, however when i try the infinite scroll it just goes into a loop calling more posts but doesnt add them to my ng-repeat and ionicLoading renders the app useless.
ion-infinite-scroll works with some sort of paginated result and you seem to feed your list with all the results.
Your API should look something like this:
http://www.examplesite.com/tna_wp/wp-json/posts?filter[category_name]=international&filter[posts_per_page]=2&filter[page]=1
notice I've added a page filter.
and your service responsible to fetch the data should look something like this:
.factory('dataService', function($http) {
return {
GetPosts: function(page, pageSize) {
return $http.get("http://mywebservice/api/test/posts/" + page + "/" + pageSize);
}
};
});
Your controller
.controller('mainController', function($scope, dataService) {
$scope.posts = [];
$scope.theEnd = false;
var page = 0;
var pageSize = 10;
$scope.$on('$stateChangeSuccess', function() {
$scope.loadMore();
});
$scope.loadMore = function(argument) {
page++;
dataService.GetPosts(page, pageSize)
.then(function(result) {
console.log('items fetched: ' + result.data.length);
if (result.data.length > 0) {
angular.forEach(result.data, function(value, key) {
$scope.posts.push(value);
});
}
else {
$scope.theEnd = true;
}
})
.finally(function() {
$scope.$broadcast("scroll.infiniteScrollComplete");
});
};
})
would initialize an array of objetct - as you're doing - and a boolean which tells the directive ion-infinite-scroll when you've reached the end:
$scope.posts = [];
$scope.theEnd = false;
Then you can have some variables to control the pagination:
var page = 0;
var pageSize = 10;
I start loading when the view is loaded:
$scope.$on('$stateChangeSuccess', function() {
$scope.loadMore();
});
$scope.loadMore then would increment the page number:
page++;
and call the service layer:
dataService.GetPosts(page, pageSize)
when I've reached the end of the stream I would set the variable:
$scope.theEnd = true;
to let the directive know we don't have other items to append.
.finally(function() {
$scope.$broadcast("scroll.infiniteScrollComplete");
});
finally is always called when the promise is resolved.
Instead of ng-repeat you can use collection-repeat which should be much faster.
You can play with it here.
Try this create a function infiniteScrollCompleteCancelLoadMore and call it when you want to complete the scroll and you have reached the end of your list.
function infiniteScrollCompleteCancelLoadMore() {
$timeout(function () {
$timeout(function () {
$scope.$broadcast('scroll.infiniteScrollComplete');
$rootScope.canLoad = false;
});
});
}
$scope.loadMore = function() {
Worlds.GetNewPosts().then(function (worlds){
var loadedIdss = _.pluck($scope.worlds, 'id');
var newItemss = _.reject(worlds, function (item){
return _.contains(loadedIdss, item.id);
});
$scope.worlds = newItemss.concat($scope.worlds);
infiniteScrollCompleteCancelLoadMore() // CHANGE HERE
});
};
and your HTML
<ion-infinite-scroll on-infinite="loadMore()" ng-if="canLoad" distance="1%"
immediate-check="false"></ion-infinite-scroll>
OR call This is you just want to cancel loadMore loop.
function infiniteScrollComplete() {
$timeout(function () {
$timeout(function () {
$scope.$broadcast('scroll.infiniteScrollComplete');
});
});
}

Javascript callback managment

I'm having trouble with designing a class which exposes its actions through callbacks. Yes my approach works for me but also seems too complex.
To illustrate the problem I've drawn the following picture. I hope it is useful for you to understand the class/model.
In my approach, I use some arrays holding user defined callback functions.
....
rocket.prototype.on = function(eventName, userFunction) {
this.callbacks[eventName].push(userFunction);
}
rocket.prototype.beforeLunch = function(){
userFunctions = this.callbacks['beforeLunch']
for(var i in userFunctions)
userFunctions[i](); // calling the user function
}
rocket.prototype.lunch = function() {
this.beforeLunch();
...
}
....
var myRocket = new Rocket();
myRocket.on('beforeLunch', function() {
// do some work
console.log('the newspaper guys are taking pictures of the rocket');
});
myRocket.on('beforeLunch', function() {
// do some work
console.log('some engineers are making last checks ');
});
I'm wondering what the most used approach is. I guess I could use promises or other libraries to make this implementation more understandable. In this slide using callbacks is considered evil. http://www.slideshare.net/TrevorBurnham/sane-async-patterns
So, should I use a library such as promise or continue and enhance my approach?
var Rocket = function () {
this.timer = null;
this.velocity = 200;
this.heightMoon = 5000;
this.goingToMoon = true;
this.rocketStatus = {
velocity: null,
height: 0,
status: null
};
this.listener = {
};
}
Rocket.prototype.report = function () {
for (var i in this.rocketStatus) {
console.log(this.rocketStatus[i]);
};
};
Rocket.prototype.on = function (name,cb) {
if (this.listener[name]){
this.listener[name].push(cb);
}else{
this.listener[name] = new Array(cb);
}
};
Rocket.prototype.initListener = function (name) {
if (this.listener[name]) {
for (var i = 0; i < this.listener[name].length; i++) {
this.listener[name][i]();
}
return true;
}else{
return false;
};
}
Rocket.prototype.launch = function () {
this.initListener("beforeLaunch");
this.rocketStatus.status = "Launching";
this.move();
this.initListener("afterLaunch");
}
Rocket.prototype.move = function () {
var that = this;
that.initListener("beforeMove");
if (that.goingToMoon) {
that.rocketStatus.height += that.velocity;
}else{
that.rocketStatus.height -= that.velocity;
};
that.rocketStatus.velocity = that.velocity;
if (that.velocity != 0) {
that.rocketStatus.status = "moving";
}else{
that.rocketStatus.status = "not moving";
};
if (that.velocity >= 600){
that.crash();
return;
}
if (that.rocketStatus.height == 2000 && that.goingToMoon)
that.leaveModules();
if (that.rocketStatus.height == that.heightMoon)
that.landToMoon();
if (that.rocketStatus.height == 0 && !that.goingToMoon){
that.landToEarth();
return;
}
that.report();
that.initListener("afterMove");
that.timer = setTimeout(function () {
that.move();
},1000)
}
Rocket.prototype.stop = function () {
clearTimeout(this.timer);
this.initListener("beforeStop");
this.velocity = 0;
this.rocketStatus.status = "Stopped";
console.log(this.rocketStatus.status)
this.initListener("afterStop");
return true;
}
Rocket.prototype.crash = function () {
this.initListener("beforeCrash");
this.rocketStatus.status = "Crashed!";
this.report();
this.stop();
this.initListener("afterCrash");
}
Rocket.prototype.leaveModules = function () {
this.initListener("beforeModules");
this.rocketStatus.status = "Leaving Modules";
this.initListener("afterModules");
}
Rocket.prototype.landToMoon = function () {
this.initListener("beforeLandToMoon");
this.rocketStatus.status = "Landing to Moon";
this.goingToMoon = false;
this.initListener("afterLandToMoon");
}
Rocket.prototype.landToEarth = function () {
this.initListener("beforeLandToEarth");
this.stop();
this.rocketStatus.status = "Landing to Earth";
this.initListener("afterLandToEarth");
}
Rocket.prototype.relaunch = function () {
this.initListener("beforeRelaunch");
this.timer = null;
this.velocity = 200;
this.heightMoon = 5000;
this.goingToMoon = true;
this.rocketStatus = {
velocity: 200,
height: 0,
status: "relaunch"
};
this.launch();
this.initListener("afterRelaunch");
}
init;
var rocket = new Rocket();
rocket.on("afterLaunch", function () {console.log("launch1")})
rocket.on("afterLandToMoon", function () {console.log("land1")})
rocket.on("beforeLandToEarth", function () {console.log("land2")})
rocket.on("afterMove", function () {console.log("move1")})
rocket.on("beforeLaunch", function () {console.log("launch2")})
rocket.launch();
You can add any function before or after any event.
This is my solution for this kinda problem. I am not using any special methods anything. I was just wonder is there any good practise for this like problems. I dig some promise,deferred but i just can't able to to this. Any ideas ?

Categories

Resources