ES6 Proxy Objects and Magic Methods

ES6 Proxy Objects and Magic Methods

Recently I've used Proxy for a piece of code to maintain application state and manipulate it according to async API calls and user events. In this post, let's use Proxy and build a simple front end for DuckDuckGo Search API

Let's start with bare-bone Proxy Object

var ctrl = new Proxy({}, {

    get (tar, prop, rec) {

         return tar[prop];

      },

    set (tar, prop, val, rec) {
        
        tar[prop] = val

    }

})

First argument to Proxy constructor takes the initial state object that acts as target to which our proxy instance communicates to for all the CRUD operations. Second argument takes the handler for those CRUD operations. Let's see how this is significant for various use cases.

Say I want to show a loader whenever I initiate any API call and hide the loader once API call gets response.

I can imagine the application state with initial value of loading count 0. Let's wire that to our proxy instance as initial config.

var ctrl = new Proxy({

  loading: 0

} , {

    get (tar, prop, rec) {

         return tar[prop];

    },

    set (tar, prop, val, rec) {
        
        tar[prop] = val

    }


})

We need a method to make the API call given a text query and a callback. Once XHR promise is resolved, callback gets called with response object. Let's use fetch to implement this method.

var api = {

  async suggest (text, cb) {

    ctrl.loading += 1

    var resp = await fetch(`https://cors-anywhere.herokuapp.com/https://api.duckduckgo.com/?q=${text}&format=json&pretty=1&no_redirect=1`)

    var data = await resp.json()

    ctrl.loading -= 1

    cb(data)

  }

}

Typically we will be using something called XHR intercepter along with a http client library to add/remove loading counts in any XHR request made via that client.

Let's also add html for input box, loader container, and a results container to populate search results.

<body>

  <input id="ip" type="text" placeholder="search" />
 
  <div id="loader" class=""><div></div><div></div><div></div><div></div></div>
 
  <ul id="results"></ul>

</body>

We need an observer that watches for input to change and trigger API call.

var observers = {

  textChanged (e) {

    api.suggest(e.target.value, (r) => {

      var ul = ctrl.$results

      ul.innerHTML = ""

      r.RelatedTopics.forEach((i) => {

        if(i.Text) ul.innerHTML += `<li>${views.item(i)}</li>`

      })

    })

  }

}
ok. what's that ctrl.$results ?

You don't need JQuery all the time! Proxy helps to create custom selector functions.

var ctrl = new Proxy({

  loading: 0

} , {

    get (tar, prop, rec) {

        if(prop in tar) return tar[prop]

        if(prop[0] === '$') {

          tar[prop] = document.getElementById(prop.substr(1))

        }

        return tar[prop];

      },

    set (tar, prop, val, rec) {
        
        tar[prop] = val

    }


})

any property lookup to our ctrl proxy instance that starts with a $ will try to get DOM node with corresponding ID attribute. This reference is cached in our target object for any future lookups to same node. With this selector, we can get any node with ctrl.$yourNodeId.

This observer is hooked to listen to change of input.

ctrl.$ip.addEventListener('change', observers.textChanged)

now let's add a template to render each item from the API response

var views = {

  item (i) {

    return `

     <div class="item-wrap">

      <div class="item-text">${i.Result}</div>

      <img class="item-icon" src="${i.Icon.URL}" />

     </div>`
 
  }

}

Now to make our loader appear whenever ctrl.loading count is >= 0, let's modify our proxy setter method.

var ctrl = new Proxy({

  loading: 0

} , {

    get (tar, prop, rec) {

        if(prop in tar) return tar[prop]

        if(prop[0] === '$') {

          tar[prop] = document.getElementById(prop.substr(1))

        }

        return tar[prop];

      },

    set (tar, prop, val, rec) {
        
        var params = { tar, prop, val, rec }

        var actions = {

          loading_ () {

            var ld = this.rec.$loader

            var cl = "lds-ellipsis";

            (this.rec.loading > 0) ?
 
              ld.classList.add(cl) : ld.classList.remove(cl)
  
          }

        }

        tar[prop] = val

        if(`${prop}_` in actions) {

          actions[`${prop}_`].apply(params)

        }

        return rec

    }


})

