NewFiveFour | Blog | Portfolio


Javascript: Simplifying fetching data and showing loading indicators and error displays

Previously we created an error display when a fetch failed, and integreated a loading spinner in that (https://newfivefour.com/javascript-add-an-error-display.html).

The code to set the loading and error display is as follows:

function fetchData(inputText) {
  dynSet("loading", true)
  dynSet("error", "")
  sensibleFetch(`https://newfivefour.com:3000/name?name=${inputText}`)
  .then(json => {
    dynSet("loading", false)
    var list = json.map(place => place.name)
    dynSet("change_it", list)
  })
  .catch(e => {
    dynSet("loading", false)
    dynSet("error", e)
    console.log("A bad request!", e)
  })
}

The code to start and stop the loading indicator and show the error will be the same for all our future remote data fetches. So let’s integreate that code into our sensibleFetch function, renaming it accorindly:

function sensibleFetchWithLoadingAndError() {
  var loadingDyn = arguments[0]
  var errorDyn = arguments[1]
  var fetchArguments = [...arguments].slice(2)
  return new Promise((res, rej) => {
    dynSet(loadingDyn, true)
    dynSet(errorDyn, "")
    fetch(fetchArguments)
    .then(r => {
      dynSet(loadingDyn, false)
      if(r.ok) return r.json()
        .then(json => { console.log(json); res(json) })
        .catch(error => { console.log(error); rej(json) })
      else {
        rej(`${r.statusText} (${r.status})`)
        dynSet(errorDyn, `${r.statusText} (${r.status})`)
      }
    })
    .catch(e => {
      dynSet(loadingDyn, false)
      rej(e)
    })
  })
}

We can now simply our fetchData code down to this:

function fetchData(inputText) {
    sensibleFetchWithLoadingAndError("loading", "error",
      `https://newfivefour.com:3000/name?name=${inputText}`)
    .then(json => {
      dynSet("change_it", json.map(place => place.name))
    })
  }

The new cleaned up version of our code is at: https://jsfiddle.net/newfivefour/mkx0cf5g/21/

(I’ve also changed around the order of the loadingButton arguments to make them consistent with the function that shows the error display)

javascript

Javascript: Making very simple custom components with javascript

In our previous posts we started making HTML with javascript (https://newfivefour.com/javascript-turning-our-animated-loading-button-on-off.html).

This gives us a the ability to make new components, or new tags if you will, very easily.

Our previous code had this code to make a animated loading button:

  div("display: flex; flex-direction: column; align-items: flex-start;",
    input(["placeholder", "Input: lon, man, liv, etc"]),
    div("position: relative", 
      dyn("loading", false, loading =>
        div("position: absolute;", ["class", loading ? "spinner" : ""]),
      ),
      button(text("Search")),
      ["click", (e, node) => fetchData(node.previousSibling.value)]
    ),
  )

It’s very likely that the the code that has the button within it will be used again, and again, and again.

So let’s put it in function instead:

function loadingButton(buttonNode, listener, dynLoadingName) {
  return div("position: relative", 
           dyn(dynLoadingName, false, loading =>
             div("position: absolute;", ["class", loading ? "spinner" : ""]),
           ),
           buttonNode,
           ["click", listener]
         )
}

We can now use this ‘component’ in our javascript:

div("display: flex; flex-direction: column; align-items: flex-start;",
  input(["placeholder", "Input: lon, man, liv, etc"]),
  loadingButton(
    button(text("Search!")), 
    (e, node) => fetchData(node.previousSibling.value),
    "loading"
  )
)

Loading button can now be used multiple times. See an example here: https://jsfiddle.net/newfivefour/ydsgapwn/6/

javascript

Javascript: Turning our animated loading button on and off

Now we have our animated loading button (see https://newfivefour.com/javascript-passing-dom-node-to-event-listener.html), we can turn it on and off.

Our body will be the same as before except we’re using a dyn, with the identifier “loading”, to give our composite button it’s animated class:

body(
  div(
    dyn("change_it", ["one", "two", "three"], data =>
      ol(data.map(t =>
        li(text(t)))
      )
    ),
    div("display: flex; flex-direction: column; align-items: flex-start;",
      input(["placeholder", "Input: lon, man, liv, etc"]),
      div("position: relative", 
        dyn("loading", false, loading =>
          div("position: absolute;", ["class", loading ? "spinner" : ""]),
        ),
        button(text("Search")),
        ["click", (e, node) => fetchData(node.previousSibling.value)]
      ),
    )
  )
)

Next, in our fetchDate function we’ll turn this on and off:

function fetchData(inputText) {
  dynSet("loading", true)
  sensibleFetch(`https://newfivefour.com:3000/name?name=${inputText}`)
  .then(json => {
    dynSet("loading", false)
    var list = json.map(place => place.name)
    dynSet("change_it", list)
  })
  .catch(e => {
    dynSet("loading", false)
    console.log("A bad request!", e)
  })
}

And now every time we make a network request the loading spinner will appear on the button and disappear when the fetch is over.

Here’s a working example: https://jsfiddle.net/newfivefour/m0dta59v/11/

Next we can deal with cleaning up our code and looking at error messages.

javascript css

Javascript: Passing the dom node to our event listener

Previously (https://newfivefour.com/javascript-css-loading-button.html) we had this:

div("position: relative", 
  div("position: absolute;", ["class", "spinner"]),
  button(text("Search")),
  ["click", e => fetchData(e.target.parentNode.previousSibling.value)]
),

If you look at our event listener we’re traversing the dom using parentNode and previousSibling.

This is because the click event will come from the button, and so even thought we’re on the outer div in our listener, event.target will still point to the button that was clicked.

This is a problem because we won’t exactly know what target will relate to when we update this DOM in the future.

A better solution is to give this click listener a reference to it own DOM element (div("position: relative")) in our case.

We can modify our node (therefore div etc) to give it so with this new code:

var name = arguments[i][j++]
var clicker = arguments[i][j++]
h.addEventListener(name, e => clicker(e, h))

Where h refers to the current dom node. Our modified node() method looks like this now:

function node() {
  var iterator = 0;
  var h = document.createElement(arguments[iterator++])
  if(arguments[iterator] && typeof(arguments[iterator]) === "string") h.style = arguments[iterator++];
  if(arguments[iterator] && arguments[iterator] instanceof Array
    && typeof(arguments[iterator][1]) === "string") {
    for(var i=0; i<arguments[iterator].length;) {
      h.setAttribute(arguments[iterator][i++], arguments[iterator][i++])
    }
    iterator++
  }
  if(arguments[iterator])
  for(var i = iterator; i < arguments.length; i++) {
    if(arguments[i] instanceof Function) arguments[i](h)
    else if(arguments[i] instanceof Array) {
      if(arguments[i][1] instanceof Function) {
        for(var j = 0; j < arguments[i].length; ) {
          var name = arguments[i][j++]
          var clicker = arguments[i][j++]
          h.addEventListener(name, e => clicker(e, h))
        }
      } else {
        arguments[i].forEach(node => h.appendChild(node))
      }
    } else {
      h.appendChild(arguments[i])
    }
  }
  return h
}

With all our div etc helper method we can now do this:

div("display: flex; flex-direction: column; align-items: flex-start;",
  input(["placeholder", "Input: lon, man, liv, etc"]),
  div("position: relative", 
    div("position: absolute;", ["class", "spinner"]),
    button(text("Search")),
    ["click", (e, node) => fetchData(node.previousSibling.value)]
  ),
)

This should now be more stable, should we add another button inside that div, for example. Next onto working with the loading button some more.

javascript

Javascript, CSS: Make an animated loading button that works with fetch

In our previous post we remotely got JSON from a server and updated our page with dyn based on user input. (https://newfivefour.com/javascript-dynamically-fetch-remote-data.html)

We’re not showing the loading indication, however, but we can do that CSS3 animations. Here’s our new HTML that we’ll use instead of the old button:

div("position: relative", 
  div(["class", "spinner"]),
  button(text("Search"))
),

We now put the button in a relative div, and in that relative div we put a new absolute div that has the spinner class. Here’s the CSS spinner class:

@keyframes spinner {
  to {transform: rotate(360deg);}
}

.spinner {
  box-sizing: border-box;
  top: 50%;
  left: 50%;
  width: 20px;
  height: 20px;
  margin-top: -10px;
  margin-left: -10px;
  border-radius: 50%;
  border: 2px solid #ccc;
  border-top-color: blue;
  animation: spinner .6s linear infinite;
}

It sets a animated transform that rotates the element 360 degrees. It applies that animation to the .spinner class infinately every 0.6 seconds.

The .spinner class has a border at its top, and it’s 20px square. It’s placed in the centre and it has a margin half it’s width and height to centre it. It has a border-radius to make it round. It has a border-box sizing to stop the margin and padding enlarging the box.

We now have a spinning button. https://jsfiddle.net/newfivefour/17pj0wyt/7/ Next we will to turn it on and off and integrate it with fetch.

javascript css

Page 2 of 88
Previous Next