Search code examples
javascripthtmlstimulusjs

Stimulus build html element from rest API


Hello I have a stimulus code into my symfony project. This code is calling a rest API which taking around 3 seconds to provide the response. This rest api return JSON.

This is my code :

import {Controller} from "@hotwired/stimulus";
import axios from "axios";


export default class extends Controller {
    static values = {
        url: String
    }

    connect() {
        axios.get(this.urlValue)
            .then((r) => {
                if (r.data !== null) {
                    let html
                    const tmp = JSON.parse(r.data)
                    if (tmp === null) {
                        html = document.createElement("div")
                        html.classList.add("alert", "alert-danger", "alert-dismissible", "fade", "show")
                        html.innerHTML += "Asset Number Not Valid";
                        html.innerHTML += "<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\" aria-label=\"Close\"></button>"
                    } else {
                        html = document.createElement("ul")
                        html.classList.add("list-group")
                        for(let key in tmp) {
                            html.innerHTML += "<li class=\"list-group-item\">" + key + " : " + tmp[key] + "</li>";
                        }
                        html.innerHTML += "</ul>";
                    }
                    this.element.replaceWith(html);
                }
            })
    }
}

As you can see, it's building a list or display an error. This code is really simple and works well. I just don't like how html is build.

Do you have any other/cleaner way ?


Solution

  • Here is a different approach, basically leveraging the HTML Template element to move all of your HTML back into the HTML file.

    When you are building too much HTML in your Stimulus controller it can get messy, instead think about the power of targets as being able to target any element including templates and even things you add dynamically.

    Controller

    • We create some template targets for our HTML that we will use as a base to copy when inject.
    • We also have an error and item` target name for being able to easily remove any error or items that get added.
    • The results target will house the results (item templates) that get injected.
    • Also we will use innerText instead of innerHtml for safety.
    import { Controller } from '@hotwired/stimulus';
    
    export default class extends Controller {
      static targets = [
        'error',
        'errorTemplate',
        'item',
        'itemTemplate',
        'results',
      ];
    
      static values = {
        url: String,
      };
    
      connect() {
        this.clearError();
        this.clearResults();
    
        fetch(/* or axios */)
          .then(/* json parsing etc */)
          .then((items) => {
            items.forEach((value) => {
              this.addResultItem(value);
            });
          })
          .catch((error) => {
            // be sure to handle errors using nice promise like thing
            this.showError(error);
          });
      }
    
      clearError() {
        this.errorTarget && this.errorTarget.remove();
      }
    
      clearResults() {
        this.resultItemTargets.forEach((itemElement) => {
          itemElement.remove();
        });
      }
    
      showError() {
        const alert =
          this.errorTemplateTarget.content.firstElementChild.cloneNode(true);
        this.prepend(alert);
      }
    
      addResultItem(value) {
        const item =
          this.itemTemplateTarget.content.firstElementChild.cloneNode(true);
        item.innerText = value;
        this.resultsTarget.append(item);
      }
    }
    

    HTML

    • Here we have our base template elements that let us set up any HTML we want for the error and item parts.
    • The template elements have the target names that end in template
    • Note that we have the ul always in the DOM but you can use css :empty to just hide it by default if there are no items in it.
    <section class="container" data-controller="results"
      data-results-url-value="https://myapi">
      <template data-results-target="itemTemplate">
        <li class="list-group-item" data-results-target="item">__VALUE__</li>
      </template>
      <template data-results-target="errorTemplate">
        <div data-results-target="error" class="alert alert-danger
          alert-dismissible fade show" role="alert">
          <!-- remember accessibility - use proper titles & role=alert when putting things in DOM -->
          <button type="button" class="btn-close" data-bs-dismiss="alert"
            aria-label="Close"></button>
          <h3>Asset Number Not Valid</h3>
          <p>Details about the thing</p>
        </div>
      </template>
      <!-- use css to hide if :empty -->
      <ul class="list-group" data-results-target="results">
      </ul>
    </section>
    

    Helpful links