2
\$\begingroup\$

I have made an app using Microsoft's Face API: Face API JavaScript Quick Start

It shall analyze an image, showing a person's portrait, concerning the gender of the person, her/his age and emotions.

One pastes the URL of an image into the text-box. Then clicks the button.

The results are then listed below the image.

Caution! I'm using a free, temporary key in there. The app might stop working some time after my post.

(function(window) {
  var headersObject = new Headers();
  var photo = document.getElementById('analyzedPhoto');
  var attributesList = document.getElementById('attributes');

  headersObject.append('Ocp-Apim-Subscription-Key', '32fee7696d9545bdab41fd09ffb00034');
  headersObject.append('Content-Type', 'application/json');

  document
    .getElementById('analyse')
    .addEventListener('click', () => {
      var imgUrl = document.getElementById('imageUrl').value;

      var initObject = {
        headers: headersObject,
        method: 'post',
        body: '{"url": "' + imgUrl + '"}'
      }

      photo.src = imgUrl;

      fetch('https://westcentralus.api.cognitive.microsoft.com/face/v1.0/detect?returnFaceId=true&returnFaceAttributes=emotion,age,gender', initObject)
        .then((response) => {
          !response.ok &&
            Promise.reject(new Error('The request of analysis data has failed.'));

          return response.json();
        })
        .then((json) => {
          let facesAttributes =
            Object.assign({},
              json[0].faceAttributes,
              json[0].faceAttributes.emotion);
          let listItems = [];
          // Otherwise emotion-properties AND emotion-object self are contained in attributes-object.
          delete facesAttributes.emotion;

          for (let key in facesAttributes) {
            listItems.push('<li class="list-item"><span class="list-item-key">' +
              (key.slice(0, 1).toUpperCase()) +
              (key.slice(1)) + '</span>: ' + facesAttributes[key] +
              '</li>');
          }

          attributesList.innerHTML = listItems.join('');
        })
        .catch((error) => {
          console.dir(error);
          attributesList.innerHTML = 'Have you entered a valid image URL?';
        });
    });
})(window);
html {
  overflow-y: scroll;
  background-color: rgba(245, 245, 245, 1.0);
  font-family: "Trebuchet MS", "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Tahoma, sans-serif;
}

.main-wrap {
  margin: 0 auto 30px;
  max-width: 800px;
}

#imageUrl {
  display: inline-block;
  width: 100%;
  max-width: 780px;
}

#analyzedPhoto {
  margin: 15px;
  border-radius: 3px;
  max-width: 100%;
  height: auto;
}

#attributes {
  list-style: none;
  padding-left: 0;
}

#attributes .list-item {
  margin-bottom: 10px;
}

.list-item-key {
  font-weight: 900;
  color: rgba(20, 20, 20, 1.0);
}
<div class="main-wrap">
  <h1>Enter Image URL</h1>

  <section>
    <p>
      <input type="text" value="https://upload.wikimedia.org/wikipedia/commons/thumb/5/56/Tesla3.jpg/220px-Tesla3.jpg" id="imageUrl" />
    </p>
    <button id="analyse">Analyse</button>
  </section>

  <section>
    <img src="" id="analyzedPhoto" alt="Photo to analyze" />
  </section>

  <section>
    <h2>Attributes</h2>
    <ul id="attributes"></ul>
  </section>
</div>

Is my usage of the fetch-API and Promises correct and done in a good way?

These concepts are fairly new to me.

Moreover: I had a bit of hassle merging the two objects into one. Please see my comment within the code.

Is there a better solution than deleting the unnecessary sub-object?

\$\endgroup\$
2
\$\begingroup\$

Addressing Your Questions

Is my usage of fetch-API and Promises correct and done in a good way?

Yes it seems fine. The Fetch API is supported by all major browsers but not IE1 but some argue that it doesn't really have a simple way to cancel requests. If there arises a need to abort requests, then other libraries like SuperAgent or reqwest could be used.

Is there are better solution then deleting the unnecessary sub-object?

Obviously there are other ways to handle setting the values within the emotion property on faceAttributes, but those might likely require more lines. One could also conditionally display list items (e.g. add if (typeof facesAttributes[key] !== "object") { before the line that pushes strings into listItems). If there was a way to filter with a callback when calling Object.extend() that would be an advantage here but unfortunately it doesn't appear that is the case.

Other review points

Feedback

The code looks quite succint. There are two DOM lookups at the start but then one DOM lookup in the click handler so that could be cached in a variable.

Suggestions

var vs let/const

Perhaps you have already learned more about let and const since posting this question but many es-6 developers would prefer using const for values that don't change (e.g. imgUrl, initObject, etc.)

Template

One could also utilize a script template to display the list items, though that may not be desirable here.

Invalid HTML when Error occurs

This line in the promise catch:

attributesList.innerHTML = 'Have you entered a valid image URL?';

Sets the inner HTML of the unordered list tag to a text string. That is technically unpermitted content for an unordered list. Perhaps a better solution would be to have a separate element for the error message.

P.S.

Here is the output from that AJAX call using that image of Tesla, so you could fake it now that the key is invalid:

const output = [{"faceId":"950c26ae-70f0-4365-8279-fcd987f56e70","faceRectangle":{"top":75,"left":32,"width":117,"height":117},"faceAttributes":{"gender":"male","age":37.0,"emotion":{"anger":0.0,"contempt":0.0,"disgust":0.0,"fear":0.0,"happiness":0.007,"neutral":0.992,"sadness":0.0,"surprise":0.0}}}];
const p1 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 100, output);
  });
