Let a Helper return a string from a JSON-object using 'fetch'

Hi, I’m new to Ember (and fairly new to JS and programming altogether).

I’m trying to create a Helper called random.js that will fetch a JSON object from an external API, and return one string from that object to the Template that calls the Helper. I’ve created this (and dozens of variants of it):

import { helper } from '@ember/component/helper';
import fetch from 'fetch';

export function random() {
  return fetch('https://www.randomtext.me/api/gibberish/p-1/20-40')
    .then(res => res.json())
    .then(body => body.text_out)
}

export default helper(random);

The Helper keeps returning an object, even though body.text_out is exactly what I need (and it is of type ‘string’). Any advice on how I can make this Helper return the string body.text_out to the Template?

I think you probably want to make a component for this instead of a helper. Helpers aren’t meant to deal with asynchronous state like a fetch, which is why it returns an object (the promise that is returned by fetch). Helpers are basically like small utility functions for a template that return immediately, so their applicable uses with async state are limited (basically they have to return an async value like a promise or ember-concurrency task to something else that knows how to handle it).

If you really want to use a helper you could try a PromiseObject though I wouldn’t necessarily recommend that approach. Another option would be trying out an addon like this and putting your helper inside that addon’s await helper.

If you anticipate doing lots of data fetching in components I’d also recommend checking out ember-concurrency. It’s a lot to wrap your head around at first but it is a more ergonomic way to deal with async tasks in templates (or in js for that matter) and support cancellation which is a huge plus.

Thank you so much, @dknutsen! I really appreciate your help.

One more question, though. What I need to accomplish (for a job interview assignment) is to repeat a text editor five times on one page, each one time with a different random text in it (hence the call to randomtext.me).

I decided to go with your first advice to create a component which I call five times in one template.

With the component (called ‘editor’) I initially ran into the same problem as with the helper: it returned an object. So I followed your advice and installed ‘Ember Promise Helpers’. And indeed, I managed to get the actual random text in the template of component ‘editor’ by calling:

{{await randomText}}

The component’s JS-file is:

export default Component.extend({
    randomText: fetch('https://www.randomtext.me/api/gibberish/p-1/20-40')
        .then(res => res.json())
        .then(body => body.text_out)
});

So far, so good. But I the random text is now simply shown between p-tags in the component’s template, and I need to use it as content for a text editor, like so (this is editor.hbs, the component’s template):

  {{summernote-editor
      focus=false
      btnSize=bs-sm
      airMode=false
      height=height
      buttons=buttons
      toolbar=toolbar
      disabled=disabled
      callbacks=callbacks
      content=(await randomText)
      onContentChange=(action (mut value))
    }}

And this doesn’t work (the editor component works, but there’s no content). The (await randomText) is used as described in the docs of Ember Promise Helpers (I think). I tried a few variants, but I can’t figure out how to pass the ‘resolved’ randomText as content to the summernote component. Am I on the right track here and simply making some syntax error, or is this approach not right in the first place?

I don’t know what the exact parameters of your question are, but the most idiomatic way to wait for data fetching is to do it from the Route. If you make a route like this:

export default class extends Route {
  async model() {
    let fetches = [];
    for (let i = 0; i < 5; i++) {
      fetches.push(fetch('https://www.randomtext.me/api/gibberish/p-1/20-40'));
    }
    let randomTexts = await Promise.all(fetches.map(async f => {
      let response = await f;
      let json = await response.json();
      return json.text_out;
    }));
    return { randomTexts };
  }
}

Your template will be able to use models.randomTexts directly, with no awaiting involved:

{{#each this.model.randomTexts as |randomText|}}
  {{summernote-editor content=randomText}}
{{/each}}

Thank you, @ef4! I agree that the best way to fetch data is from the Route. I did that earlier for just one random text, but of course I couldn’t use that text in 5 editors because they need to have different texts (hence the attempt to solve this with a Helper).

Your solution is much more elegant - it never occurred to me that I could create an array of texts on the route with a for-loop and iterate over them in the Template.

Having said that: I see / understand what your code does, but I can’t get it to work. Error: ‘Error while processing route: editors f.json is not a function’. Is that because the map() method expects a function, and .json() isn’t a function?

I edited my example above, try it again.

I had forgotten one place to await.

1 Like

Yep, that works like a charm! And I learned a bit about asynchronous programming in the process. Thank you!