0

I am building an application with pure javascript and Web Components. I also want to use the MVC Pattern, but now I have a problem with asynchronous calls from the model.

I am developing a meal-list component. The data is coming from an API as JSON in the following format:

[
   {
     id: 1,
     name: "Burger",
    },
]

I want the controller to get the data from the model and send it to the view.

meals.js (Model)

export default {

    get all() {
        const url = 'http://localhost:8080/meals';

        let speisekarte = [];
        fetch(url, {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json'
            },
        }).then(res => {
            return res.json()
        }).then(data => {
            // This prints the result I want to use, but I can't return
            console.log(data);
            // This does not work
            speisekarte = data;
            // This also does not work
            return data;
        });
        // is undefined.
        return speisekarte;
    },
}

This is how I tried to get the data from an API.

meal-list.component.js (Controller)

import Template from './meal-list.template.js'
import Meal from '../../../../data/meal.js'

export default class MealListComponent extends HTMLElement {

    connectedCallback() {
        this.attachShadow({mode: 'open'});
         // Should send the Data from the model to the View
        this.shadowRoot.innerHTML = Template.render(Meal.all);
    }
}

if (!customElements.get('mp-meal-list')) {
    customElements.define('mp-meal-list', MealListComponent);
}

meal-list.template.js (View)

export default {
    render(meals) {
        return `${this.html(meals)}`;
    },

    html(meals) {
        let content = `<h1>Speisekarte</h1>
                       <div class="container">`;

        content += /* display the data from api with meals.forEach */
        return content + '</div>';
    },
 }

As I mentioned in the comments, I have a problem in returning the async data from the model to the view. Either it is undefined when I try to return data; or if I try to save the data into an array. I could also return the whole fetch() method, but this returns a promise and I dont think the controller should handle the promise.

I already read the long thread in How do I return the response from an asynchronous call? but I could not relate it to my case.

Cenasa
  • 531
  • 9
  • 27

2 Answers2

0

Since you declared speisekarte as an array, I'd expect it to always return as an empty array. When the fetch executes and fulfills the promise, its always too late in the above implementation.

You have to wait for the fetch result and there are multiple options you might consider:

  • Either providing a callback to the fetch result
  • Or notifying your application via event dispatch and listeners that your data has been loaded, so it can start rendering

Your link already has a very good answer on the topic callbacks and async/await, I could not put it better than what is explained there.

lotype
  • 593
  • 3
  • 8
0

Thanks to lotype and Danny '365CSI' Engelman I've found the perfect solution for my projct. I solved it with custom events and an EventBus:

meal.js (model)

get meals() {
    const url = 'http://localhost:8080/meals';

    return fetch(url, {
        method: 'GET',
        headers: {
            'Content-Type': 'application/json'
        },
    }).then(res => {
        return res.json()
    }).then(data => {
        let ce = new CustomEvent(this.ESSEN_CHANGE_EVENT, {
            detail: {
                action: this.ESSEN_LOAD_ACTION,
                meals: data,
            }
        });
        EventBus.dispatchEvent(ce);
    });
},

EventBus.js (from book: Web Components in Action)

export default {
    /**
     * add event listener
     * @param type
     * @param cb
     * @returns {{type: *, callback: *}}
     */
    addEventListener(type, cb) {
        if (!this._listeners) {
            this._listeners = [];
        }

        let listener = {type: type, callback: cb};
        this._listeners.push(listener);
        return listener;
    },

    /**
     * trigger event
     * @param ce
     */
    dispatchEvent(ce) {
        this._listeners.forEach(function (l) {
            if (ce.type === l.type) {
                l.callback.apply(this, [ce]);
            }
        });
    }
}

Now, when the data is ready, a signal to the event bus is sent. The meal-list-component is waiting for the events and then gets the data:

export default class MealListComponent extends HTMLElement {

    connectedCallback() {
        this.attachShadow({mode: 'open'});
        this.shadowRoot.innerHTML = Template.render();
        this.dom = Template.mapDOM(this.shadowRoot);

        // Load Speisekarte on init
        this.dom.meals.innerHTML = Template.renderMeals(MealData.all);

        // Custom Eventlistener - always triggers when essen gets added, deleted, updated etc.
        EventBus.addEventListener(EssenData.ESSEN_CHANGE_EVENT, e => {
            this.onMealChange(e);
        });
    }

    onMealChange(e) {
        switch (e.detail.action) {
            case EssenData.ESSEN_LOAD_ACTION:
                this.dom.meals.innerHTML = Template.renderMEals(e.detail.meals);
                break;
        }
    }
}
Cenasa
  • 531
  • 9
  • 27