Last argument to setter method is receiver object which is the proxy instance itself. This is handy to use our selector this.rec.$loader to get loader node in DOM in that context. Current loading count can also be checked from receiver object and accordingly loader class 'lds-ellipsis' is added / removed from loader node.

Above setter behaves as if it sets any value that is given to the proxy for a given property but also invokes after set callbacks if any defined in actions for that given property.

loading_() method is a after set callback for loading property. so whenever value of loading gets changed, this callback is invoked and it takes care of adding and removing loader class.

We can also have guards to our setter method.

var ctrl = new Proxy({

  loading: 0

} , {

    get (tar, prop, rec) {

        if(prop in tar) return tar[prop]

        if(prop[0] === '$') {

          tar[prop] = document.getElementById(prop.substr(1))

        }

        return tar[prop];

      },

    set (tar, prop, val, rec) {
        
        var params = { tar, prop, val, rec }

        var actions = {

          loading_ () {

            var ld = this.rec.$loader

            var cl = "lds-ellipsis";

            (this.rec.loading > 0) ? 

              ld.classList.add(cl) : ld.classList.remove(cl)
  
          },

          $loading () {

            return this.val >= 0

          }

         }


       if(`$${prop}` in actions && !actions[`$${prop}`].apply(params)) {
          
          console.log(`Invalid params ${params.val}`)

          return rec

       }

        tar[prop] = val

        if(`${prop}_` in actions) {

          actions[`${prop}_`].apply(params)

        }

        return rec

    }


})

above $loading() method acts as a guard if a set request occurs for property 'loading' with a value less than 0. This ensures ctrl.loading value will not go below 0 at any point and all API calls are balanced. One needs to ensure ctrl.loading is reduced for failed XHR calls as well to balance request out and response in count.

adding some css

  <style>

  .lds-ellipsis {
    display: inline-block;
    position: relative;
    width: 64px;
    height: 64px;
  }
  .lds-ellipsis div {
    position: absolute;
    top: 27px;
    width: 11px;
    height: 11px;
    border-radius: 50%;
    background: #fff;
    animation-timing-function: cubic-bezier(0, 1, 1, 0);
  }
  .lds-ellipsis div:nth-child(1) {
    left: 6px;
    animation: lds-ellipsis1 0.6s infinite;
  }
  .lds-ellipsis div:nth-child(2) {
    left: 6px;
    animation: lds-ellipsis2 0.6s infinite;
  }
  .lds-ellipsis div:nth-child(3) {
    left: 26px;
    animation: lds-ellipsis2 0.6s infinite;
  }
  .lds-ellipsis div:nth-child(4) {
    left: 45px;
    animation: lds-ellipsis3 0.6s infinite;
  }
  @keyframes lds-ellipsis1 {
    0% {
      transform: scale(0);
    }
    100% {
      transform: scale(1);
    }
  }
  @keyframes lds-ellipsis3 {
    0% {
      transform: scale(1);
    }
    100% {
      transform: scale(0);
    }
  }
  @keyframes lds-ellipsis2 {
    0% {
      transform: translate(0, 0);
    }
    100% {
      transform: translate(19px, 0);
    }
  }

  body {
    background: black;
    padding: 50px;
    display: flex;
    flex-direction: column;
    align-items: center;
  }

  * {
    font-size: 20px;
    color: white;
    font-family: monospace;
    list-style: none;
  }

  #ip {
    height: 40px;
    width: 200px;
    color: black;
    text-align: center;
    border-radius: 20px;
  }

  *:focus { outline: none; }

  #results li {
    margin: 40px auto;
    border: 2px solid green;
    border-radius: 10px;
    padding: 30px;
  }

  #results li a {
    margin-right: 15px;
  }

  .item-wrap {
    display: flex;
    align-items: center;
  }

  .item-text {
    flex: 90vw;
  }

  img {
    max-width: 20vw;
    background: white;
  }

  </style>

[ JSFiddle Demo ]


Other languages like Lua has similar Proxy like magic methods for facilitating Meta-Programming.

Too many magic in your code makes the developer siting on the other side of world cry when s/he reads your code.

What do you use Proxy for? Comment your thoughts below...

要查看或添加评论,请登录

Shanthakumar `的更多文章

社区洞察

其他会员也浏览了