I setup a single-page app with AngularJS and used Skrollr on the home page. I have not used Skrollr before, so I wanted to check with others about the proper 'Angular' way to integrate it with AngularJS, before I start to dive into using more features
What I did in Angular was create a service to load the script onto the page and call skrollr.init() and return it as a promise. Then injected the service to a directive which calls refresh as needed. If a page needs skrollr, I can use this directive on the page somewhere and set the data attributes per skrollr documentation.
ie this works:
<div class="main" skrollr-tag>
<div data-0="color:rgb(0,0,255);" data-90="color:rgb(255,0,0);">WOOOT</div>
</div>
It seems elements added to DOM later on, such as by ngRepeat, skrollr doesn't know about, so I need to include this directive on all elements generated dynamically w/ skrollr data attributes for it to work.
<div class="main" skrollr-tag>
<!-- this heading will animate all the time -->
<h1 data-0="opacity: 1" data-50="opacity: 0">WOOT!</h1>
<div data-ng-repeat="item in items" class="had-to-add-skrollr-again" skrollr-tag>
<!-- skrollr animates this only on page refresh, unless skrollr-tag duplicated above -->
<div data-0="color:rgb(0,0,255);" data-90="color:rgb(255,0,0);">{{item.name}}</div>
</div>
</div>
So, to recap, skrollr is 'aware' of these dynamic elements on the 1st load after refresh, but then after navigating to a different route then back again they no longer get animated unless you refresh page again, or add skrollr-tag directive to the dynamic elements themselves.
Is this a bad idea for performance reasons to include this directive on each dynamic element needing skrollr, thus calling refresh() again for each one? Ideally solution would be load skrollr-tag directive once per page, and it's aware of dynamic elements. I am open to any completely different cleaner more simple way to integrate skrollr to angular.
The angular code is here:
service:
.service('skrollrService', ['$document', '$q', '$rootScope', '$window',
function($document, $q, $rootScope, $window){
var defer = $q.defer();
function onScriptLoad() {
// Load client in the browser
$rootScope.$apply(function() {
var s = $window.skrollr.init({
forceHeight: false
});
defer.resolve(s);
});
}
// Create a script tag with skrollr as the source
// and call our onScriptLoad callback when it
// has been loaded
var scriptTag = $document[0].createElement('script');
scriptTag.type = 'text/javascript';
scriptTag.async = true;
scriptTag.src = 'lib/skrollr/dist/skrollr.min.js';
scriptTag.onreadystatechange = function () {
if (this.readyState === 'complete') onScriptLoad();
};
scriptTag.onload = onScriptLoad;
var s = $document[0].getElementsByTagName('body')[0];
s.appendChild(scriptTag);
return {
skrollr: function() { return defer.promise; }
};
}
]);
directive:
.directive('skrollrTag', [ 'skrollrService',
function(skrollrService){
return {
link: function(){
skrollrService.skrollr().then(function(skrollr){
skrollr.refresh();
});
}
};
}
])
I'm currently having the same issue trying to integrate Skrollr into AngularJS.
The problem is basically this directive, it works when the page loads for the first time but then nothing is happening even though its being called when new html elements are created - or when you change views.
.directive('skrollrTag', [ 'skrollrService',
function(skrollrService){
return {
link: function(){
skrollrService.skrollr().then(function(skrollr){
skrollr.refresh();
});
}
};
}
])
I think the reason is the way angularJS injects new html content. By the time skrollr does "refresh" its not yet rendered or some sort of conflict.
Maybe the only solution is to modify skrollr script.
This answer should help: AngularJS watch DOM change. Try updating your directive to watch for child node changes. This way, it'll automatically refresh whenever new nodes are added.
.directive('skrollrTag', [ 'skrollrService',
function(skrollrService){
return {
link: function(scope, element, attrs){
skrollrService.skrollr().then(function(skrollr){
skrollr.refresh();
});
//This will watch for any new elements being added as children to whatever element this directive is placed on. If new elements are added, Skrollr will be refreshed (pulling in the new elements
scope.$watch(
function () { return element[0].childNodes.length; },
function (newValue, oldValue) {
if (newValue !== oldValue) {
skrollrService.skrollr().then(function(skrollr){
skrollr.refresh();
});
}
});
}
};
}
]);
EDIT
Updated to account for the promise you're using (that would already be resolved), and added a comment to further explain the solution.
i made a directive for skrollr
(function () {
'use strict';
angular.module('myApp', [])
.directive('skrollr', function () {
var obj = {
link: function () {
/* jshint ignore:start */
skrollr.init().refresh();
/* jshint ignore:end */
}
};
return obj;
});
})();
and use like this
<div skrollr data-0="background-color:rgb(0,0,255);" data-500="background-color:rgb(255,0,0);">WOOOT</div>
Related
I'm fairly new to Angular and am having trouble adding directives to watch the size of elements in response to changes in the page layout.
I'm using a custom directive based on some good examples from a similar question: AngularJS: Better way to watch for height change , but I've found the watch is seeing the dimensions of the target element before the panel is resized - I assume because the directive runs before the browser renders the changes.
I've put together a JSFiddle showing the problem. http://jsfiddle.net/aW9dU/139/ . On hitting the 'toggle' button it should update the displayed header dimensions to match the new state of the page. Currently it is showing the previous height of the element. If the $timeout is uncommented then the update loop is triggered.
HTML:
<div ng-app='myApp' ng-controller='myCtrl'>
<div id="header" el-size='headerSize'>
<button id='panelToggle' ng-click="panelToggle = !panelToggle">toggle</button>
<div id='panel' ng-show='panelToggle'>Panel content. Toggle me!</div>
</div>
<div>Header dimensions: {{headerSize}}</div>
</div>
JavaScript:
angular.module('myApp', [])
.directive('elSize', ['$timeout', function ($timeout) {
return {
link: function (scope, elem, attrs) {
scope.$watch(function (newVal, oldVal) {
//$timeout(function () {
console.log(elem[0].offsetWidth, elem[0].offsetHeight);
scope[attrs.elSize] = {
width: elem[0].offsetWidth,
height: elem[0].offsetHeight
};
//});
});
}
}
}])
.controller('myCtrl', function ($scope) {
$scope.panelToggle = true;
});
The suggested solution I've found is to use a $timeout to postpone evaluation until after the page has re-rendered but doing this triggers an infinite update loop which can be seen in the console after uncommenting the $timeout line.
What have I missed?
Update:
As pointed out below, this works in Angular 1.2.x but breaks in 1.3 (1.3.0, 1.3.5, 1.3.8). The 1.2 to 1.3 migration guide doesn't appear to mention anything that might explain the change.
I've got a situation where I want to trigger an Angular-based widget iff a query arg is present in the current URL. My approach is to check (on page load) if the arg is present, and if so, append a bit of markup to the <body> that will get the app rolling:
var exampleApp = angular.module('exampleApp', []);
//init
$(function() {
var widgetMarkup = "<div ng-app=\"exampleApp\"><div ng-include=\"'/partials/widget.html'\"></div></div>";
if (window.location.href.indexOf('?arg') > -1) {
$(document.body).append(widgetMarkup);
}
});
PROBLEM: The markup is added to the DOM, but has no effect. But if I add the same markup to the raw HTML file, the app is picked up and the partial is rendered. Do I need to tell Angular to re-scan the page for directives?
what you want is angular.bootstrap. Call it when your DOM is ready and everything has been appended.
angular.bootstrap(document, ['exampleApp']);
This will start angular on the document, as if it had ng-app on it. You can bootstrap it to whatever element you want though.
You have to manually bootstrap your app.
var exampleApp = angular.module('exampleApp', []);
if (window.location.href.indexOf('?arg') > -1) {
angular.element(document).ready(function() {
angular.bootstrap(document, ['exampleApp']);
});
}
More info here: angular docs
I am trying to apply animations to ng-view (routing) depending of the views involved.
For example, from View1 to View2 I need the View1 leaving through the left side and View1 entering from the right side. Otherwise, from View2 to View1 I need View2 leaving through the right side and View1 entering from the left side.
But I have also situations where I need apply different animations to both views, for example, View1 leaving fading out and View2 entering scaling up.
What I am doing is using a scope associated variable as class in the ng-view:
<div ng-view class="{{transition}}"></div>
This variable is set in each route change with something like this in each controller:
$scope.transition=Global.transition;
$rootScope.$on("$routeChangeStart",function (event, current, previous) {
// Here I get the leaving view and the entering view and the kind of transition is selected
...
$scope.transition=selectedLeavingTransition; // Set the transition for the leaving view
Global.transition=selectedEnteringTransition; // Set the transition for the entering view
});
Global is a service to set the transition variable for the entering scope from the leaving scope.
This way, when a route change is detected, the current ng-view is set with the class associated to selectedLeavingTransition, and the entering ng-view is set with the class associated to selectedEnteringTransition.
For example, if the route change was from View1 to View2 the ng-views during the animation could be:
<div ng-view class="fadeOut ng-animate ng-leave ng-leave-active"></div>
<div ng-view class="scaleUp ng-animate ng-enter ng-enter-active"></div>
The CSS in this case could be:
fadeOut.ng-leave {animation:1s fadeOut;}
scaleUp.ng-enter {animation:1s scaleUp;}
Though it works, I am wondering if there is a simpler way to do it as it seems a little mess.
An alternative solution that doesn't require much code is to define your animations on your routes:
$routeProvider.when('/view1', {
templateUrl: 'view1.html',
controller: 'View1Controller',
animations: {
enter: 'enter-left',
leave: 'leave-left'
}
});
Then use a directive to retrieve the current route's animations and add them to the element:
app.directive('viewAnimations', function ($route) {
return {
restrict: 'A',
link: function (scope, element) {
var animations = $route.current.animations;
if (!animations) return;
if (animations.enter) element.addClass(animations.enter);
if (animations.leave) element.addClass(animations.leave);
}
};
});
And put the directive on the element that contains the ngView directive:
<body ng-view view-animations></body>
Demo: http://plnkr.co/edit/Y3ExDyiPIJwvVKO4njBT?p=preview
Edit: New solution.
To set animations during run-time I would use a service just like you are doing, but a directive to apply them.
Very basic example of service:
app.factory('viewAnimationsService', function ($rootScope) {
var enterAnimation;
var getEnterAnimation = function () {
return enterAnimation;
};
var setEnterAnimation = function (animation) {
enterAnimation = animation;
};
var setLeaveAnimation = function (animation) {
$rootScope.$emit('event:newLeaveAnimation', animation);
};
return {
getEnterAnimation: getEnterAnimation,
setEnterAnimation: setEnterAnimation,
setLeaveAnimation: setLeaveAnimation
};
});
And the directive:
app.directive('viewAnimations', function (viewAnimationsService, $rootScope) {
return {
restrict: 'A',
link: function (scope, element) {
var previousEnter, previousLeave;
var enterAnimation = viewAnimationsService.getEnterAnimation();
if (enterAnimation) {
if (previousEnter) element.removeClass(previousEnter);
previousEnter = enterAnimation;
element.addClass(enterAnimation);
}
$rootScope.$on('event:newLeaveAnimation', function (event, leaveAnimation) {
if (previousLeave) element.removeClass(previousLeave);
previousLeave = leaveAnimation;
element.addClass(leaveAnimation);
});
}
};
});
Demo: http://plnkr.co/edit/DuQXaN2eYgtZ725Zqzeu?p=preview
I have been working on it and I have a neater solution, what I was doing had some problems. Now I am just using the $routeChangeStart at root scope and selecting there the leaving and enter transitions.
The only problem I have is that on the routeChangeStart event I can't modify the leaving view so I can't establish the leaving transition to the ngView element class attribute. I had to set it directly through the DOM (I know that is bad practice).
I tried to modify the leaving view through a shared service, the root scope and $apply() but none of them worked. Once the routeChangeStart event is launched the view seems static.
Here is a working example: jsfiddle.net/isidrogarcia/Fs5NZ
I have a project that uses legacy code in .NET and WebForms. The legacy code uses several Update Panels.
My hope is to use AngularJS without impacting the legacy code base.
<div ng-app="myApp">
<NS:TheUserControl ID="TheUserControl1" runat="Server" />
</div>
Here is the javascript:
var app = angular.module("myApp", []);
app.run(function ($rootScope, $compile) {
function insertDirective() {
jQuery(targetElementSelector).attr("my-directive", "");
}
insertDirective();
var mgr = Sys.WebForms.PageRequestManager.getInstance();
mgr.add_endRequest(function (sender, args) {
insertDirective();
$compile(jQuery(targetElementSelector))($rootScope);
});
});
The code above adds an attribute to an HTML element inside NS:TheUserControl, and the attribute specifies a directive (see directive below).
Then, these steps are used when the update panel changes:
Detect the change using Sys.WebForms.PageRequestManager (EndRequest listener)
Re-add the directive using javascript
Run $compile on the newly-inserted directive
Here is the directive:
app.directive("myDirective", function () {
return {
template: "<span ng-repeat='item in items'>{{item}}</span>"
+ "<span ng-transclude></span>"
, transclude: true,
, controller: function ($scope) {
$scope.items = ["a", "b", "c"];
}
};
});
This almost works... except...
Problem 1:
AngularJS creates a scope associated with each instance of a directive. When the update panel inside NS:TheUserControl changes the DOM, the previous scopes are still present. I can see this in Batarang (a Chrome developer tool for AngularJS).
Problem 2:
For a brief moment after the update panels change, I get:
abc <-- Initial page load
abcabc <-- Subsequent update panel changes
abcabcabc
abcabcabcabc
Then, a moment later after each update panel change, the content jumps back to the correct:
abc
Questions:
So, how do I either:
Remove the orphaned scopes?
Or, incorporate AngularJS directives in a way that plays nice with the update panels?
http://plnkr.co/edit/GRVZl35D1cuWz1kzXZfF?p=preview
In the custom fancybox (aka lightbox, a dialog) I show contents with interpolated values.
in the service, in the "open" fancybox method, i do
open: function(html, $scope) {
var el = angular.element(html);
$compile(el)($scope); // how to know when the $compile is over?
$.fancybox.open(el); // the uncompiled is shown before the compiled
}
The problem is that the content in the dialog is loaded before the end of the $compile, so after less than a second i got a refresh of the dialog content with the values.
The plunkr works, but i want to avoid that the "el" is shown before it gets totally compiled: i want to show it only after the $compile has finished his job
Is there a way to know when the $compile it's over so i'll show the content on fancybox only after that?
You can't inject $scope into services, there is nothing like a singleton $scope.
So instead of $compile(el)($scope); try:
var compiledEl = $compile(el);
....
The $compile returns compiled data.
as a side note
I would provide service to directive and compile it into directive instead. I think it's the right way.
I've had the same problem with the ngDialog modals and popup provider. I needed to position the dialog based on its height. But the height depended on the compiled DOM.
I eventually found a solution using $timeout, like described in that post: http://blog.brunoscopelliti.com/run-a-directive-after-the-dom-has-finished-rendering/
For your code, it would give something like that:
open: function(html, $scope) {
var el = angular.element(html);
$compile(el)($scope);
$timeout(function() {
$.fancybox.open(el);
}, 0);
}