Von
/07.11.23

MacBook Pro mit HTML code.

Manchmal ist es unmöglich, HTML direkt in Rails zu rendern, zum Beispiel, wenn Sie HTML aus JavaScript prozedural generieren müssen. Es gibt verschiedene Ansätze, aber einer sticht hervor: Vorlagen-Tags & Vorlagengenerierungsmotor. In diesem Blog erklären wir, wie ein JavaScript-Entwickler HTML rendern und das Wissen mit einem Stimulus-Beispiel aus einer echten Anwendung verwenden kann. Wir werden uns auch anschauen, wie Mustache.js genau für diesen Zweck verwendet wird.

Bestehende Ansätze für HTML in JS

1. Template-Strings

  const name = 'John';
const greetingHTML = `<div id="greeting">
  <h1>Hallo ${name}</h1>
</div>`;

document.body.insertAdjacentHTML(greetingHTML, 'beforeend');

Wie in diesem Artikel von Tristan Forward angesprochen, sind Template-Strings der schnellste Ansatz, HTML mit JavaScript zu rendern. Es ist auch ziemlich lesbar, aber es gibt einige Nachteile, vor allem:

  1. Fehlende Intelligenz/Linting: Sie benötigen höchstwahrscheinlich eine IDE, um HTML-Code innerhalb eines Template-Strings korrekt hervorzuheben, und selbst mit Linting wird es wahrscheinlich nicht überprüft.

  2. Mangel an Konsistenz: Weil Sie wahrscheinlich Ihre HTML/erb-Linter auf Template-Strings nicht anwenden können, verlieren Sie die Konsistenz zwischen Ihren Vorlagen und Ihrem regulären HTML.

  3. Wartbarkeit: Vorlagen, die über Ihre JavaScript-Controller verteilt sind, können im Vergleich zu einer zentralisierteren Methode Kopfschmerzen verursachen. Ja, man kann das in eine separate Vorlagenklasse abstrahieren, aber das ist eine Menge unnötiger Arbeit.

2. Manuelles Erstellen von Elementen mit document.createElement()

Der zweite Ansatz ist ziemlich altmodisch und beinhaltet VIEL mehr Arbeit und ist recht schmerzhaft, aber er wird oft als bessere Praxis angesehen, da er expliziter ist (und bessere IE6-Unterstützung hat). Es ist auch der Ansatz, mit dem die meisten Entwickler vertraut sind:

  const greetingWrapper =  document.createElement('div');
greetingWrapper.setAttribute('id', 'greeting');

const headingElement = document.createElement('h1');
headingElement.innerHTML = `Hallo ${name}`;

greetingWrapper.appendChild(headingElement);

Der Hauptnachteil dieses Ansatzes ist, dass er nicht so beschreibend ist wie einfachesHTML™. Wenn Sie keine kostbare Gehirnleistung verschwenden möchten, nur um Code wie diesen zu lesen, besonders wenn das HTML über mehrere Methoden/Klassen verteilt ist, müssen Sie eine andere Lösung finden. Igitt!

3. Verwendung eines HTML-Elements als Vorlage + Interpolieren von Werten mit dem Query Selector

  <template id="greetingTemplate">
  <div class="greeting">
    <h1>Hallo {{name}}</h1>
  </div>
</template>

Haben Sie noch nie einen Vorlagen-Tag gesehen?

Wenn Sie Ihr HTML-Wissen in Frage stellen, weil Sie zum ersten Mal einen anderen Tag sehen, machen Sie sich keine Sorgen, denn der <template>-Tag wird nicht so oft verwendet, hat aber dennoch einen Zweck:

Das <template> HTML Element ist ein Mechanismus zum Halten von HTML, das beim Laden einer Seite zunächst nicht gerendert wird, aber später zur Laufzeit mit JavaScript instanziiert werden kann.

Zitat: https://developer.mozilla.org/de/docs/Web/HTML/Element/template

Zurück zum Thema

Beachten Sie, dass der Text Hallo {{name}} vollständig ersetzt wird und sein einziger Zweck die Verbesserung der Lesbarkeit ist.

  const greetingTemplate = document.getElementById('greetingTemplate').cloneNode(true);
const greetingWrapper = greetingTemplate.firstChild;
const heading = greetingWrapper.querySelector('h1');

