I want to implement "pull down to refresh" in Angular. A lot of the JS libraries don't work with Angular, but I found this one:
https://github.com/mgcrea/angular-pull-to-refresh
However, it doesn't work. I see the text "pull down to refresh" on top, so directive is installed properly. But that's it, nothing happens when I scroll down.
var feedApp = angular.module("feedApp", ['mgcrea.pullToRefresh']);
....
<div id="feed-list-content" data-role="content">
<ul class="feed-list" data-role="listview" pull-to-refresh="onReload()">
<li ng-repeat="summaryItem in summaryItems | orderBy: 'created':true" class="feed-item">
....
</li>
</ul>
</div>
Same questions here: Pull to refresh in Angular Js and here: Angular JS - Pull to refresh
but no solution to my problem.
The onReload method is neither called.
I'm testing on both Android Emulator and Android device. Can't test on iOS, is it caused by Android?
I modified this plugin and solved my problem.
Try like this:
directive("pullToRefresh", function ($compile, $timeout, $q) {
return {
scope: true,
restrict: "A",
transclude: true,
template: '<div class="pull-to-refresh"><i ng-class="icon[status]"></i> <span ng-bind="text[status]"></span></div><div ng-transclude></div>',
compile: function compile(elem, attrs, transclude) {
return function postLink(scope, iElem, iAttrs) {
var body = $('body');
var scrollElement = iElem.parent();
var ptrElement = window.ptr = iElem.children()[0];
scope.text = {
pull: "pull to refresh",
release: "release to refresh",
loading: "refreshing..."
};
scope.icon = {
pull: "fa fa-arrow-down",
release: "fa fa-arrow-up",
loading: "fa fa-refresh fa-spin"
};
scope.status = "pull";
var shouldReload = false;
var setScrolTop = false;
var setStatus = function (status) {
shouldReload = status === "release";
scope.$apply(function () {
scope.status = status;
});
};
var touchPoint = 0;
iElem.bind("touchmove", function (ev) {
var top = body.scrollTop();
if (touchPoint === 0) touchPoint = ev.touches[0].clientY;
var diff = ev.touches[0].clientY - touchPoint;
if (diff >= 130) diff = 130;
if (diff < 80 && shouldReload) {
setStatus("pull");
setScrolTop = true;
}
if (top <= 0) {
scrollElement[0].style.marginTop = diff + "px";
if (diff > 80 && !shouldReload) setStatus("release");
}
});
iElem.bind("touchend", function (ev) {
if (setScrolTop) {
setScrolTop = false;
body.scrollTop(0);
}
if (!shouldReload) {
touchPoint = 0;
scrollElement[0].style.marginTop = "0px";
return;
}
ptrElement.style.webkitTransitionDuration = 0;
ptrElement.style.margin = '0 auto';
setStatus("loading");
var start = +new Date();
$q.when(scope.$eval(iAttrs.pullToRefresh))
.then(function () {
var elapsed = +new Date() - start;
$timeout(function () {
body.scrollTop(0);
touchPoint = 0;
scrollElement[0].style.marginTop = "0px";
ptrElement.style.margin = "";
ptrElement.style.webkitTransitionDuration = "";
scope.status = "pull";
}, elapsed < 400 ? 400 - elapsed : 0);
});
});
scope.$on("$destroy", function () {
iElem.unbind("touchmove");
iElem.unbind("touchend");
});
}
}
};
});
Related
WHAT I TRIED (DOES NOT WORK CORRECTLY):
CODE:
<script>
var app = angular.module('app', ['firebase']);
app.controller('ctrl', function ($scope, $firebaseArray, $timeout) {
$scope.data = [];
var _n = Math.ceil(($(window).height() - 50) / (350)) + 1;
var start = 0;
var end = _n - 1;
var lastScore = <%=lastScore%>;
console.log("FIRST FIRST FIRST LAST SCORE:" + lastScore);
var firstElementsLoaded = false;
$scope.getDataset = function() {
fb.orderByChild('score').endAt(lastScore).limitToLast(_n).on("child_added", function(dataSnapshot) {
lastScore = dataSnapshot.child("score").val() - 1;
console.log("LAST TOP LIKED:"+ lastScore);
$scope.data.push(dataSnapshot.val());
$scope.$apply();
console.log("THE VALUE:"+$scope.data);
$scope.data.splice(start, end).concat($scope.data.reverse());
$scope.$apply();
start = start + _n;
end = end + _n
firstElementsLoaded = true;
});
};
$scope.getDataset();
window.addEventListener('scroll', function() {
if (firstElementsLoaded == true) {
if (window.scrollY === document.body.scrollHeight - window.innerHeight) {
$scope.$apply($scope.getDataset());
}
}
});
});
// Compile the whole <body> with the angular module named "app"
angular.bootstrap(document.body, ['app']);
QUESTION:
How do I revert the data client-side to get my posts from top to bottom according to their score (from highest to lowest)?
WHAT I WOULD LIKE TO ACHIEVE:
Get my posts in descending order according to score which is a bit trickier with the infinite scroll.
Since the posts are displayed in reverse order, you should reverse the order of "pages"(use endAt instead of startAt), and sort posts reversely in every page.
See also this answer
Example
Setup:
$scope.data = [];
var n = Math.ceil(($(window).height() - 50) / (350)) + 1;
var firstElementsLoaded = false;
var lastScore = MAX_SCORE, lastKey
Function for scroll event listener:
$scope.getDataset = function() {
fb.orderByChild('score')
.endAt(lastScore, lastKey)
//endAt is inclusive, take one more for the n-th
.limitToLast(n + firstElementsLoaded)
.once('value', loadPosts)
function loadPosts(snapshot) {
var posts = snapshot.val()
function compare(a, b) {
if (posts[a].score != posts[b].score) {
return b.score - a.score
}
if (a < b) return 1
if (a > b) return -1
return 0
}
//skip the post included by endAt
var ordered = Object.keys(posts).sort(compare).slice(firstElementsLoaded)
lastKey = ordered[ordered.length-1]
lastScore = posts[lastKey].score
ordered.forEach(function(key) {
$scope.data.push(posts[key])
})
$scope.$apply();
firstElementsLoaded = true;
}
}
For a more elegant way, you may try store scores reversely, or use an additional value.
fb.orderByChild('reversedScore')
I ended up using an inversedScore property:
<script>
var app = angular.module('app', ['firebase']);
app.controller('ctrl', function ($scope, $firebaseArray, $timeout) {
$scope.data = [];
var _n = Math.ceil(($(window).height() - 50) / (350)) + 1;
var firstElementsLoaded = false;
var lastScore = -100000;
$scope.getDataset = function() {
fb.orderByChild('inversedScore').startAt(lastScore).limitToFirst(_n).on("child_added", function(dataSnapshot) {
lastScore = dataSnapshot.child("inversedScore").val() + 1;
console.log("LAST LIKE SCORE:"+ lastScore);
$scope.data.push(dataSnapshot.val());
$scope.$apply();
console.log("THE VALUE:"+$scope.data);
firstElementsLoaded = true;
});
};
$scope.getDataset();
window.addEventListener('scroll', function() {
if (firstElementsLoaded == true) {
if (window.scrollY === document.body.scrollHeight - window.innerHeight) {
$scope.$apply($scope.getDataset());
}
}
});
});
// Compile the whole <body> with the angular module named "app"
angular.bootstrap(document.body, ['app']);
</script>
I have some extremely weird interference between two angular functions in my application that I cannot explain. The end result of the problem causes this to happen:
I have a countdown directive embedded in one of my pages which is also the domain of an angular controller. Here is the countdown directive:
(function() {
var app = angular.module('app');
app.directive('countdown', ['$interval', function($interval) {
return {
restrict: 'E',
scope: {
specificity: '=',
countdownTo: '=',
callback: '&?'
},
link: function($scope, elem, attrs) {
$scope.isLaunchExact = ($scope.specificity == 6 || $scope.specificity == 7);
$scope.$watch('specificity', function(newValue) {
$scope.isLaunchExact = (newValue == 6 || newValue == 7);
});
var countdownProcessor = function() {
var launchUnixSeconds = $scope.launchUnixSeconds;
var currentUnixSeconds = Math.floor(Date.now() / 1000);
if (launchUnixSeconds >= currentUnixSeconds) {
$scope.secondsAwayFromLaunch = launchUnixSeconds - currentUnixSeconds;
var secondsBetween = $scope.secondsAwayFromLaunch;
// Calculate the number of days, hours, minutes, seconds
$scope.days = Math.floor(secondsBetween / (60 * 60 * 24));
secondsBetween -= $scope.days * 60 * 60 * 24;
$scope.hours = Math.floor(secondsBetween / (60 * 60));
secondsBetween -= $scope.hours * 60 * 60;
$scope.minutes = Math.floor(secondsBetween / 60);
secondsBetween -= $scope.minutes * 60;
$scope.seconds = secondsBetween;
$scope.daysText = $scope.days == 1 ? 'Day' : 'Days';
$scope.hoursText = $scope.hours == 1 ? 'Hour' : 'Hours';
$scope.minutesText = $scope.minutes == 1 ? 'Minute' : 'Minutes';
$scope.secondsText = $scope.seconds == 1 ? 'Second' : 'Seconds';
} else {
}
if (attrs.callback) {
$scope.callback();
}
};
// Countdown here
if ($scope.isLaunchExact) {
$scope.launchUnixSeconds = moment($scope.countdownTo).unix();
$interval(countdownProcessor, 1000);
} else {
$scope.countdownText = $scope.countdownTo;
}
},
templateUrl: '/js/templates/countdown.html'
}
}]);
})();
Here is the Angular controller that is being interfered with:
(function() {
var app = angular.module('app', []);
//app.value('duScrollDuration', 1000);
app.controller("homeController", ['$scope', 'Statistic', function($scope, Statistic) {
$scope.statistics = [];
$scope.activeStatistic = false;
$scope.goToClickedStatistic = function(statisticType) {
history.replaceState('', document.title, '#' + statisticType);
$scope.activeStatistic = statisticType;
};
$scope.goToNeighborStatistic = function(index) {
if (index >= 0 && index < $scope.statistics.length) {
var stat = $scope.statistics[index];
history.replaceState('', document.title, '#' + stat.camelCaseType); // WhyIsThisFlashing
$scope.activeStatistic = stat.camelCaseType;
return stat.camelCaseType;
} else {
$scope.goHome();
}
};
$scope.goToFirstStatistic = function() {
};
$scope.goHome = function() {
history.replaceState('', document.title, window.location.pathname);
$scope.activeStatistic = false;
return 'home';
};
/*$window.on('scroll',
$.debounce(100, function() {
$('div[data-stat]').fracs('max', 'visible', function(best) {
$scope.activeStatistic($(best).data('stat'));
});
})
);*/
(function() {
laravel.statistics.forEach(function(statistic) {
$scope.statistics.push(new Statistic(statistic));
});
if (window.location.hash) {
$scope.activeStatistic = window.location.hash.substring(1);
}
})();
}]);
app.factory('Statistic', function() {
return function(statistic) {
var self = {};
self.changeSubstatistic = function(newSubstatistic) {
self.activeSubstatistic = newSubstatistic;
};
statistic.forEach(function(substatistic) {
if (!self.substatistics) {
self.substatistics = [];
self.activeSubstatistic = substatistic;
self.type = substatistic.type;
self.camelCaseType = self.type.replace(" ", "");
}
self.substatistics.push(substatistic);
});
return self;
}
});
})();
Each time my countdown directive's countdownProcessor function present in the $interval is run, goToNeighborStatistic in my angular controller is, somehow, called. I don't have a clue why.
If I step through my countdown directive, after stepping through the countdownProcessor function, an "Anonymous Script" named SCRIPT0 is called with the following contents:
"use strict";
var fn=function(s,l,a,i){var v0,v1,v2,v3=l&&('goToNeighborStatistic' in l),v4,v5,v6=l&&('\u0024index' in l);v2=v3?l:s;if(!(v3)){if(s){v1=s.goToNeighborStatistic;}}else{v1=l.goToNeighborStatistic;}if(v1!=null){ensureSafeFunction(v1,text);if(!(v6)){if(s){v5=s.$index;}}else{v5=l.$index;}v4=ifDefined(v5,0)-ifDefined(1,0);ensureSafeObject(v2,text);v0=ensureSafeObject(v2.goToNeighborStatistic(ensureSafeObject(ifDefined(v5,0)-ifDefined(1,0),text)),text);}else{v0=undefined;}return v0;};return fn;
Why is "goToNeighborStatistic" present in this? Why is this script even present? From there, goToNeighborStatistic() runs, at which point, the countdown is called again and everything repeats. What's going on here?
EDIT:
In Chrome, stepping through everything produces different results. At the end of the countdownProcessor function, stepping into produces this:
I am new to angularjs the below directive is to support decimal and commas, it works fine when a change is made to the field how ever the data in the fields are not validated when the page loads
var app = angular.module('myApply.directives', [], function () {
});
app.directive('numericDecimalInput', function($filter, $browser, $locale,$rootScope) {
return {
require: 'ngModel',
priority: 1,
link: function($scope, $element, $attrs, ngModelCtrl) {
var replaceRegex = new RegExp($locale.NUMBER_FORMATS.GROUP_SEP, 'g');
var fraction = $attrs.fraction || 0;
var listener = function() {
var value = $element.val().replace(replaceRegex, '');
$element.val($filter('number')(value, fraction));
};
var validator=function(viewValue) {
ngModelCtrl.$setValidity('outOfMax', true);
ngModelCtrl.$setValidity(ngModelCtrl.$name+'Numeric', true);
ngModelCtrl.$setValidity('rangeValid', true);
if(!_.isUndefined(viewValue))
{
var newVal = viewValue.replace(replaceRegex, '');
var newValAsNumber = newVal * 1;
// check if new value is numeric, and set control validity
if (isNaN(newValAsNumber)) {
ngModelCtrl.$setValidity(ngModelCtrl.$name + 'Numeric', false);
} else
{
if (newVal < 0) {
ngModelCtrl.$setValidity(ngModelCtrl.$name + 'Numeric', false);
}
else {
newVal = newValAsNumber.toFixed(fraction);
ngModelCtrl.$setValidity(ngModelCtrl.$name + 'Numeric', true);
if (!(_.isNull($attrs.maxamt) || _.isUndefined($attrs.maxamt))) {
var maxAmtValue = Number($attrs.maxamt) || Number($scope.$eval($attrs.maxamt));
if (newVal > maxAmtValue) {
ngModelCtrl.$setValidity('outOfMax', false);
} else {
ngModelCtrl.$setValidity('outOfMax', true);
}
if(!(_.isNull($attrs.minamt) || _.isUndefined($attrs.minamt)))
{
var minAmtValue = Number($attrs.minamt)|| Number($scope.$eval($attrs.minamt));
if((newVal > maxAmtValue) || (newVal < minAmtValue)){
ngModelCtrl.$setValidity('rangeValid', false);
}
else
{
ngModelCtrl.$setValidity('rangeValid', true);
}
}
}
else if((!(_.isNull($attrs.minamt) || _.isUndefined($attrs.minamt))))
{
var minAmtValue = Number($attrs.minamt)|| Number($scope.$eval($attrs.minamt));
if(newVal < minAmtValue)
{
ngModelCtrl.$setValidity('outOfMin', false);
}
else
{
ngModelCtrl.$setValidity('outOfMin', true);
}
}
else {
ngModelCtrl.$setValidity('outOfMax', true);
}
}
}
return newVal;
}
};
// This runs when the model gets updated on the scope directly and keeps our view in sync
ngModelCtrl.$render = function() {
ngModelCtrl.$setValidity('outOfMax', true);
ngModelCtrl.$setValidity(ngModelCtrl.$name+'Numeric', true);
$element.val($filter('number')(ngModelCtrl.$viewValue, fraction));
};
$element.bind('change', listener);
$element.bind('keydown', function(event) {
var key = event.keyCode;
// If the keys include the CTRL, SHIFT, ALT, or META keys, home, end, or the arrow keys, do nothing.
// This lets us support copy and paste too
if (key == 91 || (15 < key && key < 19) || (35 <= key && key <= 40))
return;
});
$element.bind('paste cut', function() {
$browser.defer(listener);
});
ngModelCtrl.$parsers.push(validator);
ngModelCtrl.$formatters.push(validator);
}
};
});
Could some one please let me know as what I am missing .
Thanking you in advance.
Have you declared your app (doesn't show this in your code)? ie:
var app = angular.module('app', []);
Do you have ng-app in your HTML document anywhere?
<html lang="en-GB" ng-app="app">
Check out the documentation to get started with modules (Angular is very well documented, well worth reading): https://docs.angularjs.org/guide/module
app.directive('numericDecimalInput', function($filter, $browser, $locale,$rootScope) {
return {
require: 'ngModel',
priority: 1,
link: function($scope, $element, $attrs, ngModelCtrl) {
...
validator($element.val());
}
};
});
I implemented a directive including plangular experiment
Here the code:
as.directive('plangular', function ($document, $rootScope, $http) {
// Define the audio engine
var audio = $document[0].createElement('audio');
// Define the player object
var player = {
track: false,
playing: false,
paused: false,
tracks: null,
i: null,
play: function(tracks, i) {
if (i == null) {
tracks = new Array(tracks);
i = 0;
};
player.tracks = tracks;
player.track = tracks[i];
player.i = i;
if (player.paused != player.track) audio.src = player.track.stream_url + '?client_id=' + clientID;
audio.play();
player.playing = player.track;
player.paused = false;
},
pause: function() {
audio.pause();
if (player.playing) {
player.paused = player.playing;
player.playing = false;
};
},
// Functions for playlists (i.e. sets)
playPlaylist: function(playlist) {
if (player.tracks == playlist.tracks && player.paused) player.play(player.tracks, player.i);
else player.play(playlist.tracks, 0);
},
next: function(playlist) {
if (!playlist){
if (player.i+1 < player.tracks.length) {
player.i++;
player.play(player.tracks, player.i);
} else {
player.pause();
};
} else if (playlist && playlist.tracks == player.tracks) {
if (player.i + 1 < player.tracks.length) {
player.i++;
player.play(playlist.tracks, player.i);
} else {
player.pause();
};
};
},
previous: function(playlist) {
if (playlist.tracks == player.tracks && player.i > 0) {
player.i = player.i - 1;
player.play(playlist.tracks, player.i);
};
}
};
audio.addEventListener('ended', function() {
$rootScope.$apply(function(){
if (player.tracks.length > 0) player.next();
else player.pause();
});
}, false);
// Returns the player, audio, track, and other objects
return {
restrict: 'A',
scope: true,
link: function (scope, elem, attrs) {
// RESOLVE THE AUDIO
var params = { url: attrs.src, client_id: clientID, callback: 'JSON_CALLBACK' }
$http.jsonp('//api.soundcloud.com/resolve.json', { params: params }).success(function(data){
// Handle playlists (i.e. sets)
if (data.tracks) scope.playlist = data;
// Handle single track
else if (data.kind == 'track') scope.track = data;
// Handle all other data
else scope.data = data;
});
scope.player = player;
scope.audio = audio;
scope.currentTime = 0;
scope.duration = 0;
// Updates the currentTime and duration for the audio
audio.addEventListener('timeupdate', function() {
if (scope.track == player.track || (scope.playlist && scope.playlist.tracks == player.tracks)){
scope.$apply(function() {
scope.currentTime = (audio.currentTime * 1000).toFixed();
scope.duration = (audio.duration * 1000).toFixed();
});
};
}, false);
// Handle click events for seeking
scope.seekTo = function($event){
var xpos = $event.offsetX / $event.target.offsetWidth;
audio.currentTime = (xpos * audio.duration);
};
}
}
});
And here how I used it in my index.html view.
<section plangular data-src="https://soundcloud.com/a-beautiful-place-1/the-stoners-guide-to-the-galaxy" class="measure-wide wrap mb4">
<div>
<div class="controllButton">
<a href ng-click="player.play(track)" ng-hide="player.playing == track" class="button button-geo anim-popin">
<div class=""><img src="lib/player_js/icons/play.png" /></div>
</a>
<a href="" ng-click="player.pause()" ng-show="player.playing == track" class="button button-geo anim-popin">
<div> <img src="lib/player_js/icons/pause.png" /> </div>
</a>
</div>
<div class="controllScrobbler">
<div class="absolute t0 r0 b0 l0 seekHolder" >
<div class=" t0 r0 b0 l0 overlaySeeker" ng-click="seekTo($event)"></div>
<div class=" t0 b0 l0 seeker" style="width: {{ (currentTime / duration) * 100 }}%"></div>
<div class=" t0 r0 b0 l0 bg-white"></div>
</div>
</div>
</div>
</section>
Now, it works perfectly. What I would like to do is to interact with the object player within the directive from different controllers. The controllers are loaded in the partials of different pages. There is no scope sharing between the directive and the other controllers.
I'm looking to something like scope.plangular.player.MyFunctionInsideTheDirective(my args)
Is it possible? Something like the directive is a service, and the controllers are interact with that.
Any ideas?
I am using infinite scroll directive and this is the code:
angApp.directive('infiniteScroll', [
'$rootScope', '$window', '$timeout', function ($rootScope, $window, $timeout) {
return {
link: function (scope, elem, attrs) {
var checkWhenEnabled, handler, scrollDistance, scrollEnabled;
$window = angular.element($window);
scrollDistance = 0;
if (attrs.infiniteScrollDistance != null) {
scope.$watch(attrs.infiniteScrollDistance, function (value) {
return scrollDistance = parseInt(value, 10);
});
}
scrollEnabled = true;
checkWhenEnabled = false;
if (attrs.infiniteScrollDisabled != null) {
scope.$watch(attrs.infiniteScrollDisabled, function (value) {
scrollEnabled = !value;
if (scrollEnabled && checkWhenEnabled) {
checkWhenEnabled = false;
return handler();
}
});
}
handler = function () {
var elementBottom, remaining, shouldScroll, windowBottom;
console.log($window);
windowBottom = $window.height() + $window.scrollTop();
elementBottom = elem.offset().top + elem.height();
remaining = elementBottom - windowBottom;
shouldScroll = remaining <= $window.height() * scrollDistance;
if (shouldScroll && scrollEnabled) {
if ($rootScope.$$phase) {
return scope.$eval(attrs.infiniteScroll);
} else {
return scope.$apply(attrs.infiniteScroll);
}
} else if (shouldScroll) {
return checkWhenEnabled = true;
}
};
$window.on('scroll', handler);
scope.$on('$destroy', function () {
return $window.off('scroll', handler);
});
return $timeout((function () {
if (attrs.infiniteScrollImmediateCheck) {
if (scope.$eval(attrs.infiniteScrollImmediateCheck)) {
return handler();
}
} else {
return handler();
}
}), 0);
}
};
}
]);
The problem with this code is sometimes it works sometimes it doesn't. If I do a hard refresh by pressing Ctrl + F5 it most certainly throws the below error.
This is the error:
I am using Firefox 29.0.1. What am I missing?
Normally this should work, but there might be some problem with requireJS. Using $() instead should ensure you are using Jquery. You might need the order plug-in for requireJS since normally RequireJS loads and evaluates scripts in an undetermined order.