implementing external links to a page with angular-scroll and ng-route - angularjs

So this is is an angularjs app.
I have implemented this angular-scroll api :https://github.com/oblador/angular-scroll, to show a catalog of products, where the content is loaded from db. this catalog has all the subcategories (with its products) and every subcategory has an anchor identified like: anchor+categoryId.
So from the menu , i click a category and it scroll nicely to the correct section.
The problem arise when I need to create some links from other pages of the site, to go to an specific section category inside the catalog. Because I have ng-route, i need to create a new url to redirect to the catalog, and there capture when the content is loaded to do the scroll to the required category.
BUT I have a directive associated with the route of the catalog, that looks for the partials depending on the domain of the client, so to show the correct template i have to use an $http , get the content and replace it in my directive.
Because that I dont know how i can know when the content of the directive is ready to make the call to the scroll... better show some code here :
this is the route that is receiving the call
$routeProvider.
when('/products/category/:categoryId/page/:page/anchor/:anchorId?', {
template:'<product-display-view></product-display-view>',
controller: 'ProductListCtrl',
access: {
authorizedRoles: [USER_ROLES.all]
},
resolve: {
wait : 'waitForIt',
prefetchDataProducts: ['waitForIt','$route','SearchService',
function(waitForIt,$route,SearchService) {
return waitForIt.then(function() {
return SearchService.getProducts($route.current.params.categoryId,$route.current.params.page);
});
}],
prefetchDataCategories:['waitForIt','CategoryService',
function(waitForIt,CategoryService) {
return waitForIt.then(function() {
return CategoryService.getCategories();
});
}]
}
}).
this is the directive product-display
productDirectives.directive('productDisplayView',['$rootScope','$compile','$http','$templateCache' ,'$document',
function($rootScope,$compile, $http, $templateCache,$document){
return {
restrict: 'E',
link: function (scope, element, attrs) {
var templateUrl = 'users/catwizardAngularCore/app/partials/themes/' + scope.app.theme.themeName + '/partials/product-display.html';
$http.get(templateUrl, {cache: $templateCache})
.success(function (templateContent) {
element.replaceWith($compile(templateContent)(scope));
});
/* this doesn't work because the someElement doesn't exist*/
var newHash = 'anchor' + scope.anchorId;
var someElement = angular.element(document.getElementById(newHash));
angular.element(someElement).ready(function () {
$document.scrollToElement(someElement, 200, 2000);
});
}
}]);

There is a duplicate question with the correct answer, but it has not been accepted yet so I am copying the answer here.
The $anchorScroll has to occur after the page has been rendered,
otherwise the anchor doesn't exist. This can be achieved using
$timeout().
$timeout(function() {
$anchorScroll('myAnchor');
});
Credits to Tony

Related

ngDialog not programmatically closing on IE but works on Chrome

I have a site that allows a person to import some data. They click a button, and the file select opens and they select a file. When they select a file I open a dialog that they can't close that tells them their data is being imported. Once I get the call back from the api call to import the file, I then close that dialog and open a new one that gives the status of the import.
On Chrome the "please wait while importing" dialog closes as expected. On IE it doesn't. If you use IE 11 it should happen in the following fiddle:
http://jsfiddle.net/og6qsxdw/
You see a ghost like outline of the dialog go up and fade away like its trying to close but the dialog and overlay still remain.
<div ng-controller="MyCtrl">
<input type="file" ng-simple-upload web-api-url="http://www.fakeresponse.com/api/?sleep=5" select-fn="importFileSelected" callback-fn="importDataComplete" />
<script type="text/ng-template" id="templateId">
<div>
Getting Data
</div>
</script>
</div>
JavaScript/AngularJS code:
var myApp = angular.module('myApp', ['ngDialog', 'ngSimpleUpload']);
function MyCtrl($scope, $http, ngDialog) {
$scope.importDataComplete = function() {
$scope.dlg.close();
}
$scope.importFileSelected = function() {
$scope.dlg = ngDialog.open({
template: 'templateId',
className: 'ngdialog-theme-default',
closeByDocument: false,
showClose: false
});
}
}
angular.module('ngSimpleUpload', [])
.directive('ngSimpleUpload', [function() {
return {
scope: {
webApiUrl: '#',
callbackFn: '=',
selectFn: '=',
buttonId: '#'
},
link: function(scope, element, attrs) {
// if button id value exists
if (scope.buttonId) {
$('#' + scope.buttonId).on('click', function() {
// retrieves files from file input
var files = element[0].files;
// will not fire until file(s) are selected
if (files.length == 0) {
console.log('No files detected.');
return false;
}
Upload(files);
});
} else {
// original code, trigger upload on change
element.on('change', function(evt) {
var files = evt.__files_ || (evt.target && evt.target.files);
Upload(files);
// removes file(s) from input
$(this).val('');
});
}
function Upload(files) {
var fd = new FormData();
angular.forEach(files, function(v, k) {
fd.append('file', files[k]);
});
// this tell us the user clicked open instead of cancel so we can start our overlay
scope.selectFn();
return $.ajax({
type: 'GET',
url: scope.webApiUrl,
async: true,
cache: false,
contentType: false,
processData: false
}).done(function(d) {
// callback function in the controller
scope.callbackFn(d);
}).fail(function(x) {
console.log(x);
});
}
}
}
}]);
Alright, so here's the deal. In IE, when you open the dialog, two instances are instantiated. When the upload completes, you have a reference to close the most recent dialog, but one existed milliseconds before as well.
I had originally thought at quick glance that this was just an ugly IE bug, and you had to "keep track" of the instances, however, I failed to take note of jQuery's involvment in your link function. Thusly, my initial solution was a hack/workaround, but better can be done.
It seems that the mixture of the two libraries is the culprit, where Angular and jQuery are not communicating properly. I've inserted a reference below to a ticket that discusses jQuery events with Angular.
jQuery and AngularJS: Bind Events to Changing DOM
Solution
My suggestion, as always in these cases, is not to leverage jQuery on top of Angular. It adds an additional layer of confusion, and requires you to be prudent about maintaining proper coupling between the two (in circumstances such as this).
I have provided a solution, where I clean up your link function. It uses a lot of your existing code, but with the absence of the jQuery bits. It seems to work just fine for me in both Chrome and IE now.
http://plnkr.co/edit/6Z4Rzg1Zm3w5rYyqQqmg?p=preview
link: function(scope, element, attrs) {
console.warn("Element is", element);
// original code, trigger upload on change
element.on('change', function(evt) {
var files = evt.__files_ || (evt.target && evt.target.files);
Upload(files);
});
function Upload(files) {
var fd = new FormData();
angular.forEach(files, function(v, k) {
fd.append('file', files[k]);
console.log("File loaded");
});
// this tell us the user clicked open instead of cancel so we can start our overlay
scope.selectFn();
$http({
url: scope.webApiUrl,
method: "GET",
cache: true
}).success(function(d, status, headers, config) {
scope.callbackFn(d);
}).error(function(data, status, headers, config) {
console.warn("Request failed...");
});
}
}

implementing advertisement directive in angular js

I am a newbie angular. I am working with an angular application. My client requirement is to add an advertisement in some pages. I have the data like position for advertisement, advertisement image etc. I would like to implement this functionality as creating an angular directive. So I can call this directive as tag in my pages. So can anyone tell how to write this directive?
Please see the code below:
zentieraDirectives.directive('advertisement',function($http,$rootScope){
$http.post($rootScope.STATIC_URL + 'admins/getadvertisement').success(function(response){
console.log("success advertisement");
return {
template: 'Name: <img src="https://angularjs.org/img/AngularJS-small.png" /> <span ng-click="closeAdvertisement()">X</span>'
};
}) .error(function(err){
console.log("Error"+err);
});
});
When I check on console, I got "success advertisement", but not returned the template. I checked this on another way
app.directive('advertisement', function() {
return {
template: 'Name: {{advertisement}} <img src="https://angularjs.org/img/AngularJS-small.png" /> <span ng-click="closeAdvertisement()">X</span>'
};
});
My need is that, on the success of api call to node, I have to return the template. My view page is
<h1>advertisement</h1>
<advertisement ng-show="advertisementShow"></advertisement>
app.directive('navBannerTop', ['NavBannerServiceTop','$rootScope', function (nbs,$window) {
return {
restrict: 'E',
//scope: true,
scope: {},
template:' <div> <img ng-src="{{zentieraUrl}}/assets/images/adBanner/{{banner_pic_url}}"></div>',
link: function ($scope,$element,$attr,$rootScope) {
var imagePosition=$attr.imageposition;
nbs.getImage(imagePosition).then(function(result){
$scope.banner_pic_url = result.data.banner;
});
}
};
}]);
Finally I have solved the problem. First I create a directive. In that directive, I returned a template. The directive uses a service too.
app.service('NavBannerServiceTop', ['$http', '$q', function ($http, $q,$rootScope) {
var deferred = $q.defer();
var service = {};
service.getImage = function (imagePosition) {
var params={
position:imagePosition
};
$http.post(Config.STATIC_URL + 'users/getadvertisement',params).success(function (data) {
var adImage=data.data.banner;
var advlink=data.data.advlink;
deferred.resolve(data);
}).error(function () {
deferred.reject('some error');
});
return deferred.promise;
};
return service;
}]);
In the service, I take the data from server, ie the image url.This image url is passed to directive. In the view side I call the directive as a custom tag.
I create a little example of this
http://embed.plnkr.co/HD0KGabjWoq7bnwGaN5E/
I create a directive for the advertisement, and the "X" is for close this.
The functionality is in the controller (this not recomend but is for example).
I hope solve your doubt

Angular UI Router Reload Controller on Back Button Press

I have a route that can have numerous optional query parameters:
$stateProvider.state("directory.search", {
url: '/directory/search?name&email',
templateUrl: 'view.html',
controller: 'controller'
When the user fills the form to search the directory a function in the $scope changes the state causing the controller to reload:
$scope.searchDirectory = function () {
$state.go('directory.search', {
name: $scope.Model.Query.name,
email: $scope.Model.Query.email
}, { reload: true });
};
In the controller I have a conditional: if($state.params){return data} dictating whether or not my service will be queried.
This works great except if the user clicks the brower's forward and/or back buttons. In both these cases the state (route) changes the query parameters correctly but does not reload the controller.
From what I've read the controller will be reloaded only if the actual route changes. Is there anyway to make this example work only using query parameters or must I use a changing route?
You should listen to the event for succesful page changes, $locationChangeSuccess. Checkout the docs for it https://docs.angularjs.org/api/ng/service/$location.
There is also a similar question answered on so here How to detect browser back button click event using angular?.
When that event fires you could put whatever logic you run on pageload that you need to run when the controller initializes.
Something like:
$rootScope.$on('$locationChangeSuccess', function() {
$scope.searchDirectory()
});
Or better setup like:
var searchDirectory = function () {
$state.go('directory.search', {
name: $scope.Model.Query.name,
email: $scope.Model.Query.email
}, { reload: true });
$scope.searchDirectory = searchDirectory;
$rootScope.$on('$locationChangeSuccess', function() {
searchDirectory();
});
Using the above, I was able to come up with a solution to my issue:
controller (code snippet):
...var searchDirectory = function (searchParams) {
if (searchParams) {
$scope.Model.Query.name = searchParams.name;
$scope.Model.Query.email = searchParams.email;
}
$state.go('directory.search', {
name: $scope.Model.Query.name,
email: $scope.Model.Query.email,
}, { reload: true });
};...
$rootScope.$on('$locationChangeSuccess', function () {
//used $location.absUrl() to keep track of query string
//could have used $location.path() if just interested in the portion of the route before query string params
$rootScope.actualLocation = $location.absUrl();
});
$rootScope.$watch(function () { return $location.absUrl(); }, function (newLocation, oldLocation) {
//event fires too often?
//before complex conditional was used the state was being changed too many times causing a saturation of my service
if ($rootScope.actualLocation && $rootScope.actualLocation !== oldLocation && oldLocation !== newLocation) {
searchDirectory($location.search());
}
});
$scope.searchDirectory = searchDirectory;
if ($state.params && Object.keys($state.params).length !== 0)
{..call to service getting data...}
This solution feels more like a traditional framework such as .net web forms where the dev has to perform certain actions based on the state of the page. I think it's worth the compromise of having readable query params in the URL.

How to implement path aliases in ui-router

I'm trying to find a way to implement route aliases in Angular.js using ui-router.
Let's say I have a state with the url article/:articleId and I want to have /articles/some-article-title redirected (internally) to /article/76554 without changing the browser location.
Could this be done with ui-router?
I. Doubled state mapping (reuse of controller, views)
NOTE: This is original answer, showing how to solve the issue with two states. Below is another approach, reacting on the comment by Geert
There is a plunker with working example. Say we have this two objects (on a server)
var articles = [
{ID: 1, Title : 'The cool one', Content : 'The content of the cool one',},
{ID: 2, Title : 'The poor one', Content : 'The content of the poor one',},
];
And we would like to use URL as
// by ID
../article/1
../article/2
// by Title
../article/The-cool-one
../article/The-poor-one
Then we can create this state definitions:
// the detail state with ID
.state('articles.detail', {
url: "/{ID:[0-9]{1,8}}",
templateUrl: 'article.tpl.html',
resolve : {
item : function(ArticleSvc, $stateParams) {
return ArticleSvc.getById($stateParams.ID);
},
},
controller:['$scope','$state','item',
function ( $scope , $state , item){
$scope.article = item;
}],
})
// the title state, expecting the Title to be passed
.state('articles.title', {
url: "/{Title:[0-9a-zA-Z\-]*}",
templateUrl: 'article.tpl.html',
resolve : {
item : function(ArticleSvc, $stateParams) {
return ArticleSvc.getByTitle($stateParams.Title);
},
},
controller:['$scope','$state','item',
function ( $scope , $state , item){
$scope.article = item;
}],
})
As we can see, the trick is that the Controller and the Template (templateUrl) are the same. We just ask the Service ArticleSvc to getById() or getByTitle(). Once resolved, we can work with the returned item...
The plunker with more details is here
II. Aliasing, based on native UI-Router functionality
Note: This extension reacts on Geert appropriate comment
So, there is a UI-Router built-in/native way for route aliasing. It is called
$urlRouterProvider - .when()
I created working plunker here. Firstly, we will need only one state defintion, but without any restrictions on ID.
.state('articles.detail', {
//url: "/{ID:[0-9]{1,8}}",
url: "/{ID}",
We also have to implement some mapper, converting title to id (the alias mapper). That would be new Article service method:
var getIdByTitle = function(title){
// some how get the ID for a Title
...
}
And now the power of $urlRouterProvider.when()
$urlRouterProvider.when(/article\/[a-zA-Z\-]+/,
function($match, $state, ArticleSvc) {
// get the Title
var title = $match.input.split('article/')[1];
// get some promise resolving that title
// converting it into ID
var promiseId = ArticleSvc.getIdByTitle(title);
promiseId.then(function(id){
// once ID is recieved... we can go to the detail
$state.go('articles.detail', { ID: id}, {location: false});
})
// essential part! this will instruct UI-Router,
// that we did it... no need to resolve state anymore
return true;
}
);
That's it. This simple implementation skips error, wrong title... handling. But that is expected to be implemented anyhow... Check it here in action

Why will my twitter widget not render if i change the view in angularjs?

Hi and thanks for reading.
I have a angular app im making and ive stumbled on a problem. set up as so
index.html-
<html ng-app="myApp">
...
<div ng-view></div>
<div ng-include="'footer.html'"></div>
...
</html>
I wont bother putting my routes its pretty simple /home is shows the /home/index.html and so on...
/home/index.html (default view when you come to the site)
<div class="responsive-block1">
<div class="tweet-me">
<h1> tweet me </h1>
</div>
<div class="twitter-box">
<twitter-timeline></twitter-timeline>
</div>
twitter timeline directive
directives.directive("twitterTimeline", function() {
return {
restrict: 'E',
template: '<a class="twitter-timeline" href="https://twitter.com/NAME" data-widget-id="XXXXXXXXXXXXXX">Tweets by #NAME</a>',
link: function(scope, element, attrs) {
function run(){
(!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+"://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs"));
console.log('run script');
};
run();
}
};
});
So I have just created a basic twitter directive using the tag from twitter. But when I change the view example to /blog then go back to /home the twitter widget no longer renders at all.
Im also using an $anchorScroll and if i jump to anyway on the page with this the widget also disappears. Any info would be great thanks.
See this post: https://dev.twitter.com/discussions/890
I think that you may be able to get the widget to re-render by calling
twttr.widgets.load().
If you find that this does not work, you will need to wrap this code into $timeout in your controller:
controller('MyCtrl1', ['$scope', '$timeout', function ($scope, $timeout) {
$timeout = twttr.widgets.load();
}])
To build on Sir l33tname's answer:
In services declaration:
angular.module('app.services', []).
service('tweetWidgets', function() {
this.loadAllWidgets = function() {
/* widgets loader code you get when
* declaring you widget with Twitter
* this code is the same for all widgets
* so calling it once will reference whatever
* widgets are active in the current ng-view */
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+"://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");
};
this.destroyAllWidgets = function() {
var $ = function (id) { return document.getElementById(id); };
var twitter = $('twitter-wjs');
if (twitter != null)
twitter.remove();
};
});
Then in controller declarations:
angular.module('app.controllers', []).
controller('view_1_Controller', tweetWidgets) {
// load them all
tweetWidgets.loadAllWidgets();
}).
controller('view_2_Controller', tweetWidgets) {
// now destroy them :>
tweetWidgets.destroyAllWidgets();
});
Now whenever you leave view #1 to go to view #2, your controller for view #2 will remove the widgets associated with view #1 and when you return to view #1 the widgets will be re-instatiated.
The problem is because when Angular switches views the script tag that was originally inserted is not removed from the document. I fixed this on my own website by removing the Twitter script element whenever my Twitter timeline directive is not in the view. See the code below with comments.
function (scope, el, attrs) {
el.bind('$destroy', function() {
var twitterScriptEl = angular.element('#twitter-wjs');
twitterScriptEl.remove();
});
// function provided by Twitter that's been formatted for easier reading
function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0], p = /^http:/.test(d.location) ? 'http' : 'https';
// If the Twitter script element is already on the document this will not get called. On a regular webpage that gets reloaded this isn't a problem. Angular views are loaded dynamically.
if (!d.getElementById(id)) {
js = d.createElement(s);
js.id = id;
js.src = p + "://platform.twitter.com/widgets.js";
js.parentNode.insertBefore(js, fjs);
}
}(document, "script", "twitter-wjs");
}
Basically it's what Loc Nguyen say.
So every time you recreate it you must remove it first.
var $ = function (id) { return document.getElementById(id); };
function loadTwitter() {!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+"://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");}
var twitter = $('twitter-wjs');
twitter.remove();
loadTwitter();
Answer by #b1r3k works without problems :
put this in your controller:
$timeout(function () { twttr.widgets.load(); }, 500);
For those trying to load twttr.widgets.load() inside their controller, you will most likely get an error that twttr is not defined AT SOME POINT in your UX, because the async call to load the twitter script may not be completed by the time you controller instantiates and references twttr.
So I created this TwitterService
.factory('TwitterService', ['$timeout', function ($timeout) {
return {
load: function () {
if (typeof twttr === 'undefined') {
(function() {
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');
})();
} else {
$timeout = twttr.widgets.load();
};
}
}
}])
and then call TwitterService.load() inside the controllers that require your widgets. This worked pretty well. It basically just checks if the twttw object exists and if it does, just reload the script... otherwise just reload the script.
Not sure if this is the best implementation, but it seems like all other solutions have edge cases where it will throw an error. I have yet to find one with this alternative.

Resources