const name = 'John';
heading.innerText = `Hallo ${name}`;
document.body.appendChild(greetingWrapper);

Das Hauptproblem bei diesem Ansatz ist der Query Selector. Dies wird als aufwendige Operation betrachtet und kann langwierig sein, wenn viele Werte ersetzt werden müssen 😓.

4. Verwendung eines HTML-Elements als Vorlage + Interpolieren von Werten mit einem Vorlagengenerierungsmotor (🥇DER BESTE ANSATZ🥇)

Wenn Sie vermeiden möchten, Ihren eigenen Vorlagengenerierungsmotor zu schreiben (wie ich es getan habe, bevor ich Mustache entdeckt habe), um die Wertersetzung selbst zu übernehmen, sollten Sie in Erwägung ziehen, eine Bibliothek zu verwenden. Hier kommt Mustache ins Spiel.

Zuerst deklarieren Sie Ihr HTML-Element und dann verwenden Sie {{}} um die Platzhalterwerte zu kennzeichnen:

  <template id="greetingTempltae">
  <div class="greeting">
    <h1>Hallo {{name}}!</h1>
  </div>
</template>

Dann können Sie Werte aus einem Hash in JavaScript einsetzen, um das HTML zu rendern:

  const view = {
  name: "John"
}

const template = document.getElementById("greetingTemplate").innerHTML

const greetingHTML = Moustache.render(template, {
name: "John"
})

document.insertAdjacentHTML(greetingHTML, 'beforeend')

Die Vorteile dieses Ansatzes:

  1. Lesbarkeit: Die Verwendung von Template-Strings und eines Vorlagengenerierungsmotors ist sehr lesbar, da Sie einen perfekten Überblick über die Hierarchie haben, insbesondere bei komplexeren Partials.

  2. Trennung der Anliegen: Es ist generell schlechte Praxis, HTML aus Template-Strings in JavaScript zu rendern, somit ist das Extrahieren des HTMLs aus JavaScript und nur das Interpolieren der Werte viel besser.

  3. Es ist mühelos! 🎉

Mapbox-Beispiel

