← Blog
AngularJS directive for slide-down animation with lazy rendering
- Angular
- JavaScript
- Programming
In this blog post I am going to talk about a directive I created earlier that is available in my ng-slide-down GitHub repository.
In the company I am working for we have a lot of widgets that are expanded via jQuery “slideToggle” function, thus when I make new components I have to adhere to existing style. As you have probably guessed, new parts are written in Angular.
Another requirement I had to satisfy is that we can have a lot of these widgets on a single page, and rendering them all in Angular would be wildly inefficient, so it had to only render HTML if the widget was expanded.
In order to use the resulting directive, all you need to do is add the directive ng-slide-down to your HTML element and pass a variable that is going to control it:
<div ng-slide-down="slideDown"></div>
Code
So let’s start writing the code (fiddle).
"use strict"
angular.module("ng-slide-down", []).directive "ngSlideDown", ($timeout )->
link = (scope, element, attrs) ->
duration = attrs.duration || 1 # check if attribute 'duration' was passed
element.css { # css animation
overflow: "hidden"
transitionProperty: "height"
transitionDuration: "#{duration}s"
transitionTimingFunction: "ease-in-out"
}
# because of the way we do animation we can't get height of the element itself,
# so we have to get height of it's children
getHeight = ()->
height = 0
children = element.children()
for c in children
height += c.clientHeight
"#{height}px"
show = ()->
element.css('height', getHeight())
hide = ()->
element.css('height', '0px')
# watch for changes in passed variable
scope.$watch 'expanded', (value, oldValue)->
if value
$timeout show # show in the next angular cycle
else
$timeout hide # hide in the next cycle
return {
restrict: 'A'
scope: {
expanded: '=ngSlideDown' # bind variable 'expanded' to attribute 'ng-slide-down'
}
transclude: true
link: link
template: "<div ng-transclude></div>" # ng-transclude is replaced with passed HTML
}
Lazy rendering
In the next step we are going to add an option to stop Angular from rendering HTML unless the element is expanded.
<div ng-slide-down="slideDown" lazy-render></div>
"use strict"
angular.module("ng-slide-down", []).directive "ngSlideDown", ($timeout )->
# this function is going to generate template
getTemplate = (tElement, tAttrs)->
if tAttrs.lazyRender != undefined
# ng-if directive stops angular from generating HTML
"<div ng-if='lazyRender' ng-transclude></div>"
else
"<div ng-transclude></div>"
link = (scope, element, attrs) ->
duration = attrs.duration || 1
# check if "lazy-render" attribute was passed
lazyRender = attrs.lazyRender != undefined
if lazyRender
scope.lazyRender = scope.expanded
element.css {
overflow: "hidden"
transitionProperty: "height"
transitionDuration: "#{duration}s"
transitionTimingFunction: "ease-in-out"
}
getHeight = ()->
height = 0
children = element.children()
for c in children
height += c.clientHeight
"#{height}px"
show = ()->
scope.lazyRender = true if lazyRender
element.css('height', getHeight())
hide = ()->
element.css('height', '0px')
if lazyRender
$timeout ()->
scope.lazyRender = false if lazyRender
, duration*1000
scope.$watch 'expanded', (value, oldValue)->
if value
$timeout show
else
$timeout hide
return {
restrict: 'A'
scope: {
expanded: '=ngSlideDown'
}
transclude: true
link: link
# use function to generate template
template: (tElement, tAttrs)-> getTemplate(tElement, tAttrs)
}
Final steps
Now we need to take care of a few extra details. Like, for example, what should we do if the user closes the widget before it’s been fully expanded? In this case we should cancel promise. Another case we need to take care of is change in children’s height. The final code looks as follows
"use strict"
angular.module("ng-slide-down", []).directive "ngSlideDown", ($timeout )->
getTemplate = (tElement, tAttrs)->
if tAttrs.lazyRender != undefined
"<div ng-if='lazyRender' ng-transclude></div>"
else
"<div ng-transclude></div>"
link = (scope, element, attrs) ->
duration = attrs.duration || 1
lazyRender = attrs.lazyRender != undefined
if lazyRender
scope.lazyRender = scope.expanded
closePromise = null
element.css {
overflow: "hidden"
transitionProperty: "height"
transitionDuration: "#{duration}s"
transitionTimingFunction: "ease-in-out"
}
getHeight = (passedScope)->
height = 0
children = element.children()
for c in children
height += c.clientHeight
"#{height}px"
show = ()->
# If element was closed before animation has completed don't stop rendering
$timeout.cancel(closePromise) if closePromise
scope.lazyRender = true if lazyRender
element.css('height', getHeight())
hide = ()->
element.css('height', '0px')
if lazyRender
closePromise = $timeout ()-> # save timeout promise
scope.lazyRender = false if lazyRender
, duration*1000
scope.$watch 'expanded', (value, oldValue)->
if value
$timeout show
else
$timeout hide
# watch for changes in children's height and adjust parent's height accordingly
scope.$watch getHeight, (value, oldValue)->
if scope.expanded && value!=oldValue
$timeout show
return {
restrict: 'A'
scope: {
expanded: '=ngSlideDown'
}
transclude: true
link: link
template: (tElement, tAttrs)-> getTemplate(tElement, tAttrs)
}
P.S.
Make sure to update your AngularJS, because older versions contain a bug (fixed) in the way ng-transclude and ng-if directives interact.
Be careful in general when you are using ng-if, because it creates a new scope.