JS 202 - Click to expand

Lets create a common interaction, which is click to expand. We will first learn how to make this interactive with javascript, learn how make height automatically fit the contents.

  1. Starting code

    Although no interations exist yet, pay attention to the `.open` class. It will change `#expandable`'s height when it is added its class list. We will be doing this via javascript so that it can be triggered with a click.

    #expandable
      .content
        p clicky here
    #expandable
      height: 35px
      width: 200px
      background-color: #4C9AFF`
      color: white
      cursor: pointer
      .content
        padding: 5px 10px
      &.open
        height: 200px
  2. Add event listeners

    We select the element we want to make interactive using `document.getElementById.` Then we attach an event listener on the `click` action. Inside the anonymous function passed to the event listener, we can toggle open state using `classList.toggle`.

    var proto = {
      init : function(){
        var expandable = document.getElementById("expandable")
        expandable.addEventListener("click",proto.expand)
      },
      expand : function(){
        var expandable = document.getElementById("expandable")
        expandable.classList.toggle("open")
      }
    }
    
    document.addEventListener('DOMContentLoaded', proto.init)
    
    

    clicky here

  3. Add animations

    Let's add a transition to make this animate smoothly between states. The choice of animation curve will affect the look and feel of the interaction. Variables for Atlassian's style are defined as `$ak-ease` and `$ak-ease-out`. `$ak-ease-out` should be used for elements that were clicked and `$ak-ease` should be used for elements other than what was clicked.

    Can guess which one should be used for this element?

    #expandable
      transition: height 0.3s $ak-ease-out   
      height: 35px
      width: 200px
      background-color: #4C9AFF
      color: white
      cursor: pointer
      .content
        padding: 5px 10px
      &.open
        height: 200px

    clicky here

  4. Adjust height to automatically fit contents

    Now add some stuff to our expandable div like an image and some text. To make sure the contents are hidden while it is closed, add `overflow:hidden`. However the content is not necessarily 200px in height, so we need to set this dynamically with javascript. In particular, we want to set the height of `#expandable` to the height of `.content` div inside of it.

    `.open` will still be used to mark whether `#expandable` is in a open or closed state but we will not be using it to set the height anymore as this will be handled by javascript.

    #expandable
      .content
        p clicky here 
        img(src="https://media.giphy.com/media/111ebonMs90YLu/giphy.gif")
        p good job! 
    #expandable   
      height: 35px
      width: 200px
      background-color: #4C9AFF
      color: white
      cursor: pointer
      overflow:hidden
      transition: height 0.3s $ak-ease-out 
      .content
        padding: 5px 10px
        img
          width: 100%

    Explanation

    In the code snippet below, we get the `.content` element with `querySelector`. Then we determine whether `#expandable` is open or closed using `classList.contains`. If it is open, we set it to be the height of the inside contents with `offsetHeight`. If it is closed, set `#expandable` back to the default height of 35px.

    var proto = {
      init : function(){
        var expandable = document.getElementById("expandable")
        expandable.addEventListener("click",proto.expand)
      },
      expand : function(e){
        var expandable = document.getElementById("expandable")
        var content = document.querySelector("#expandable .content")
        expandable.classList.toggle("open")
        if(expandable.classList.contains("open")){
          expandable.style.height = content.offsetHeight + "px"
        } else {
          expandable.style.height = "35px"
        }
      }
    }

    clicky here

    good job!

  5. Handling multiple expandable elements

    Suppose we want to have multiple expandable elements. We change `#expandable` to `.expandable` since it is no longer unique and rewrite the pug like this:

    .expandable
      .content
        p clicky here 
        img(src="https://media.giphy.com/media/rhWvvwZcRAT4Y/giphy.gif")
    .expandable
      .content
        p clicky here 
        img(src="https://media.giphy.com/media/xBAreNGk5DapO/giphy.gif")
    .expandable
      .content
        p clicky here 
        img(src="https://media.giphy.com/media/26FPCXdkvDbKBbgOI/giphy.gif")

    Pop quiz

    If we use the code from the previous step, the interactions will no longer work. Can you guess which two lines will cause problems and why?

    // expandable is no longer unique so we cannot use getElementById
    // moveover, we need to be able to attach event listeners to multple
    // elements, not just one
    var expandable = document.getElementById("expandable")
    
    // similarly, if expandable is no longer unique, we cannot select its child 
    // .content in as querySelector will just return the first that matches
    // in the document. However querySelector() can be used on elements other than the
    // document, such as e.currentTarget
    var content = document.querySelector("#expandable .content")

    Explanation

    To handle mutliple elements we need to use `querySelectorAll()` to find all the elements that match a certain structure. In this case that is anything called `.expandable`. A loop is then required to go through all these elements to attach listeners.

    Inside the listener function `expand`, the element that you attached the listener to can be accessed in `e.currentTarget` where `e` is the event. We have to pass in the event to use it, so rewrite `expand()` as `expand(e)`. This is not to be confused with `e.target` which is the element that triggered the event. In this case, this could be `#expandable`, or any element inside it like `.content`

    var proto = {
      init : function(){
        //if using prototyping server, use this single line instead
        //bind(".expandable","click",proto.expand)
        var expandables = document.querySelectorAll(".expandable")
        expandables.forEach(function(expandable){
          expandable.addEventListener("click",proto.expand)
        })
      },
      expand : function(e){
          e.currentTarget.classList.toggle("open")
          var content = e.currentTarget.querySelector(".content")
          if(e.currentTarget.classList.contains("open")){
            e.currentTarget.style.height = content.offsetHeight + "px"
          } else {
            e.currentTarget.style.height = "35px"
          }
      }
    }
    
    document.addEventListener('DOMContentLoaded', proto.init)

    clicky here

    clicky here

    clicky here