In diesem Beispiel zeige ich Ihnen, wie ich Moustache JS in meiner TypeScript + Rails-Anwendung verwendet habe, um Radiobuttons für Mapbox-Stile zu erstellen. Um die Anwendung DRY zu halten (Don't Repeat Yourself), habe ich mich entschieden, die Radiobuttons in JavaScript statt in Rails zu generieren, um eine bessere Wartbarkeit zu erreichen, da die Stile rein für die Clientseite sind.

Kontext

Mapbox-Stile sind vergleichbar mit Google Maps-Layern (Gelände, Verkehr, Fahrradfahren). Diese Radiobuttons werden verwendet, um zwischen verschiedenen Ansichten zu wechseln. Derzeit besteht die Anwendung aus den folgenden Stilen: Satellit und Outdoor. Aber es kann neue Kartenstile geben, wie z.B. Swisstopo-Kacheln.

Die Daten

Ich habe das folgende Stil-Objekt erstellt, um die Stil-URL und das entsprechende Icon zu speichern:

  export type MapboxStyle = {  
  url: string  
  icon: string  
}

export const MAPBOX_STYLES: Record<string, MapboxStyle> = {  
  outdoor: {  
    url: 'mapbox://styles/...',  
    icon: 'fa fa-hiking',  
  },  
  satellite: {  
    url: 'mapbox://styles/...',  
    icon: 'fa fa-satellite',  
  },  
}

export const DEFAULT_MAPBOX_STYLE_KEY = 'outdoor'
export const DEFAULT_MAPBOX_STYLE = MAPBOX_STYLES[DEFAULT_MAPBOX_STYLE_KEY]

Das HTML

Das ist die Vorlage, die ich verwende, um die einzelnen Radiobuttons zu erstellen:

  <div id="map-wrapper" class="map__wrapper"
     data-controller="map markerRelocation mapboxLayers"
     data-map-locations-path-value="<%= locations_url(format: :json) %>"
     data-markerRelocation-map-outlet="#map-wrapper"
     data-mapboxLayers-map-outlet="#map-wrapper"
     data-map-access-token-value="<%= ENV.fetch('MAPBOX_ACCESS_TOKEN') %>">
  <div id="map" class="map__container flex-grow-1"></div>
  <div data-mapboxLayers-target="mapboxStyleRadioWrapper" class="mapbox-style-radio__wrapper"></div>

  <script data-mapboxLayers-target="mapboxStyleRadioTemplate" type="x-tmpl-mustache">
    <div class="mapbox-style-radio">  
      <input id="{{radio_id}}" class="form-check-input" type="radio" name="map-style"
            value="{{mapbox_style_url}}"
            data-action="mapboxLayers#swapLayer"
            {{#checked}} checked {{/checked}}>  
      <label for="{{radio_id}}" class="form-check-label">  
        {{style_name}}  
        <i class="fa-solid {{icon_class}}"></i>  
      </label>  
    </div>  
  </script>
</div>

Werte interpolieren

In einem vorherigen Abschnitt haben wir die entsprechende HTML-Vorlage für den Radiobutton mit individuellen Vorlagenattributen erstellt. Um die Vorlage zu rendern, holen wir zuerst das Innere HTML des Vorlagentags und verwenden dann Mustache.js, um den interpolierten String zu generieren:

  import { Controller } from '@hotwired/stimulus'
import MapController from './map_controller'

export default class MapboxLayers extends Controller {
  static outlets = ['map']

  static targets = ['mapboxStyleRadioTemplate', 'mapboxStyleRadioWrapper']

  declare readonly mapOutlet: MapController

  declare readonly mapboxStyleRadioTemplateTarget: HTMLTemplateElement

  declare readonly mapboxStyleRadioWrapperTarget: HTMLDivElement

  connect() {
    this.createLayers()
  }

  createLayers() {
    this.mapboxStyleRadioWrapperTarget.innerHTML = ''

    Object.keys(MAPBOX_STYLES).forEach((key) => {
      const { url, icon } = MAPBOX_STYLES[key]
      
      const options = {
        mapboxStyleUrl: url,
        iconClass: icon,
        radioId: `mapbox-style-radio-${key}`,
        styeleName: I18n.map[`style_${key}`],
        checked: DEFAULT_MAPBOX_STYLE_KEY === key
      }

      const htmlString = Mustache.render(template, options)

      this.mapboxStyleRadioWrapperTarget.insertAdjacentHTML('beforeend', htmlString)
    })
  }

  swapLayer(event: Event) {
    const target = event.target as HTMLInputElement
    const styleURL = target.value

    if (!styleURL) throw new Error('Style-URL nicht auf Element angegeben')

    this.mapOutlet.map.setStyle(styleURL)
  }
}

Warten Sie, warum ist es jetzt ein <script>-Tag?

Das Überprüfen von Radiobuttons oder das Hinzufügen von Booleschen Attributen ist im Allgemeinen in JavaScript eher lästig, hauptsächlich, weil checked="false" und checked="true" denselben Effekt haben. Das Boolesche Attribut muss vorhanden sein, damit das Attribut aktiv ist.

Das <template> HTML Element ist ein Mechanismus zum Halten von HTML, das beim Laden einer Seite zunächst nicht gerendert wird, aber später zur Laufzeit mit JavaScript instanziiert werden kann.

Es ist erwähnenswert, dass ein Boolesches Attribut wahr ist, wenn es vorhanden ist, und falsch, nur wenn es fehlt.

https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML

Wie auch immer, ich verwende ein <script>-Tag für meine Vorlage, um spezielle Moustache-Interpolationen zu verwenden. Das Boolesche checked="" wird hinzugefügt, wenn options["checked"] richtig ist. Das ist alles! Dies ist der Code, auf den ich mich im Besonderen beziehe:

  ...
{{#checked}} checked {{/checked}}>

Fazit

Dieser Artikel behandelt verschiedene Ansätze zur Darstellung von HTML in JavaScript und empfiehlt die Verwendung von Vorlagentags und einem Vorlagengenerator, insbesondere Mustache.js, für optimale Lesbarkeit, Wartbarkeit und Trennung der Anliegen innerhalb einer Rails-Anwendung. Ich habe gezeigt, wie diese Methode mit einem realen Beispiel umgesetzt werden kann, das Radiobuttons für Mapbox-Stile beinhaltet, und die Vorteile der Verwendung dieses Ansatzes hervorgehoben.