Resyncing $urlRouterProvider.deferIntercept(); for angular 1 ui router - angularjs

In Angular 1.5 and angular ui router, I want to change the back button behaviour of my states to recognise the names of the states and not the params as I have a url /restaurant/1?param1=value&param2=value which dynamically changes without refreshing the page. I have the binding and url not changing when a property changes, but when I hit the back button it goes to the previous params state, not the saved state in $rootScope by name. I've debugged this for hours and my current solution only works sometimes, it's not consistent because the url sync isn't always called in time thus the state does not update when the url updates. I'm correctly identifying when the back button is hit but it's not refreshing the state. Right now, I have to use a location.assign and yet it only works some of the time. Is there anyway to resync the deferIntercept in angular ui router?
function bind(propGetters,
getUrl) {
let unwatch = this._$rootScope.$watchGroup(propGetters, () => {
let trimmedUrl = getUrl();
let remove = this._$rootScope.$on('$locationChangeSuccess', () => {
this._$rootScope.disableBackButton = false;
remove();
});
this._$rootScope.disableBackButton = true;
if (!this._$rootScope._hitBackButton) {
this._$rootScope.shouldSync = false;
this._$location.url(trimmedUrl);
}
});
return {
unbind(): void {
unwatch();
}
};
module.run(
($urlRouter, $rootScope) => {
$rootScope.shouldSync = true;
$rootScope.$on('$locationChangeSuccess', e => {
if ($rootScope.shouldSync) {
$urlRouter.sync();
} else if (!$rootScope._hitBackButton) {
e.preventDefault();
}
$rootScope.shouldSync = true;
});
$rootScope.$on("$stateChangeSuccess", (event, toState, toParams, fromState, fromParams) => {
if ($rootScope.previousState) {
if ($rootScope.previousState.name !== fromState.name && !$rootScope.disableBackButton) {
$rootScope.previousState = lodash.merge(fromState, { params: fromParams });
console.log($rootScope.previousState.name, 'previousState');
}
} else {
this._$rootScope.previousState = lodash.merge(fromState, { params: fromParams });
}
});
$rootScope.$on('$locationChangeSuccess', (evt, newUrl, oldUrl) => {
$rootScope.actualPrevious = oldUrl;
if ($rootScope._hitBackButton) {
this._urlProvider.sync();
$rootScope._hitBackButton = false;
}
});
$rootScope.$on('$locationChangeStart', (evt, newUrl, oldUrl) => {
if ($rootScope.actualPrevious === newUrl && $rootScope.previousState && !$rootScope.disableBackButton && !$rootScope._hitBackButton) {
$rootScope.shouldSync = true;
event.preventDefault();
console.log('hit back', $rootScope.previousState.name);
$rootScope._hitBackButton = true;
window.location.assign(this._urlService.getFullPath(this._$state.href($rootScope.previousState.name, $rootScope.previousState.params)))
// this._$state.go($rootScope.previousState.name, $rootScope.previousState.params, { reload: true }); - this doesn't seem to always work because of watch collisions?
}
});
$urlRouter.listen();
});

After playing around for 5 hours,
I found it's best to avoid using deferIntercept all together and instead reloadOnSearch: false like so
$stateProvider.state('app.restaurant', {
url: '/restaurant/{slug}?param1&param2&param3',
resolve: {
param1: function 1,
param2: function 2,
param3: function 3
},
reloadOnSearch: false,
params: {
slug: { value: null },
},
controller: 'restaurantController',
template:
});
Note you will have to use deferIntercept for slashes instead of params. I'd strongly advise using params if your url changes. The back button should work fine with my solution above minus the deferIntercept and plus the reloadOnSearch: false

Full modified implementation:
this._$rootScope.$on('$locationChangeStart', (evt, newUrl, oldUrl) => {
if (this._$rootScope.actualPrevious === newUrl && this._$rootScope.previousState && !this._$rootScope._hitBackButton) {
evt.preventDefault();
this._$rootScope._hitBackButton = true;
this._$state.go(this._$rootScope.previousState.name, this._$rootScope.previousState.params);
}
});
this._$rootScope.$on("$stateChangeSuccess", (event, toState, toParams, fromState, fromParams) => {
if (this._$rootScope.previousState) {
if (this._$rootScope.previousState.name !== fromState.name) {
this._$rootScope.previousState = lodash.merge(fromState, { params: fromParams });
}
} else {
this._$rootScope.previousState = lodash.merge(fromState, { params: fromParams });
}
});
this._$rootScope.$on('$locationChangeSuccess', (evt, newUrl, oldUrl) => {
this._$rootScope.actualPrevious = oldUrl;
if (this._$rootScope._hitBackButton) {
this._$rootScope._hitBackButton = false;
}
});

Related

TypeError: Attempted to assign to readonly property on '$stateChangeStart' event's params

We have the following function that shows a confirmation dialog to the user when there are unsaved changes and the user tried to navigate away.
$scope.$on('$stateChangeStart', (event, toState, toParams, __fromState, fromParams) => {
if (fromParams.userConfirmedRedirect) {
return;
}
const checklistIsActive = $scope.revision &&
$scope.revision.checklist.status === ChecklistConstants.Status.ACTIVE;
const stateChangeIsWithinSameChecklist = toParams.id === $scope.checklistId;
if (checklistIsActive && !stateChangeIsWithinSameChecklist) {
const queueKey = PromiseQueueKeyGenerator.generateByChecklistId($scope.checklistId);
if (PromiseQueueService.isInProgressByKey(queueKey)) {
event.preventDefault();
MessageBox.confirm({
title: 'Are you sure?',
message: 'Please wait while we save your changes and try again.',
okButton: {
type: 'danger',
text: 'Yes',
action() {
fromParams.userConfirmedRedirect = true;
$state.go(toState.name, toParams, options);
},
},
});
}
}
});
This used to work perfectly fine before, but now throws an error (probably after an upgrade or something).
The error is:
TypeError: Attempted to assign to readonly property.
at action(./app/features/checklist/checklist-ctrl.js:517:33)
at fn(./app/features/message-box/confirm-ctrl.js:13:53)
at expensiveCheckFn(./node_modules/angular/angular.js:16213:1)
at $element(./node_modules/angular/angular.js:26592:1)
at $apply(./node_modules/angular/angular.js:18094:1)
at ? (./node_modules/angular/angular.js:26597:1)
at dispatch(/npm/jquery/dist/jquery.min.js:2:41777)
at s(/login:5:23562)
at _a(./node_modules/#sentry/browser/dist/index.js:3257:1)
The line that it's thrown from is this:
fromParams.userConfirmedRedirect = true;
I understand the reason for the error, however, I am stuck with a solution.
We need to pass an additional parameter to be available on the next $stateChangeStart event in order to make sure that the confirmation only shown once per route change.
I tried using the options argument for that:
$scope.$on('$stateChangeStart', (event, toState, toParams, __fromState, __fromParams, options) => {
if (options.custom.userConfirmedRedirect) {
return;
}
const checklistIsActive = $scope.revision &&
$scope.revision.checklist.status === ChecklistConstants.Status.ACTIVE;
const stateChangeIsWithinSameChecklist = toParams.id === $scope.checklistId;
if (checklistIsActive && !stateChangeIsWithinSameChecklist) {
const queueKey = PromiseQueueKeyGenerator.generateByChecklistId($scope.checklistId);
if (PromiseQueueService.isInProgressByKey(queueKey)) {
event.preventDefault();
MessageBox.confirm({
title: 'Are you sure?',
message: 'Please wait while we save your changes and try again.',
okButton: {
type: 'danger',
text: 'Yes',
action() {
options.custom.userConfirmedRedirect = true;
$state.go(toState.name, toParams, options);
},
},
});
}
}
});
But this approach didn't work.
What are my other options?

Delay loading data in Angular JS

I have code like this
(function (app) {
app.controller('productListController', productListController)
productListController.$inject = ['$scope', 'apiService', 'notificationService', '$ngBootbox', '$filter'];
function productListController($scope, apiService, notificationService, $ngBootbox, $filter) {
$scope.products = [];
$scope.page = 0;
$scope.pagesCount = 0;
$scope.getProducts = getProducts;
$scope.keyword = '';
$scope.search = search;
$scope.deleteProduct = deleteProduct;
$scope.selectAll = selectAll;
$scope.deleteMultiple = deleteMultiple;
function deleteMultiple() {
var listId = [];
$.each($scope.selected, function (i, item) {
listId.push(item.ID);
});
var config = {
params: {
checkedProducts: JSON.stringify(listId)
}
}
apiService.del('/api/product/deletemulti', config, function (result) {
notificationService.displaySuccess('Deleted successfully ' + result.data + 'record(s).');
search();
}, function (error) {
notificationService.displayError('Can not delete product.');
});
}
$scope.isAll = false;
function selectAll() {
if ($scope.isAll === false) {
angular.forEach($scope.products, function (item) {
item.checked = true;
});
$scope.isAll = true;
} else {
angular.forEach($scope.products, function (item) {
item.checked = false;
});
$scope.isAll = false;
}
}
$scope.$watch("products", function (n, o) {
var checked = $filter("filter")(n, { checked: true });
if (checked.length) {
$scope.selected = checked;
$('#btnDelete').removeAttr('disabled');
} else {
$('#btnDelete').attr('disabled', 'disabled');
}
}, true);
function deleteProduct(id) {
$ngBootbox.confirm('Are you sure to detele?').then(function () {
var config = {
params: {
id: id
}
}
apiService.del('/api/product/delete', config, function () {
notificationService.displaySuccess('The product hase been deleted successfully!');
search();
}, function () {
notificationService.displayError('Can not delete product');
})
});
}
function search() {
getProducts();
}
function getProducts(page) {
page = page || 0;
var config = {
params: {
keyword: $scope.keyword,
page: page,
pageSize: 20
}
}
apiService.get('/api/product/getall', config, function (result) {
if (result.data.TotalCount == 0) {
notificationService.displayWarning('Can not find any record.');
}
$scope.products = result.data.Items;
$scope.page = result.data.Page;
$scope.pagesCount = result.data.TotalPages;
$scope.totalCount = result.data.TotalCount;
}, function () {
console.log('Load product failed.');
});
}
$scope.getProducts();
}
})(angular.module('THTCMS.products'));
So my problem is when i loading data the application take me some time to load data.
I need load data as soon as
Is the any solution for this?
Since you are loading data via api call, there will be a delay. To handle this delay, you should display a loading screen. Once the data is loaded, the loading screen gets hidden and your main screen is visible. You can achieve this using $http interceptors.
See : Showing Spinner GIF during $http request in angular
The api-call is almost certainly causing the delay. Data may be received slowly via the api-call so you could display any sort of loading text/image to notify the use that the data is being loaded.
If u want the data ready at the time when controller inits, u can add a resolve param and pass the api call as a $promise in the route configuration for this route.

How to test handler with if/then blocks in a unit test for an Angular.js service?

I am new to testing and I need to test the handler in my Angular.js service (Using Jasmine and Mocha). This service checks the url and redirects to a state main.loadbalancer.readonly if the device number is readonly.
I am not sure how to test the handler, so since they are 2 if/then blocks with a function, I broke them up into their each beforeEach(inject(function() {}). Since they are going to be reused for several instances, I thought that would be best and then call them by callHandler1(....), etc.
Right now, I am getting:
readonly service paste url with device 55, first load, will not go to main state
รข ReferenceError: callHandler1 is not defined
Am I doing this correctly?
Service:
angular.module("main.loadbalancer").run(function($rootScope, $state, DeviceVal) {
return $rootScope.$on("$stateChangeStart", function(event, toState) {
var checkUrlDirectEdit, device;
device = window.location.hash.split("/")[2];
checkUrlDirectEdit = function() {
return DeviceVal.previousDevice !== device && toState.name !== "main" && DeviceVal.lb.id === "undefined";
};
if (typeof DeviceVal.lb.id === "undefined" && toState.name !== "main" || checkUrlDirectEdit()) {
event.preventDefault();
DeviceVal.previousDevice = device;
$state.transitionTo("main", {}, {
location: false
});
}
if (DeviceVal.readonly && toState.name !== "main.loadbalancer.readonly") {
event.preventDefault();
$state.transitionTo("main.loadbalancer.readonly", {
id: DeviceVal.lb.id
}, {
location: true,
reload: true
});
}
return window.addEventListener("hashchange", function() {
return location.reload();
});
});
});
Test for service:
describe("readonly service", function() {
beforeEach(function() {
return module("main");
});
beforeEach(inject(function($rootScope, _$state_, _DeviceVal_) {
$rootScope = $rootScope;
$state = _$state_;
DeviceVal = _DeviceVal_;
spyOn($state, "transitionTo");
spyOn($rootScope, "$broadcast");
event = jasmine.createSpyObj("event", ["preventDefault"]);
event.preventDefault();
}));
beforeEach(inject(function() {
checkUrlDirectEdit = function(DeviceVal, toState) {
return DeviceVal.previousDevice !== device && toState.name !== "main" && DeviceVal.lb.id === "undefined";
}
}));
beforeEach(inject(function() {
function callHandler1(DeviceVal, toState, device) {
if (typeof DeviceVal.lb.id === "undefined" && toState.name !== "main" || checkUrlDirectEdit()) {
event.preventDefault();
DeviceVal.previousDevice = device;
$state.transitionTo("main", {}, {
location: false
});
}
}
}));
beforeEach(inject(function() {
function callHandler2(DeviceVal, toState, device) {
if (DeviceVal.readonly && toState.name !== "main.loadbalancer.readonly") {
event.preventDefault();
$state.transitionTo("main.loadbalancer.readonly", {
id: DeviceVal.lb.id
}, {
location: true,
reload: true
});
}
}
}));
it("paste url with device 55, first load, will not go to main state", function() {
device = 55;
DeviceVal.lb.id = undefined;
toState = { name: "not main" },
DeviceVal.previousDevice = undefined;
DeviceVal.readonly = false;
callHandler1(DeviceVal, toState, device);
callHandler2(DeviceVal, toState, device)
expect(event.preventDefault).toBeDefined();
});
it("paste url with device 56, first load, will go to main state", function() {
device = 56;
Device.lb.id = undefined;
toState = { name: "not main" },
DeviceVal.previousDevice = undefined;
DeviceVal.readonly = true;
callHandler1(DeviceVal, toState, device);
callHandler2(DeviceVal, toState, device)
expect(event.preventDefault).toHaveBeenCalled();
expect($state.transitionTo).toHaveBeenCalledWith("main.loadbalancer.readonly",
{ id: 55 },
{ location: true, reload: true });
});
});

Authorization Service fails on page refresh in angularjs

Implemented the authorization using the POST
The problem is when i go to a privileged page say '/admin' it works but when i refresh the page
manually, the admin page is redirecting to the '/unauthorized' page
Permissions service
angular.module('myApp')
.factory('PermissionsService', function ($rootScope,$http,CookieService) {
var permissionList;
return {
setPermissions: function(permissions) {
permissionList = permissions;
$rootScope.$broadcast('permissionsChanged')
},
getPermissions: function() {
var roleId = 5
if(CookieService.getLoginStatus())
var roleId = CookieService.getUserData().ROLE_ID;
return $http.post('api/user-permissions', roleId).then(function(result){
return result.data;
});
},
hasPermission: function (permission) {
permission = permission.trim();
return _.some(permissionList, function(item) {
if(_.isString(item.name))
return item.name.trim() === permission
});
}
};
});
hasPermissions directive
angular.module('myApp')
.directive('hasPermission', function(PermissionsService) {
return {
link: function(scope, element, attrs) {
if(!_.isString(attrs.hasPermission))
throw "hasPermission value must be a string";
var value = attrs.hasPermission.trim();
var notPermissionFlag = value[0] === '!';
if(notPermissionFlag) {
value = value.slice(1).trim();
}
function toggleVisibilityBasedOnPermission() {
var hasPermission = PermissionsService.hasPermission(value);
if(hasPermission && !notPermissionFlag || !hasPermission && notPermissionFlag)
element.show();
else
element.hide();
}
toggleVisibilityBasedOnPermission();
scope.$on('permissionsChanged', toggleVisibilityBasedOnPermission);
}
};
});
app.js
var myApp = angular.module('myApp',['ngRoute','ngCookies']);
myApp.config(function ($routeProvider,$httpProvider) {
$routeProvider
.when('/', {
templateUrl: 'app/module/public/index.html',
header: 'app/partials/header.html',
footer: 'app/partials/footer.html'
})
.when('/login', {
templateUrl: 'app/module/login/login.html',
header: 'app/partials/header.html',
footer: 'app/partials/footer.html'
})
.when('/home', {
templateUrl: 'app/module/home/home.html',
header: 'app/partials/header.html',
footer: 'app/partials/footer.html'
})
.when('/register', {
templateUrl: 'app/module/register/register.html',
header: 'app/partials/header.html',
footer: 'app/partials/footer.html'
})
.when('/admin', {
templateUrl: 'app/module/admin/admin.html',
header: 'app/partials/header.html',
footer: 'app/partials/footer.html',
permission: 'admin'
})
.when('/unauthorized', {
templateUrl: 'app/partials/unauthorized.html',
header: 'app/partials/header.html',
footer: 'app/partials/footer.html'
})
.otherwise({redirectTo: '/'});
$httpProvider.responseInterceptors.push('securityInterceptor');
});
myApp.provider('securityInterceptor', function() {
this.$get = function($location, $q) {
return function(promise) {
return promise.then(null, function(response) {
if(response.status === 403 || response.status === 401) {
$location.path('/unauthorized');
}
return $q.reject(response);
});
};
};
});
myApp.run(function($rootScope, $location, $window, $route, $cookieStore, CookieService, PermissionsService) {
PermissionsService.getPermissions().then(function(permissionList){
PermissionsService.setPermissions(permissionList);
});
// Check login status on route change start
$rootScope.$on( "$routeChangeStart", function(event, next, current) {
if(!CookieService.getLoginStatus() && $location.path() != '/register' && $location.path() != '/login') {
$location.path("/");
$rootScope.$broadcast('notloggedin');
}
if(CookieService.getLoginStatus() && $location.path() == '/login') {
$location.path("/home");
}
var permission = next.$$route.permission;
if(_.isString(permission) && !PermissionsService.hasPermission(permission))
$location.path('/unauthorized');
});
// Adds Header and Footer on route change success
$rootScope.$on('$routeChangeSuccess', function (ev, current, prev) {
$rootScope.flexyLayout = function(partialName) { return current.$$route[partialName] };
});
});
CookieService
angular.module('myApp')
.factory('CookieService', function ($rootScope,$http,$cookieStore) {
var cookie = {
data: {
login: false,
user: undefined
},
saveLoginData: function(user) {
cookie.data.login = true;
cookie.data.user = user;
$cookieStore.put('__iQngcon',cookie.data);
},
deleteLoginData: function() {
cookie.data.login = false;
cookie.data.user = undefined;
$cookieStore.put('__iQngcon',cookie.data);
},
getLoginStatus: function() {
if($cookieStore.get('__iQngcon') === undefined)
return cookie.data.login;
return $cookieStore.get('__iQngcon').login;
},
getUserData: function() {
return $cookieStore.get('__iQngcon').user;
}
};
return cookie;
});
It seems like the permissions data are lost on page refresh. Is there any other way i can solve the problem? Or is there any problem with the code??
when i refresh the page manually, the admin page is redirecting to the
'/unauthorized' page
Isn't that expected behavior? If you reload the page; then all UI state is lost; it is just like shutting down the app and starting from scratch.
It seems like the permissions data are lost on page refresh. Is there
any other way i can solve the problem? Or is there any problem with
the code??
If you want to be able to retain UI state after a page reload, you'll have to retain the Login information somehow, such as in a browser cookies. When the app loads; check for that cookie value. If it exists, you can load the user info from the database, essentially mirroring a login.
I'd be cautious about storing actual user credentials in a cookie without some type of encryption.
One approach I've used is to store a unique user key which can be sent to the DB to load user info. Sometimes this may be a UUID associated with the user, Avoid using an auto-incrementing primary key because that is easy to change to get access to a different user's account.
Well had to think for some time and made the following change for it to work. It may not be the best practice but ya worked for me. Would appreciate if anyone suggested me a better solution in the comments if found.
myApp.run(function($rootScope, $location, $window, $route, $cookieStore, CookieService, PermissionsService) {
var permChanged = false;
PermissionsService.getPermissions().then(function(permissionList){
PermissionsService.setPermissions(permissionList);
});
// Check login status on route change start
$rootScope.$on( "$routeChangeStart", function(event, next, current) {
console.log('$routeChangeStart');
if(!CookieService.getLoginStatus() && $location.path() != '/register' && $location.path() != '/login') {
$location.path("/");
$rootScope.$broadcast('notloggedin');
}
if(CookieService.getLoginStatus() && $location.path() == '/login') {
$location.path("/home");
}
$rootScope.$on('permissionsChanged', function (ev, current, prev) {
permChanged = true;
});
if(CookieService.getLoginStatus() && permChanged) {
var permission = next.$$route.permission;
if(_.isString(permission) && !PermissionsService.hasPermission(permission))
$location.path('/unauthorized');
}
});
// Adds Header and Footer on route change success
$rootScope.$on('$routeChangeSuccess', function (ev, current, prev) {
$rootScope.flexyLayout = function(partialName) { return current.$$route[partialName] };
});
});
What i did was wait for the permissions to be set and then use the permissionChanged broadcast to set a permChanged variable to true and then combined with if user loggedin status and permchanged had to check the permissions if had
$rootScope.$on('permissionsChanged', function (ev, current, prev) {
permChanged = true;
});
if(CookieService.getLoginStatus() && permChanged) {
var permission = next.$$route.permission;
if(_.isString(permission) && !PermissionsService.hasPermission(permission))
$location.path('/unauthorized');
}

Can angular's ngTouch library be used to detect a long click (touch/hold/release in place) event?

My AngularJS app needs to be able to detect both the start and stop of a touch event (without swiping). For example, I need to execute some logic when the touch begins (user presses down their finger and holds), and then execute different logic when the same touch ends (user removes their finger). I am looking at implementing ngTouch for this task, but the documentation for the ngTouch.ngClick directive only mentions firing the event on tap. The ngTouch.$swipe service can detect start and stop of the touch event, but only if the user actually swiped (moved their finger horizontally or vertically) while touching. Anyone have any ideas? Will I need to just write my own directive?
Update 11/25/14:
The monospaced angular-hammer library is outdated right now, so the Hammer.js team recommend to use the ryan mullins version, which is built over hammer v2.0+.
I dug into ngTouch and from what I can tell it has no support for anything other than tap and swipe (as of the time of this writing, version 1.2.0). I opted to go with a more mature multi-touch library (hammer.js) and a well tested and maintained angular module (angular-hammer) which exposes all of hammer.js's multi-touch features as attribute directives.
https://github.com/monospaced/angular-hammer
It is a good implementation:
// pressableElement: pressable-element
.directive('pressableElement', function ($timeout) {
return {
restrict: 'A',
link: function ($scope, $elm, $attrs) {
$elm.bind('mousedown', function (evt) {
$scope.longPress = true;
$scope.click = true;
// onLongPress: on-long-press
$timeout(function () {
$scope.click = false;
if ($scope.longPress && $attrs.onLongPress) {
$scope.$apply(function () {
$scope.$eval($attrs.onLongPress, { $event: evt });
});
}
}, $attrs.timeOut || 600); // timeOut: time-out
// onTouch: on-touch
if ($attrs.onTouch) {
$scope.$apply(function () {
$scope.$eval($attrs.onTouch, { $event: evt });
});
}
});
$elm.bind('mouseup', function (evt) {
$scope.longPress = false;
// onTouchEnd: on-touch-end
if ($attrs.onTouchEnd) {
$scope.$apply(function () {
$scope.$eval($attrs.onTouchEnd, { $event: evt });
});
}
// onClick: on-click
if ($scope.click && $attrs.onClick) {
$scope.$apply(function () {
$scope.$eval($attrs.onClick, { $event: evt });
});
}
});
}
};
})
Usage example:
<div pressable-element
ng-repeat="item in list"
on-long-press="itemOnLongPress(item.id)"
on-touch="itemOnTouch(item.id)"
on-touch-end="itemOnTouchEnd(item.id)"
on-click="itemOnClick(item.id)"
time-out="600"
>{{item}}</div>
var app = angular.module('pressableTest', [])
.controller('MyCtrl', function($scope) {
$scope.result = '-';
$scope.list = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
{ id: 4 },
{ id: 5 },
{ id: 6 },
{ id: 7 }
];
$scope.itemOnLongPress = function (id) { $scope.result = 'itemOnLongPress: ' + id; };
$scope.itemOnTouch = function (id) { $scope.result = 'itemOnTouch: ' + id; };
$scope.itemOnTouchEnd = function (id) { $scope.result = 'itemOnTouchEnd: ' + id; };
$scope.itemOnClick = function (id) { $scope.result = 'itemOnClick: ' + id; };
})
.directive('pressableElement', function ($timeout) {
return {
restrict: 'A',
link: function ($scope, $elm, $attrs) {
$elm.bind('mousedown', function (evt) {
$scope.longPress = true;
$scope.click = true;
$scope._pressed = null;
// onLongPress: on-long-press
$scope._pressed = $timeout(function () {
$scope.click = false;
if ($scope.longPress && $attrs.onLongPress) {
$scope.$apply(function () {
$scope.$eval($attrs.onLongPress, { $event: evt });
});
}
}, $attrs.timeOut || 600); // timeOut: time-out
// onTouch: on-touch
if ($attrs.onTouch) {
$scope.$apply(function () {
$scope.$eval($attrs.onTouch, { $event: evt });
});
}
});
$elm.bind('mouseup', function (evt) {
$scope.longPress = false;
$timeout.cancel($scope._pressed);
// onTouchEnd: on-touch-end
if ($attrs.onTouchEnd) {
$scope.$apply(function () {
$scope.$eval($attrs.onTouchEnd, { $event: evt });
});
}
// onClick: on-click
if ($scope.click && $attrs.onClick) {
$scope.$apply(function () {
$scope.$eval($attrs.onClick, { $event: evt });
});
}
});
}
};
})
li {
cursor: pointer;
margin: 0 0 5px 0;
background: #FFAAAA;
}
<div ng-app="pressableTest">
<div ng-controller="MyCtrl">
<ul>
<li ng-repeat="item in list"
pressable-element
on-long-press="itemOnLongPress(item.id)"
on-touch="itemOnTouch(item.id)"
on-touch-end="itemOnTouchEnd(item.id)"
on-click="itemOnClick(item.id)"
time-out="600"
>{{item.id}}</li>
</ul>
<h3>{{result}}</h3>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
Based on: https://gist.github.com/BobNisco/9885852
The monospaced angular-hammer library is outdated right now, so the Hammer.js team recommend to use the ryan mullins version, which is built over hammer v2.0+.

Resources