Exercises

  1. Add visual feedback

    As they are, the expandable elements are rather minimal. Lets provide more feedback to indicate that they are interactive. Get it to change background colour when open and add a hover and active state to the stylus file. Also add the `background-color` property to transition to animate the colour changes as well.

    Hint: you may use the pseudoclasses `&:hover` and `&:active`

    .expandable4
      height: 35px
      width: 200px
      background-color: #4C9AFF
      color: white
      cursor: pointer
      overflow:hidden                
      transition: height 0.3s $ak-ease-out, background-color 0.3s $ak-ease-out
      .content
        padding: 5px 10px
        img
          width: 100%
      &.open
        background-color: #2684ff
      &:hover
        background-color #2684ff
      &:active
        background-color: #0065ff

    clicky here

    clicky here

    clicky here

  2. Collapse others when expanding current element

    When you have many expanding elements, it is convenient to collapse all the others when expanding one. Rewrite the `expand` function to do this.

    Hint: You may use a loop that compares each expandable to `e.currentTarget`. If the element is not `e.currentTarget`, remove the `.open` class. Then apply the logic we have already on setting heights for all elements.

    var proto = {
      init : function(){
        //if using prototyping server, use this single line instead
        //bind(".expandable","click",proto.expand)
        var expandables = document.querySelectorAll(".expandable")
        expandables.forEach(function(expandable){
          expandable.addEventListener("click",proto.expand)
        })
      },
      expand : function(e){
        var expandables = document.querySelectorAll(".expandable")
        expandables.forEach(function(expandable){
          if(expandable == e.currentTarget){
            expandable.classList.toggle("open")
          } else {
            expandable.classList.remove("open")
          }
          var content = expandable.querySelector(".content")
          if(expandable.classList.contains("open")){
            expandable.style.height = content.offsetHeight + "px"
          } else {
            expandable.style.height = "35px"
    
          }
        })    
      }
    }
    
    document.addEventListener('DOMContentLoaded', proto.init)

    clicky here

    clicky here

    clicky here

  3. Expand on width and height

    Earlier in the lesson you learnt how to get the height with `offsetHeight` and set it with`style.height`. Now adapt it to this project expand.zip to get it to expand both its width and height.

    var proto = {
      init : function(){
        //if using prototyping server, use this single line instead
        //bind(".expandable","click",proto.expand)
        var expandables = document.querySelectorAll(".expandable")
        expandables.forEach(function(expandable){
          expandable.addEventListener("click",proto.expand)
        })
      },
      expand : function (e) {
        var expandable = e.currentTarget
        expandable.classList.toggle("open")
        var content = expandable.querySelector(".content")
        if(expandable.classList.contains("open")){
          expandable.style.width = content.offsetWidth + "px"
          expandable.style.height = content.offsetHeight + "px"
        } else {
          expandable.style.width = "200px"
          expandable.style.height = "35px"
        }    
      }
    }
    
    document.addEventListener('DOMContentLoaded', proto.init)
    

    clicky here

    In Holland's embassy in Moscow, Russia, the staff noticed that the two Siamese cats kept meowing and clawing at the walls of the building. Their owners finally investigated, thinking they would find mice. Instead, they discovered microphones hidden by Russian spies. The cats heard the microphones when they turned on.