p1.then(console.log);

| improve this answer | |
\$\endgroup\$
0
\$\begingroup\$

Other side of the coin

Is there are better solution then deleting the unnecessary sub-object?

After thinking about this a little more, I feel like one could utilize Object.entries() with a recrusive assignment function and more es-6 features like Array Destructuring to avoid the delete operation:

const facesAttributes = {};
const handleEntries = ([key, value]) => typeof value === "object" ? Object.entries(value).forEach(handleEntries) : facesAttributes[key] = value;
Object.entries(json[0]).forEach(handleEntries);

So while it may not be any shorter, there is no need to use the delete operation. I tried using Array.reduce() but wasn't able to find a way to have a one-line arrow function that both assigned values to the accumulator object and returned the accumulator object.

const instead of let

I know it has already been mentioned that const can be used instead of var for some values that don't change (e.g. imgUrl, initObject, etc.). There are also some block-scoped variables declared using let that can be declared using const since they are never re-assigned: facesAttributes and listItems.

Updated code

(function(window) {
  var headersObject = new Headers();
  var photo = document.getElementById('analyzedPhoto');
  var attributesList = document.getElementById('attributes');

  headersObject.append('Ocp-Apim-Subscription-Key', '32fee7696d9545bdab41fd09ffb00034');
  headersObject.append('Content-Type', 'application/json');

  document
    .getElementById('analyse')
    .addEventListener('click', () => {
      const imgUrl = document.getElementById('imageUrl').value;

      const initObject = {
        headers: headersObject,
        method: 'post',
        body: '{"url": "' + imgUrl + '"}'
      }

      photo.src = imgUrl;

      const output = [{"faceId":"950c26ae-70f0-4365-8279-fcd987f56e70","faceRectangle":{"top":75,"left":32,"width":117,"height":117},"faceAttributes":{"gender":"male","age":37.0,"emotion":{"anger":0.0,"contempt":0.0,"disgust":0.0,"fear":0.0,"happiness":0.007,"neutral":0.992,"sadness":0.0,"surprise":0.0}}}];
const p1 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 100, output);
  });
        p1.then((json) => {
        
          const facesAttributes = {};
          const handleEntries = ([key, value]) => typeof value === "object" ? Object.entries(value).forEach(handleEntries) : facesAttributes[key] = value;
          Object.entries(json[0]).forEach(handleEntries);
      
          const listItems = [];

          for (let key in facesAttributes) {
            listItems.push('<li class="list-item"><span class="list-item-key">' +
              (key.slice(0, 1).toUpperCase()) +
              (key.slice(1)) + '</span>: ' + facesAttributes[key] +
              '</li>');
          }

          attributesList.innerHTML = listItems.join('');
        })
        .catch((error) => {
          console.dir(error);
          attributesList.innerHTML = 'Have you entered a valid image URL?';
        });
    });
})(window);
html {
  overflow-y: scroll;
  background-color: rgba(245, 245, 245, 1.0);
  font-family: "Trebuchet MS", "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Tahoma, sans-serif;
}

.main-wrap {
  margin: 0 auto 30px;
  max-width: 800px;
}

#imageUrl {
  display: inline-block;
  width: 100%;
  max-width: 780px;
}

#analyzedPhoto {
  margin: 15px;
  border-radius: 3px;
  max-width: 100%;
  height: auto;
}

#attributes {
  list-style: none;
  padding-left: 0;
}

#attributes .list-item {
  margin-bottom: 10px;
}

.list-item-key {
  font-weight: 900;
  color: rgba(20, 20, 20, 1.0);
}
<div class="main-wrap">
  <h1>Enter Image URL</h1>

  <section>
    <p>
      <input type="text" value="https://upload.wikimedia.org/wikipedia/commons/thumb/5/56/Tesla3.jpg/220px-Tesla3.jpg" id="imageUrl" />
    </p>
    <button id="analyse">Analyse</button>
  </section>

  <section>
    <img src="" id="analyzedPhoto" alt="Photo to analyze" />
  </section>

  <section>
    <h2>Attributes</h2>
    <ul id="attributes"></ul>
  </section>
</div>

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.