Ein Freund kontaktierte mich einmal und fragte, ob ich eine Möglichkeit hätte, ein HTML-Element innerhalb eines Vue-Template-Blocks dynamisch in ein anderes zu ändern. Zum Beispiel, ein <div>-Element basierend auf bestimmten Kriterien in ein <span>-Element umzuwandeln. Der Trick bestand darin, dies zu tun, ohne auf eine Reihe von v-if und v-else-Code zurückzugreifen.
Ich habe nicht viel darüber nachgedacht, weil ich keinen starken Grund sah, so etwas zu tun; es kommt einfach nicht so oft vor. Später am selben Tag meldete er sich jedoch erneut und erzählte mir, dass er gelernt hatte, Elementtypen zu ändern. Er wies aufgeregt darauf hin, dass Vue eine integrierte Komponente hat, die als dynamisches Element auf genau die Weise verwendet werden kann, wie er es brauchte.
Diese kleine Funktion kann den Code im Template schön und übersichtlich halten. Sie kann den v-if- und v-else-Überschuss auf eine kleinere Code-Menge reduzieren, die leichter zu verstehen und zu warten ist. Dies ermöglicht es uns, Methoden oder berechnete Methoden zu verwenden, um sauber gecodete und doch ausgefeiltere Bedingungen im Skriptblock zu erstellen. Dort gehören solche Dinge hin: ins Skript, nicht in den Template-Block.
Die Idee für diesen Artikel entstand hauptsächlich, weil wir diese Funktion an mehreren Stellen im Designsystem verwenden, in dem ich arbeite. Zugegeben, es ist keine riesige Funktion und wird in der Dokumentation kaum erwähnt, zumindest soweit ich das beurteilen kann. Doch sie hat das Potenzial, bestimmte HTML-Elemente in Komponenten zu rendern.
Vue's integrierte <component>-Element
In Vue gibt es mehrere Funktionen, die einfache dynamische Änderungen der Ansicht ermöglichen. Eine davon ist die integrierte <component>-Element, die es Komponenten ermöglicht, dynamisch und nach Bedarf umgeschaltet zu werden. Sowohl in der Vue 2 als auch in der Vue 3-Dokumentation gibt es einen kleinen Hinweis zur Verwendung dieses Elements mit HTML-Elementen; das ist der Teil, den wir nun untersuchen werden.
Die Idee ist, diesen Aspekt des <component>-Elements zu nutzen, um gängige HTML-Elemente auszutauschen, die in ihrer Natur ähnlich sind; aber mit unterschiedlicher Funktionalität, Semantik oder Optik. Die folgenden einfachen Beispiele zeigen das Potenzial dieses Elements, um Vue-Komponenten ordentlich und sauber zu halten.
Button oder Link?
Buttons und Links werden oft austauschbar verwendet, aber es gibt große Unterschiede in ihrer Funktionalität, Semantik und sogar Optik. Im Allgemeinen ist ein Button (<button>) für eine interne Aktion in der aktuellen Ansicht gedacht, die mit JavaScript-Code verbunden ist. Ein Link (<a>) hingegen soll auf eine andere Ressource verweisen, entweder auf dem Host-Server oder eine externe Ressource; meistens Webseiten. Single-Page-Anwendungen neigen dazu, mehr auf Buttons als auf Links zu setzen, aber es besteht Bedarf an beidem.
Links werden oft visuell wie Buttons gestylt, ähnlich wie Bootstrap's .btn-Klasse, die ein button-ähnliches Aussehen erzeugt. In diesem Sinne können wir leicht eine Komponente erstellen, die basierend auf einer einzigen Prop zwischen den beiden Elementen wechselt. Die Komponente ist standardmäßig ein Button, aber wenn eine href-Prop übergeben wird, wird sie als Link gerendert.
Hier ist die <component> im Template
<component
:is="element"
:href="href"
class="my-button"
>
<slot />
</component>
Dieses gebundene is-Attribut verweist auf eine berechnete Methode namens element und das gebundene href-Attribut verwendet die treffend benannte href-Prop. Dies nutzt das normale Verhalten von Vue aus, dass das gebundene Attribut nicht im gerenderten HTML-Element erscheint, wenn die Prop keinen Wert hat. Der Slot liefert den inneren Inhalt, unabhängig davon, ob das endgültige Element ein Button oder ein Link ist.
Die berechnete Methode ist einfach gehalten
element () {
return this.href ? 'a' : 'button';
}
Wenn eine href-Prop vorhanden ist, wird ein <a>-Element verwendet; andernfalls erhalten wir einen <button>.
<my-button>this is a button</my-button>
<my-button href="https://www.css-tricks.com">this is a link</my-button>
Das HTML wird wie folgt gerendert
<button class="my-button">this is a button</button>
<a href="https://www.css-tricks.com" class="my-button">this is a link</a>
In diesem Fall könnte man erwarten, dass diese beiden visuell ähnlich sind, aber für semantische und barrierefreie Zwecke sind sie eindeutig unterschiedlich. Das bedeutet nicht, dass die beiden ausgegebenen Elemente visuell gleich gestylt sein *müssen*. Sie könnten entweder das Element mit dem Selektor div.my-button im Style-Block verwenden oder eine dynamische Klasse erstellen, die sich basierend auf dem Element ändert.
Das Gesamtziel ist es, die Dinge zu vereinfachen, indem man einer Komponente erlaubt, je nach Bedarf als zwei verschiedene HTML-Elemente gerendert zu werden – ohne v-if oder v-else!
Geordnete oder ungeordnete Liste?
Eine ähnliche Idee wie beim Button-Beispiel oben können wir eine Komponente haben, die verschiedene Listenelemente ausgibt. Da eine ungeordnete Liste und eine geordnete Liste die gleichen Listenelement-<li>-Elemente als Kinder verwenden, ist das einfach genug; wir tauschen einfach <ul> und <ol> aus. Selbst wenn wir eine Option für eine Beschreibungsliste, <dl>, hätten, ist das leicht zu bewerkstelligen, da der Inhalt nur ein Slot ist, der <li>-Elemente oder <dt>/<dd>-Kombinationen akzeptieren kann.
Der Template-Code ist weitgehend derselbe wie im Button-Beispiel
<component
:is="element"
class="my-list"
>
<slot>No list items!</slot>
</component>
Beachten Sie den Standardinhalt im Slot-Element, dazu komme ich gleich.
Es gibt eine Prop für den zu verwendenden Listentyp, die standardmäßig auf <ul> gesetzt ist
props: {
listType: {
type: String,
default: 'ul'
}
}
Auch hier gibt es eine berechnete Methode namens element
element () {
if (this.$slots.default) {
return this.listType;
} else {
return 'div';
}
}
In diesem Fall testen wir, ob der Standard-Slot existiert, d.h. ob er Inhalt zum Rendern hat. Wenn ja, wird der über die listType-Prop übergebene Listentyp verwendet. Andernfalls wird das Element zu einem <div>, das die Nachricht "Keine Listenelemente!" innerhalb des Slot-Elements anzeigt. Auf diese Weise wird, wenn keine Listenelemente vorhanden sind, das HTML nicht als Liste mit einem Element gerendert, das besagt, dass keine Elemente vorhanden sind. Dieser letzte Aspekt liegt bei Ihnen, obwohl es gut ist, die Semantik einer Liste mit keinen offensichtlich gültigen Elementen zu berücksichtigen. Eine weitere Überlegung ist die potenzielle Verwirrung für Barrierefreiheits-Tools, die dies als eine Liste mit einem Element interpretieren, das nur besagt, dass keine Elemente vorhanden sind.
Ähnlich wie im obigen Button-Beispiel könnten Sie auch jede Liste unterschiedlich stylen. Dies könnte auf Selektoren basieren, die das Element mit dem Klassennamen ul.my-list ansprechen. Eine weitere Option ist die dynamische Änderung des Klassennamens basierend auf dem gewählten Element.
Dieses Beispiel folgt einer BEM-ähnlichen Klassenbenennungsstruktur
<component
:is="element"
class="my-list"
:class="`my-list__${element}`"
>
<slot>No list items!</slot>
</component>
Die Verwendung ist genauso einfach wie im vorherigen Button-Beispiel
<my-list>
<li>list item 1</li>
</my-list>
<my-list list-type="ol">
<li>list item 1</li>
</my-list>
<my-list list-type="dl">
<dt>Item 1</dt>
<dd>This is item one.</dd>
</my-list>
<my-list></my-list>
Jede Instanz rendert das angegebene Listenelement. Die letzte Instanz ergibt jedoch ein <div>, das keine Listenelemente anzeigt, weil es eben keine zu zeigende Liste gibt!
Manche fragen sich vielleicht, warum eine Komponente erstellt wird, die zwischen verschiedenen Listentypen wechselt, wenn es auch einfache HTML-Elemente sein könnten. Während es Vorteile haben kann, Listen innerhalb einer Komponente aus Styling- und Wartungsgründen zu belassen, könnten auch andere Gründe in Betracht gezogen werden. Nehmen wir zum Beispiel an, einige Funktionalitäten würden an die verschiedenen Listentypen gebunden? Vielleicht eine Sortierung einer <ul>-Liste, die zu einer <ol> wechselt, um die Sortierreihenfolge anzuzeigen, und dann wieder zurückwechselt, wenn sie fertig ist?
Jetzt kontrollieren wir die Elemente
Obwohl diese beiden Beispiele im Wesentlichen das Root-Element einer Komponente ändern, betrachten wir tiefere Ebenen innerhalb einer Komponente. Zum Beispiel einen Titel, der sich von einem <h2> zu einem <h3> ändern muss, basierend auf bestimmten Kriterien.
Wenn Sie feststellen, dass Sie ternäre Lösungen verwenden müssen, um Dinge jenseits einiger Attribute zu steuern, empfehle ich, bei v-if zu bleiben. Mehr Code schreiben zu müssen, um Attribute, Klassen und Eigenschaften zu verwalten, verkompliziert den Code nur mehr als v-if. In diesen Fällen sorgt v-if auf lange Sicht für einfacheren Code, und einfacherer Code ist leichter zu lesen und zu warten.
Wenn Sie eine Komponente erstellen und ein einfaches v-if zum Umschalten zwischen Elementen vorhanden ist, sollten Sie diesen kleinen Aspekt einer Hauptfunktion von Vue in Betracht ziehen.
Erweiterung der Idee, ein flexibles Kartensystem
Betrachten wir alles, was wir bisher besprochen haben, und wenden es auf eine flexible Kartenkomponente an. Dieses Beispiel einer Kartenkomponente ermöglicht es, drei verschiedene Kartentypen an bestimmten Stellen im Layout eines Artikels zu platzieren.
- Hero-Karte: Diese wird am oberen Rand der Seite erwartet und soll mehr Aufmerksamkeit erregen als andere Karten.
- Call-to-Action-Karte: Diese wird als Reihe von Benutzeraktionen vor oder innerhalb des Artikels verwendet.
- Info-Karte: Diese ist für Pull-Quotes gedacht.
Betrachten Sie jede dieser Karten als Teil eines Designsystems, und die Komponente steuert die HTML-Struktur für Semantik und Styling.
Im obigen Beispiel sehen Sie die Hero-Karte oben, gefolgt von einer Reihe von Call-to-Action-Karten, und dann – etwas weiter unten beim Scrollen – die Info-Karte auf der rechten Seite.
Hier ist der Template-Code für die Kartenkomponente
<component :is="elements('root')" :class="'custom-card custom-card__' + type" @click="rootClickHandler">
<header class="custom-card__header" :style="bg">
<component :is="elements('header')" class="custom-card__header-content">
<slot name="header"></slot>
</component>
</header>
<div class="custom-card__content">
<slot name="content"></slot>
</div>
<footer class="custom-card__footer">
<component :is="elements('footer')" class="custom-card__footer-content" @click="footerClickHandler">
<slot name="footer"></slot>
</component>
</footer>
</component>
Es gibt drei "component"-Elemente in der Karte. Jedes repräsentiert ein spezifisches Element innerhalb der Karte, wird aber je nach Kartentyp geändert. Jede Komponente ruft die Methode elements() mit einem Parameter auf, der angibt, welcher Abschnitt der Karte den Aufruf tätigt.
Die Methode elements() lautet
elements(which) {
const tags = {
hero: { root: 'section', header: 'h1', footer: 'date' },
cta: { root: 'section', header: 'h2', footer: 'div' },
info: { root: 'aside', header: 'h3', footer: 'small' }
}
return tags[this.type][which];
}
Es gibt wahrscheinlich mehrere Möglichkeiten, dies zu handhaben, aber Sie müssen den Weg gehen, der mit den Anforderungen Ihrer Komponente übereinstimmt. In diesem Fall gibt es ein Objekt, das die HTML-Element-Tags für jeden Abschnitt in jedem Kartentyp speichert. Dann gibt die Methode das benötigte HTML-Element basierend auf dem aktuellen Kartentyp und dem übergebenen Parameter zurück.
Für die Styles habe ich eine Klasse auf das Root-Element der Karte angewendet, basierend auf dem Kartentyp. Das macht es einfach genug, das CSS für jeden Kartentyp basierend auf den Anforderungen zu erstellen. Sie könnten das CSS auch basierend auf den HTML-Elementen selbst erstellen, aber ich bevorzuge Klassen. Zukünftige Änderungen an der Kartenkomponente könnten die HTML-Struktur ändern und weniger wahrscheinlich die Logik ändern, die die Klasse erstellt.
Die Karte unterstützt auch ein Hintergrundbild im Header für die Hero-Karte. Dies geschieht mit einer einfachen Berechnung, die auf dem Header-Element angewendet wird: bg. Dies ist die Berechnung
bg() {
return this.background ? `background-image: url(${this.background})` : null;
}
Wenn eine Bild-URL in der background-Prop angegeben ist, gibt die Berechnung einen String für einen Inline-Stil zurück, der das Bild als Hintergrundbild anwendet. Eine ziemlich einfache Lösung, die leicht robuster gestaltet werden könnte. Zum Beispiel könnte sie Unterstützung für benutzerdefinierte Farben, Farbverläufe oder Standardfarben haben, falls kein Bild bereitgestellt wird. Es gibt eine Vielzahl von Möglichkeiten, die dieses Beispiel nicht abdeckt, da jeder Kartentyp potenziell eigene optionale Props für Entwickler haben könnte.
Hier ist die Hero-Karte aus dieser Demo
<custom-card type="hero" background="https://picsum.photos/id/237/800/200">
<template v-slot:header>Article Title</template>
<template v-slot:content>Lorem ipsum...</template>
<template v-slot:footer>January 1, 2011</template>
</custom-card>
Sie sehen, dass jeder Abschnitt der Karte seinen eigenen Slot für Inhalte hat. Und um die Dinge einfach zu halten, wird nur Text in den Slots erwartet. Die Kartenkomponente kümmert sich um das benötigte HTML-Element, ausschließlich basierend auf dem Kartentyp. Wenn die Komponente nur Text erwartet, ist die Verwendung der Komponente eher simpel. Sie ersetzt die Notwendigkeit, Entscheidungen über die HTML-Struktur zu treffen, und die Karte wird einfach implementiert.
Zum Vergleich, hier sind die anderen beiden Typen, die in der Demo verwendet werden
<custom-card type="cta">
<template v-slot:header>CTA Title One</template>
<template v-slot:content>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</template>
<template v-slot:footer>footer</template>
</custom-card>
<custom-card type="info">
<template v-slot:header>Here's a Quote</template>
<template v-slot:content>“Maecenas ... quis.”</template>
<template v-slot:footer>who said it</template>
</custom-card>
Beachten Sie erneut, dass jeder Slot nur Text erwartet, da jeder Kartentyp seine eigenen HTML-Elemente generiert, wie von der elements()-Methode definiert. Wenn in Zukunft entschieden wird, dass ein anderes HTML-Element verwendet werden soll, ist dies eine einfache Sache der Aktualisierung der Komponente. Das Einbauen von Funktionen für Barrierefreiheit ist eine weitere potenzielle zukünftige Aktualisierung. Sogar Interaktionsfunktionen können basierend auf den Kartentypen erweitert werden.
Die Macht liegt in der Komponente, die in der Komponente steckt
Das seltsam benannte <component>-Element in Vue-Komponenten war für einen Zweck gedacht, aber wie es oft geschieht, hat es einen kleinen Nebeneffekt, der es auf andere Weise nützlich macht. Das <component>-Element war dazu gedacht, Vue-Komponenten dynamisch innerhalb einer anderen Komponente bei Bedarf umzuschalten. Eine grundlegende Vorstellung davon könnte ein Tab-System sein, um zwischen Komponenten zu wechseln, die als Seiten fungieren; was tatsächlich in der Vue-Dokumentation demonstriert wird. Dennoch unterstützt es das Gleiche mit HTML-Elementen.
Dies ist ein Beispiel für eine neue Technik, die von einem Freund geteilt wurde und zu einem überraschend nützlichen Werkzeug im Gürtel von Vue-Funktionen geworden ist, die ich verwendet habe. Ich hoffe, dieser Artikel vermittelt die Ideen und Informationen über diese kleine Funktion, damit Sie sie in Ihren eigenen Vue-Projekten nutzen können.
Angenehm zu lesen.
Ich bin einmal einen ähnlichen Weg gegangen.
Was mich schließlich stolpern ließ, war der beste Weg, die beabsichtigte Methode und die Daten, die sie benötigt, zu übermitteln.
Ich habe bemerkt, dass Sie... in der Komponente verwendet haben
Was passiert da?
Das ist nur ein Klick-Handler für die gesamte Karte. Wahrscheinlich nicht für jeden Fall nützlich, aber ich hatte wahrscheinlich einen Gedanken dazu, als ich den Prototyp baute. Je nachdem, wie tief Sie in die Handhabung von Ereignissen gehen möchten, könnte jeder Aspekt der gesamten Komponente einen Klick-Handler haben. Dann wäre es eine Frage, den aktuellen Zustand der Karte und das erwartete Ergebnis zu bestimmen. Man könnte das sogar einfach vom Root-Element selbst aus tun. Die Handhabung von Klick-Ereignissen bietet bei dieser Art von Build sehr viele Optionen, und ich wollte nicht zu tief in diesen Aspekt eintauchen.