Search code examples
javascriptasynchronousweb-componentfetch-api

Return asynchronous call through Web Components (MVC)


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.


Solution

  • 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;
            }
        }
    }