Vue - Components: Unterschied zwischen den Versionen

Aus Wikizone
Wechseln zu: Navigation, Suche
 
(30 dazwischenliegende Versionen von 2 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
 
Siehe auch  
 
Siehe auch  
 +
[[Vue.js]]
 
  [[Vue CLI]]
 
  [[Vue CLI]]
  
Zeile 227: Zeile 228:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
==== Fallthrough props ====
+
==== Fallthrough props - $attr Objekt ====
 
Angenommen Du hast eine BaseButton.vue Komponente ohne definierte Props.
 
Angenommen Du hast eine BaseButton.vue Komponente ohne definierte Props.
 
'''Im Parent kannst Du trotzdem Attribute und Events''' nutzen...
 
'''Im Parent kannst Du trotzdem Attribute und Events''' nutzen...
Zeile 235: Zeile 236:
 
  this.$attrs
 
  this.$attrs
 
zugreifen kannst. Dies kann z.b. bei Komponenten mit nur geringer Funktionalität nützlich sein, die eher zur reinen Ausgabe dienen.
 
zugreifen kannst. Dies kann z.b. bei Komponenten mit nur geringer Funktionalität nützlich sein, die eher zur reinen Ausgabe dienen.
 +
 +
==== Fallthrough props - root Element ====
 +
Fallthrough Properties ermöglichen es Properties und Events in Komponenten zu nutzen.
 +
 +
Wenn man Properties oder Events in einer Komponente nutzt werden Sie von Vue automatisch  per default auf das Root Element angewendet.
 +
 +
Einsatz der BaseButton Komponente
 +
<syntaxhighlight lang="html5">
 +
<template>
 +
<base-button @click="setSelectedTab('add-resource')">Add Resource</base-button>
 +
</template>
 +
</syntaxhighlight>
 +
 +
Der click Listener wird hier automatisch auf das Root Element der Button Komponente angewendet - nämlich den button tag.
 +
 +
Definition der BaseButton Komponente:
 +
<syntaxhighlight lang="html5">
 +
<template>
 +
    <button :type="type" :class="mode">
 +
        <slot>Label goes here</slot>
 +
    </button>
 +
</template>
 +
</syntaxhighlight>
  
 
==== Alle props in einem Objekt binden ====
 
==== Alle props in einem Objekt binden ====
Zeile 369: Zeile 393:
 
         }
 
         }
 
     }
 
     }
 +
}
 +
</script>
 +
</syntaxhighlight>
 +
 +
=== provide und inject ===
 +
Manchmal hat man eine Art "Passthrough" Komponenten.
 +
 +
Dies bedeutet, dass diese Komponenten keine eigene Logik für eingehende props oder ausgehende emits zur Verfügung stellen sondern diese nur an die nächste Ebene (props an die Kind-Komponente und emit an die Elternelemente weitergeben.
 +
 +
Beispiel:
 +
 +
'''App.vue'''
 +
<syntaxhighlight lang="html5">
 +
<template>
 +
<!-- ... -->
 +
<knowledge-base :topics="topics" @select-topic="activateTopic"></knowledge-base>
 +
<!-- ... -->
 +
</template>
 +
<script>
 +
export default {
 +
  data() {
 +
    return {
 +
      topics: [
 +
        {
 +
          id: 'basics',
 +
          title: 'The Basics',
 +
          description: 'Core Vue basics you have to know',
 +
          fullText:
 +
            'Vue is a great framework and it has a couple of key concepts: Data binding, events, components and reactivity - that should tell you something!',
 +
        },
 +
        {
 +
          id: 'components',
 +
          title: 'Components',
 +
          description:
 +
            'Components are a core concept for building Vue UIs and apps',
 +
          fullText:
 +
            'With components, you can split logic (and markup) into separate building blocks and then combine those building blocks (and re-use them) to build powerful user interfaces.',
 +
        },
 +
      ],
 +
      activeTopic: null,
 +
    };
 +
  },
 +
  methods: {
 +
    activateTopic(topicId) {
 +
      this.activeTopic = this.topics.find((topic) => topic.id === topicId);
 +
    },
 +
  },
 +
};
 +
</script>
 +
</syntaxhighlight>
 +
 +
'''KnowledgeBase.vue'''
 +
Hier werden die Props und das event quasi nur durchgeleitet.
 +
<syntaxhighlight lang="html5">
 +
<template>
 +
<knowledge-grid :topics="topics" @select-topic="$emit('select-topic', $event)"></knowledge-grid>
 +
</template>
 +
<script>
 +
export default { props: ['topics'], emits: ['select-topic'],};
 +
</script>
 +
</syntaxhighlight>
 +
 +
'''KnowledgeGrid.vue'''
 +
Hier werden die Props zwar in der for Schleife genutzt, das select-topic event wird durchgeleitet.
 +
<syntaxhighlight lang="html5">
 +
<template>
 +
    <knowledge-element
 +
      v-for="topic in topics"
 +
      :key="topic.id"
 +
      :id="topic.id"
 +
      :topic-name="topic.title"
 +
      :description="topic.description"
 +
      @select-topic="$emit('select-topic', $event)"
 +
    ></knowledge-element>
 +
</template
 +
<script>
 +
export default { props: ['topics'], emits: ['select-topic']};
 +
</script>
 +
</syntaxhighlight>
 +
 
 +
'''KnowledgeElement.vue'''
 +
Hier wird schließlich das Event select-topic erzeugt.
 +
<syntaxhighlight lang="html5">
 +
<button @click="$emit('select-topic', id)">Learn More</button>
 +
</syntaxhighlight>
 +
 +
==== provide und inject eine Eigenschaft ====
 +
Mit dem Provide Objekt kannst du Properties bereitstellen und mit inject in einer anderen Komponente nutzbar machen. Hinweis: Du kannst nur '''in Richtung der Kindelemente''' injecten. Im Beispiel oben möchten wir die topics direkt zu KnowledgeGrid.vue senden. Das geht folgendermaßen
 +
* provide Object in App.vue hinzufügen und Daten hinzufügen
 +
* statt dem props Object die Daten mit inject nutzen
 +
In App.vue ergänzen wir die configuration objects um das provide object (z.b. unter dem data) object
 +
<syntaxhighlight lang="javascript">
 +
//..
 +
provide() {
 +
    return {topics:this.topics}
 +
},
 +
</syntaxhighlight>
 +
Wir stellen also eine Eigenschaft "topics" zum injecten (nutzen) bereit. Der Name ist frei wählbar. Hier kopieren wir einfach das schon vorhandene topics Ojekt aus data. Man kann aber auch eigene Objekte definieren.
 +
 +
Um das jetzt in KnowledgeGrid zu nutzen müssen wir dort das inject Objekt nutzen. Es ersetzt in diesem Fall vollständig die props. Daher schreiben wir statt props einfach inject:
 +
inject: ['topics'],
 +
 +
==== provide und inject für custom events ====
 +
provide und inject funktionieren auch mit Methoden. So können einfach die Methode (oder besser den Zeiger auf eine Methode) injekten.
 +
'''App.vue - provide object'''
 +
selectTopic: this.activateTopic // point at the method
 +
In KnowledgeElement injekten wir:
 +
'''KnowledgeElement.vue'''
 +
inject: ['selectTopic'],
 +
und schreiben den button etwas um:
 +
  <button @click="selectTopic(id)">Learn More</button>
 +
==== Komplettes Beispiel ====
 +
App.vue
 +
<syntaxhighlight lang="html5">
 +
<template>
 +
  <div>
 +
    <active-element
 +
      :topic-title="activeTopic && activeTopic.title"
 +
      :text="activeTopic && activeTopic.fullText"
 +
    ></active-element>
 +
    <knowledge-base></knowledge-base>
 +
  </div>
 +
</template>
 +
 +
<script>
 +
export default {
 +
  data() {
 +
    return {
 +
      topics: [
 +
        {
 +
          id: 'basics',
 +
          title: 'The Basics',
 +
          description: 'Core Vue basics you have to know',
 +
          fullText:
 +
            'Vue is a great framework and it has a couple of key concepts: Data binding, events, components and reactivity - that should tell you something!',
 +
        },
 +
        {
 +
          id: 'components',
 +
          title: 'Components',
 +
          description:
 +
            'Components are a core concept for building Vue UIs and apps',
 +
          fullText:
 +
            'With components, you can split logic (and markup) into separate building blocks and then combine those building blocks (and re-use them) to build powerful user interfaces.',
 +
        },
 +
      ],
 +
      activeTopic: null,
 +
    };
 +
  },
 +
  provide() {
 +
    return {
 +
      topics: this.topics,
 +
      selectTopic: this.activateTopic // point at the method
 +
    }
 +
  },
 +
  methods: {
 +
    activateTopic(topicId) {
 +
      this.activeTopic = this.topics.find((topic) => topic.id === topicId);
 +
    },
 +
  },
 +
  // use a hook to simulate data changes
 +
  // this proves, that data in provide is updated too
 +
  mounted(){
 +
    setTimeout(
 +
      () => {
 +
        this.topics.push({
 +
          id: 'events',
 +
          title: 'Events',
 +
          description:
 +
            'Events are important in vue',
 +
          fullText:
 +
            'Events allow you to trigger code on demand.',
 +
        })
 +
      },
 +
      3000
 +
    )
 +
  }
 +
};
 +
</script>
 +
 +
<style>
 +
/* styling goes here */
 +
</style>
 +
</syntaxhighlight>
 +
 +
'''KnowledgeBase.vue'''
 +
<syntaxhighlight lang="html5">
 +
<template>
 +
  <section>
 +
    <h2>Select a Topic</h2>
 +
    <knowledge-grid></knowledge-grid>
 +
  </section>
 +
</template>
 +
 +
<script>
 +
export default {
 +
};
 +
</script>
 +
</syntaxhighlight>
 +
 +
'''KnowledgeGrid.vue'''
 +
<syntaxhighlight lang="html5">
 +
<template>
 +
  <ul>
 +
    <knowledge-element
 +
      v-for="topic in topics"
 +
      :key="topic.id"
 +
      :id="topic.id"
 +
      :topic-name="topic.title"
 +
      :description="topic.description"
 +
    ></knowledge-element>
 +
  </ul>
 +
</template>
 +
 +
<script>
 +
export default {
 +
  inject: ['topics'],
 +
};
 +
</script>
 +
</syntaxhighlight>
 +
 +
'''KnowledgeElement.vue'''
 +
<syntaxhighlight lang="html5">
 +
<template>
 +
  <li>
 +
    <h3>{{ topicName }}</h3>
 +
    <p>{{ description }}</p>
 +
    <button @click="selectTopic(id)">Learn More</button>
 +
  </li>
 +
</template>
 +
 +
<script>
 +
export default {
 +
  inject: ['selectTopic'], // we expect a function named selectTopic
 +
  props: ['id', 'topicName', 'description'],
 +
  emits: ['select-topic'],
 +
};
 +
</script>
 +
</syntaxhighlight>
 +
 +
== Global / Local ==
 +
Bisher haben wir Komponenten in main.js global registriert. Das ist sinnvoll bei "General Purpose Components" oder wenn Sie in vielen anderen Komponenten eingesetzt werden sollen.
 +
Global
 +
* Überall verfügbar, auch wenn nicht genutzt
 +
* Muss beim ersten Aufruf der Seite vom Browser geladen werden (Performance)
 +
main.js
 +
import BaseBadge from './components/BaseBadge.vue';
 +
const app = createApp(App);
 +
app.component('base-badge', BaseBadge);
 +
app.mount('#app');
 +
Oft ist es besser Komponenten nur dort zu registrieren wo sie benötigt werden.
 +
App.vue - <script>
 +
<syntaxhighlight lang="javascript">
 +
import TheHeader from './components/TheHeader.vue';
 +
import BadgeList from './components/BadgeList.vue';
 +
import UserInfo from './components/UserInfo.vue';
 +
export default {
 +
  components: {
 +
    TheHeader,
 +
    // or 'the-header': TheHeader,
 +
    // or TheHeader : TheHeader
 +
    BadgeList,
 +
    UserInfo,
 +
  },
 +
</syntaxhighlight>
 +
Diese Komponenten sind nur in App.vue verfügbar. Du kannst Sie natürlich über import auch in weiteren Komponenten nutzen.
 +
 +
=== scoped styling ===
 +
Mit dem Attribut scoped werden Styles nur für die Komponente angewendet.
 +
<style scoped>
 +
Best practice: Globale Styles in App.vue
 +
 +
== Slots ==
 +
Slots sind eine Art Wrapper für eine Kind-Komponente.
 +
 +
Komponenten können in einem Template nicht ineinander Verschachtelt werden. Manchmal möchte man aber genau das haben um z.B. ein gestyltes Element (z.b. eine Card) wiederverwenden zu können. Der Inhalt soll aber über eine andere Komponente gerendert werden. So kann man z.B. Styles gut wiederverewnden.
 +
* Komponente registrieren (oft in main.js da man sie global haben möchte)
 +
* <slot></slot> Tag in die Slot Komponente einfügen
 +
* in der Kindkomponente einfach den custom Tag der Komponente als Wrapper nutzen
 +
Für solche Zwecke gibt es Slots.
 +
BaseCard.vue
 +
<syntaxhighlight lang="html5">
 +
<template>
 +
    <div>
 +
        <slot></slot>
 +
    </div>
 +
</template>
 +
<style scoped>
 +
div {
 +
  margin: 2rem auto;
 +
  max-width: 30rem;
 +
  border-radius: 12px;
 +
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
 +
  padding: 1rem;
 +
}
 +
</style>
 +
</syntaxhighlight>
 +
 +
ChildComponent.vue
 +
<syntaxhighlight lang="html5">
 +
<template>
 +
    <base-card>
 +
      <header>
 +
        <h3>{{ fullName }}</h3>
 +
        <base-badge :type="role" :caption="role.toUpperCase()"></base-badge>
 +
      </header>
 +
      <p>{{ infoText }}</p>
 +
  </base-card>
 +
</template>
 +
 +
<script>
 +
export default {
 +
  props: ['fullName', 'infoText', 'role'],
 +
};
 +
</script>
 +
 +
<style scoped>
 +
div header {
 +
  display: flex;
 +
  justify-content: space-between;
 +
  align-items: center;
 +
}
 +
</style>
 +
</syntaxhighlight>
 +
 +
So wie die props zum Transport von Daten dienen, so dienen die Slots zum Transport von dynamischem HTML Code zwischen den Komponenten.
 +
 +
=== Named Slots - v-slot===
 +
Mit Named Slots kann man mehrere Slots nutzen, und damit auch komplexere Templates erzeugen. Dazu gibt es die Direktive '''v-slot'''. Wir nutzen zusätzlich das Standard HTML Tag <template> Das bietet sich an, da es keinen Inhat - also nicht mal eine Leerzeile - ausgibt.
 +
v-slot:[slotname]
 +
statt v-slot: kann man auch den Shortcut # nehmen:
 +
#[slotname]
 +
 +
Beispiel BaseCard.vue
 +
<syntaxhighlight lang="html5">
 +
<template>
 +
    <div>
 +
        <header>
 +
            <slot name="headline"></slot>
 +
        </header>
 +
        <slot></slot>
 +
    </div>
 +
</template>
 +
...
 +
</syntaxhighlight>
 +
Kindkomponente
 +
<syntaxhighlight lang="html5">
 +
<template>
 +
    <base-card>
 +
      <template v-slot:headline>
 +
        <h3>{{ fullName }}</h3>
 +
        <base-badge :type="role" :caption="role.toUpperCase()"></base-badge>
 +
      </template>
 +
      <template v-slot:default>
 +
        <p>{{ infoText }}</p>
 +
      </template>
 +
  </base-card>
 +
</template>
 +
</syntaxhighlight>
 +
 +
=== Default Content für Slots ===
 +
Du kannst default Inhalt anzeigen wenn die Kindkomponente keinen Inhalt für einen Slot hat.
 +
<pre>
 +
<slot name="headline">
 +
<h2>This default fallback content be shown if no content is provided</h2>
 +
</slot>
 +
</pre>
 +
 +
=== v-if mit $slots ===
 +
Manche Slots möchte man nur anzeigen wenn auch Inhalt dafür vorhanden ist. Ansonsten soll auch das umgebende Tag nicht ausgegeben werden. Dies kann man mit v-if und der $slots Variablen erreichen.
 +
 +
$slots enthält die Daten für einen aktuellen Slot. Das Nutzen wir über ein v-if:
 +
<syntaxhighlight lang="html5">
 +
<header v-if="slots.headline">
 +
  <slot name="headline">
 +
  </slot>
 +
</header>
 +
</syntaxhighlight>
 +
So wird die headline nur ausgegeben, wenn sie auch vorhanden ist.
 +
 +
Tipp: Den Inhalt von $slots kannst du dir über einen Hook in der Konsole ausgeben lassen:
 +
    mounted(){
 +
        console.log(this.$slots.headline)
 +
    }
 +
=== scoped Slots ===
 +
Scoped Slots erlauben es Daten vom Slot der Komponente zur Parent App zu übertragen.
 +
 +
Du kannst einfach im Slot Tag Attribute definieren, die dann in einem Objekt in der Eltern Komponente genutzt werden können:
 +
 +
'''Komponente'''
 +
<syntaxhighlight lang="html5">
 +
<template>
 +
    <ul>
 +
        <li v-for="goal in goals" :key="goal">
 +
            <slot :item="goal" another-prop="hello"></slot>
 +
        </li>
 +
    </ul>
 +
</template>
 +
 +
<script>
 +
export default {
 +
    data() {
 +
        return {
 +
            goals: ['Finish the coarse', 'Learn Vue'],
 +
        }
 +
    }
 +
}
 +
</script>
 +
 +
</syntaxhighlight>
 +
 +
Und sie in der Elternkomponente auswerten.
 +
 +
'''Main App'''
 +
<syntaxhighlight lang="html5">
 +
    <course-goals>
 +
      <template #default="myProps">
 +
        <h4>{{ myProps.item }}</h4>
 +
        <p>{{ myProps.anotherProp }}</p>
 +
      </template>
 +
    </course-goals>
 +
</syntaxhighlight>
 +
 +
Im Template Tag kann man den Slot (hier default auslesen und in eine beliebige Variable (hier myProps) schreiben. '''Hinweis:''' in aktuellen Vue Versionen kann man camelCase Schreibweise nutzen und Vue konvertiert automatisch.
 +
 +
== Dynamic Components <component> ==
 +
Mit dem '''component Tag und dem is Attribut''' kannst du Komponenten dynamisch anzeigen. Das heißt an der Stelle von <component> können unterschiedliche Komponenten angezeigt werden.
 +
 +
<component v-bind:is="selectedComponent"></component>
 +
 +
Hier wird die Komponente mit dem Namen der in der Variablen selectedComponent steckt angezeigt.
 +
 +
=== <keep-alive> component ===
 +
<syntaxhighlight lang="html5">
 +
    <keep-alive>
 +
      <component v-bind:is="selectedComponent"></component>
 +
    </keep-alive>
 +
</syntaxhighlight>
 +
KeepAlive ist eine Vue-Interne Komponente, die oft mit dynamischen Komponenten in Verbindung eingesetzt wird. Da Komponenten wenn sie nicht mehr benötigt werden aus dem DOM entfernt werden, sind auch User-Inhalte wie die Eingabe in einem Input Feld gelöscht, wenn sie später wieder angezeigt werden.
 +
 +
Wrapt man die Komponente mit keep-alive werden die Inhalte vor dem Löschen gecacht und können später wieder hergestellt werden.
 +
 +
=== <teleport> ===
 +
Teleport ist eine eingebaute Vue Komponente. Mit dieser kannst Du DOM Elemente in andere Bereiche "teleportieren"
 +
 +
Dies kann z.b. für Modal Fenster sinnvoll sein, die man aus semantischen Gründen lieber auf der App oder <body> Ebene haben möchte anstatt tief vergraben im DOM
 +
 
 +
<teleport to="#selector"></teleport>
 +
 
 +
Teleport benötigt '''das Attribut to''', in dem man einen CSS-Selector angibt, wo das Element eingehängt werden soll.
 +
<syntaxhighlight lang="html5">
 +
<teleport to="#app">
 +
  <error-alert v-if="inputIsInvalid">
 +
    <h2>Input is invalid</h2>
 +
    <p>Please enter something.</p>
 +
    <button @click="confirmModal">Okay</button>
 +
  </error-alert>
 +
</teleport>
 +
</syntaxhighlight>
 +
 +
 +
== Custom Form Control ==
 +
Wenn man v-model in einer eigenen Component nutzt
 +
<my-component v-model="myProp"></my-component>
 +
macht Vue im Prinzip eine Kombi aus event listener und binding mit diesen Objekten:
 +
<my-component :model-value="" @update:modelValue=""></my-component>
 +
Die beiden Snipperts verhalten sich also identisch.
 +
 +
Sobald v-model eingesetzt wird stehen also auch das '''Emit und das Prop in der Komponente''' zur Verfügung:
 +
 +
props:['model-value'],
 +
emits: ['update:modelValue'],
 +
 +
Diese können wir nutzen wie hier im Beispiel. modelValue ist immer aktuell, da eine Aktualisierung über $emit an die Parent Komponente geht und von dieser wieder über das prop model-value zurückkommt und so auch in <template> bei modelValue aktualisiert wird.
 +
 +
Beispiel RatingControl.vue
 +
<syntaxhighlight lang="html5">
 +
<template>
 +
  <ul>
 +
    <li><button :class="{active: modelValue === 'poor'}" @click="activate('poor')" type="button">Poor</button></li>
 +
    <li><button :class="{active: modelValue === 'average'}" @click="activate('average')" type="button">Average</button></li>
 +
    <li><button :class="{active: modelValue === 'great'}" @click="activate('great')" type="button">Great</button></li>
 +
  </ul>
 +
</template>
 +
 +
<script>
 +
export default {
 +
    props:['model-value'],
 +
    emits: ['update:modelValue'],
 +
    methods:{
 +
        activate(option){
 +
            this.$emit('update:modelValue', option);
 +
        }
 +
    }
 +
}
 +
</script>
 +
</syntaxhighlight>
 +
Parent
 +
<syntaxhighlight lang="html5">
 +
<template>
 +
<rating-control v-model="rating"></rating-control>
 +
</template>
 +
 +
<script>
 +
import RatingControl from './RatingControl.vue'
 +
export default {
 +
  components:{
 +
    RatingControl
 +
  },
 +
  data(){
 +
    return {
 +
      rating: null
 +
    }
 +
  },
 +
  methods: {
 +
    submitForm(){
 +
      console.log('Rating: ' + this.rating);
 +
      this.rating = null;
 +
  },
 +
  }
 
}
 
}
 
</script>
 
</script>
 
</syntaxhighlight>
 
</syntaxhighlight>

Aktuelle Version vom 6. Januar 2021, 14:37 Uhr

Siehe auch

Vue.js
Vue CLI

Components sind wiederverwendbare Logikbausteine. Du kannst damit z.B. Logik so Kapseln, dass sie nicht durch andere Ereignisse oder Eigenschaften beeinflusst wird.

Durch die Aufteilung einer großen App in kleinere Bausteine wird sie weniger fehleranfällig und leichter wartbar und die Entwicklung gerade bei größeren Apps effektiver.

Eine Component sieht im Template wie ein neuer Tag aus. Damit es keine Namenskonflikte gibt verwendest Du als Name am Besten eine Bezeichnung mit zwei Worten und Bindestrich <contact-details>. Dies kommmt bei normalen Tags nicht vor.

Definiert wird eine Komponente mit der component() Methode. Technisch gesehen ist eine Komponente eine App in einer App. Daher erzeugt man sie auch ganz ähnlich:

const app = Vue.createApp({ //... }) // creation of an app
app.component('single-contact', { //... }) // creation of a component in this app

Im ersten Argument definiert man den Namen über den später als Tag (<single-contact>) zugegriffen werden kann. Im zweiten Argument übergibt man ein Objekt, dass wiederum eigene data, methods... Objekte enthalten kann die aber nur für diese Komponente gelten.

Component Template[Bearbeiten]

Die Component ist also eine Art Mini-App in der Hauptapp. Sie benötigt wie eine App ein Template. Es gibt mehrere Möglichkeiten das Template zu übergeben.

template Object[Bearbeiten]

app.component('friend-contact', {
    template: ` 
    <li>
        <h2>{{friend.name}}</h2>
        <button @click="toggleDetails()">Show Details</button>
        <ul v-if="detailsVisible">
            <li><strong>Phone:</strong> {{friend.phone}} </li>
            <li><strong>Email:</strong> {{friend.email}} </li>
        </ul>
    </li>
    `,// backticks allow multiline code

Quickstart - Component mit CLI[Bearbeiten]

Wenn man mit Components arbeitet bietet sich auch die Arbeit mit dem Vue CLI an. In Kurzform die Schritte die Du benötigst um eine Vue App und Komponenten zu nutzen. Komponente erstellen:

  • components/MyComponent.vue
    • <template> Abschnitt
    • <script>export default{}</script> Abschnitt

Komponente in Main App registrieren

  • App.vue erstellen
    • <template> und <script> wie oben erstellen
  • main.js
    • createApp wird beim Erstellen schon eingefügt
    • Hauptapp (i.d.R. App.vue) importieren
    • Config Oject der Komponenten importieren
    • app Instanz über createApp erzeugen
    • mit Funktion components in App Instanz registrieren
    • App mounten

Daten aus Main App senden

  • Daten definieren und im custom-tag binden
  • in der Komponente als props nutzen

Daten aus Komponente an Main App senden

  • data() der App definieren
  • Funktion mit this.$emit('my-emitter') definieren
  • Optional (good practice) in emits: ['my-emitter] listen

main.js

import { createApp } from 'vue';
import App from './App.vue';
import MyComponent from './components/MyComponent.vue';
// ...

const app = createApp(App);
app.component('my-component', MyComponent); // always use dashed names thus they can't clash with existing html names
// ...
app.mount('#app');

App.vue

<template>
    <h1>The App</h1>
    <my-component></my-component>
</template>

<script>
import MyComponent from './components/MyComponent.vue'
export default {
    components: {
        ActiveUser,
        UserData
    }
}
</script>
<template>
</template>
<script>
export default {
    
}
</script>
<style>
</style>

Single File Components mit Vue CLI[Bearbeiten]

Wenn man größere Apps bauen und effektiv mit Components arbeiten möchte hilft das Vue CLI und ein Development Setup mit Node.js und npm. Die folgenden Beispiele nutzen Single File Components. Schau unter

Vue CLI

nach wenn du nicht weißt wie man ein solches Setup aufbaut.

Component Communication[Bearbeiten]

Parent - Child Communication mit props[Bearbeiten]

https://v3.vuejs.org/guide/component-props.html

Mit props sendest Du Daten von der Parent App an die Child Component.

App.vue - <template> der Eltern App.

<ul>
         <friend-contact 
         name="Stephan" 
         phone-number="123" 
         email-address="my@email.tld">
         </friend-contact>
        <friend-contact 
         name="Finn" 
         phone-number="345" 
         email-address="my@mymail.tld">
         </friend-contact>
    </ul>

FriendContact.vue - <script> - Child App

props:[
  'name', 'phoneNumber', 'emailAddress'
],

FriendContact.vue - <template>

  <h2>{{name}}</h2>
  <button @click="toggleDetails"> {{detailsVisible ? 'Hide' : 'Show'}} Details </button>
  <ul v-if="detailsVisible">
    <li><strong>Phone: </strong>{{phoneNumber}}</li>
    <li><strong>Email: </strong>{{emailAddress}}</li>
  </ul>

Das props Objekt in der Komponente definiert welche Attribute die Komponente akzeptiert und erwartet. Im der Parent App übergibt man die Daten für diese props einfach als Attribute.

  • werden in props der component als camelCase , stehen aber als kebab-case im HTML zur Verfügung (du solltest sie auch so verwenden)
  • props stehen mit this.myProp auch in Methoden etc. zur Verfügung

Unidirectional Data Flow[Bearbeiten]

Daten die an die Kind App übertragen werden dürfen nicht mehr verändert werden.

Beispiel: Wenn Du eine prop isFavourite hast und Sie über einen Toggle Button verändern willst kannst du das nicht in der Child App verändern weil die Daten nur von Parent nach Child fließen dürfen. Vue gibt einen Fehler aus.

Es gibt zwei Strategien die man anwenden kann:

  • Daten zurück an Parent übertragen (siehe unten)
  • Daten nutzen um eine Komponentenvariable zu initialisieren und diese verändern.

Props Object[Bearbeiten]

Anstatt einem Array kann man auch Objekte übertragen. Darin kann man einige Zusatzinformationen unterbringen. Das ist vor allem bei großen Projekten vlt. mit mehreren Programmierern sinnvoll um Komponenten zuverlässiger zu machen. Die zusätzlichen Properties sind festgelegt: type, required

props:['name', 'phoneNumber', 'emailAddress'], // Array
props:{ name: String, phoneNumber: String, emailAddress: String, }, // Object mit Type 
props:{
  name: { type: String, required: false},
  isFavourite: { 
    type: String, 
    required: false, 
    default: '0', // could also be a function: function(){ return: '1'},
    validator: function(value){ return value === '0' || value === '1' }
  }
}

Mit den zusätzlichen Infos kann vue in der Konsole Fehler ausgeben oder du kannst selber Auswertungen der Daten in der Komponente vornehmen. Erlaubte Types sind:

String, Number, Boolean, Array, Object, Date, Function, Symbol

But type can also be any constructor function (built-in ones like Date or custom ones).

Props mit dynamischen Daten nutzen[Bearbeiten]

Um Props dynamisch zu nutzen kann man sie einfach mit v-bind (oder : ) an Daten binden und mit v-for durchlaufen. Plicht ist jetzt das key Attribut in die man eine eindeutige id geben muss.

App.vue:

//...
friends:[
{
  id: 'yvonne',
  name: 'Yvonne Oßwald',
  phone: '49 123 456',
  email: 'post@yvonneosswals.de',
  isFavourite: false
},
//...

in <template>

<ul>
  <friend-contact 
    v-for="friend in friends"
    :key="friend.id"
    :name="friend.name" 
    :phone-number="friend.phone" 
    :email-address="friend.email">
  </friend-contact>
</ul>

Die Komponente

<li>
        <h2>{{name}} {{friendIsFavourite ? '(*)' : ''}}</h2>
        <button @click="toggleFavourite">Toggle Favourite</button>
        <button @click="toggleDetails"> {{detailsVisible ? 'Hide' : 'Show'}} Details </button>
        <ul v-if="detailsVisible">
            <li><strong>Phone: </strong>{{phoneNumber}}</li>
            <li><strong>Email: </strong>{{emailAddress}}</li>
        </ul>
    </li>
 props:{
        name: String,
        phoneNumber: String,
        emailAddress: String,
        isFavourite: {type: Boolean, required: false, default: false}
    },

Fallthrough props - $attr Objekt[Bearbeiten]

Angenommen Du hast eine BaseButton.vue Komponente ohne definierte Props. Im Parent kannst Du trotzdem Attribute und Events nutzen...

<base-button type="submit" @click="doSomething">Click me</base-button>

... und diese dann in der Komponente auswerten. Dafür hat vue ein spezielles $attrs Objekt auf das Du mit

this.$attrs

zugreifen kannst. Dies kann z.b. bei Komponenten mit nur geringer Funktionalität nützlich sein, die eher zur reinen Ausgabe dienen.

Fallthrough props - root Element[Bearbeiten]

Fallthrough Properties ermöglichen es Properties und Events in Komponenten zu nutzen.

Wenn man Properties oder Events in einer Komponente nutzt werden Sie von Vue automatisch per default auf das Root Element angewendet.

Einsatz der BaseButton Komponente

<template>
<base-button @click="setSelectedTab('add-resource')">Add Resource</base-button>
</template>

Der click Listener wird hier automatisch auf das Root Element der Button Komponente angewendet - nämlich den button tag.

Definition der BaseButton Komponente:

<template>
    <button :type="type" :class="mode">
        <slot>Label goes here</slot>
    </button>
</template>

Alle props in einem Objekt binden[Bearbeiten]

Angenommen deine Komponente sieht etwas so aus:

UserData.vue

<template>
  <h2>{‌{ firstname }} {‌{ lastname }}</h2>
</template>
 
<script>
  export default {props: ['firstname', 'lastname'] }
</script>

Anstatt alle Props einzeln zu übertragen kannst du auch ein Übergeordnetes Objekt definieren und dieses übertragen.

App.vue

data() {
  return {person: { firstname: 'Max', lastname: 'Muster' } };
}
<!-- Einzelne Attribute binden -->
<user-data :firstname="person.firstname" :lastname="person.lastname"></user-data>
<!-- Alternativ: übergeordnetes Attribut binden (Achtung person ist value des Attributes -->
<user-data v-bind="person"></user-data>

Child - Parent Communication mit custom Events ($emit)[Bearbeiten]

Mit custom Events sendest du Daten von einer Komponente zurück an die Parent app.

Parent App -- [v-bind data] --> Component -- [custom Event data] --> 
  • Custom Events definierst du mit $emit in der Komponente
  • In der Parent App nutzt du das Event im HTML Teil mit v-on

Wenn in einer Komponente Daten verändert werden muss man sie zurück zur Elternkomponente übertragen. Das Ändern der Props ist wegen dem unidirectional dataflow nicht erlaubt.

$emit()

Um Daten zurück zu senden können wir custom Events nutzen. Dazu gibt es die $emit Funktion

this.$emit('event-name', argument1, argument2, ...) // always use kebab-case
  1. in der Component in einer method this.$emit aufrufen z.b. this.$emit('toggle-state', this.id)
    1. id ist meist sinnvoll damit in der Parent App der Datensatz zugeordnet werden kann.
  2. im HTML der Eltern App einen v-on mit diesem Event anlegen z.B. @toggle-state="toggleState"
  3. in der gebundenen Methode in der Eltern App verarbeiten z.b. toggleState(id){ //ie. store in db }

Beispiel - es soll ein isFavourite Datum aus der Kindkomponente (Friendcontact.vue) gesetzt werden. Mit Klick auf den Button wird ein CustomEvent gesendet. Dieses wird in der Parent App ausgewertet und in den Daten gesezt. Über das v-bind in der Parent App wird es zurück in die h2 der Kind Komponente gesendet und diese aktualisiert.

FriendContact.vue

<template>
    <li>
        <h2>{{name}} {{isFavourite ? '(*)' : ''}}</h2>
        <button @click="toggleFavourite">Toggle Favourite</button>
        <!-- ... -->
    </li>
</template>

<script>
export default {
    props:{
        id: {type: String, required: true},
        //...
        isFavourite: {type: Boolean, required: false, default: false}
    },
    emits:['toggle-favourite'], // emits object is optional and just for documentation purpose
    methods:{
        toggleFavourite(){
            this.$emit('toggle-favourite', this.id); 
        }
    }
    
}
</script>

App.vue

<template>
<section>
    <header><h1>My Friends</h1></header>
    <ul>
         <friend-contact 
         v-for="friend in friends"
         :key="friend.id"
         :id="friend.id"
         :name="friend.name" 
         //...
         :is-favourite="friend.isFavourite"
         @toggle-favourite="toggleFavouriteState">
         </friend-contact>
    </ul>
</section>
</template>

<script>
import FriendContact from './components/FriendContact.vue'
export default {
  components: { 
      FriendContact
    }, 
    data(){
        return {
            friends:[
                {
                    id: 'yvonne',
                    name: 'Yvonne Oßwald',
                    phone: '49 123 456',
                    email: 'post@yvonneosswals.de' 
                },
                {
                    id: 'finn',
                    name: 'Finn Schlegel',
                    phone: '49 456 789',
                    email: 'post@finnschlegel.de' ,
                    isFavourite: true
                },
            ]
        }
    },
    methods:{
        toggleFavouriteState(friendId){
            const identifiedFriend = this.friends.find(
                (friend) => friend.id === friendId // eq: function(friend){return friend.id...}
                // this callback is executed on every friend (array item)
                // first item match is returned
            )
            // identifiedFriend is a proxy which handles the origin array element of friends array
            // thats why we can modify identifiedFriend and friends will be altered too
            // console.log('identifiedFriend: ' + identifiedFriend.id)
            identifiedFriend.isFavourite = !identifiedFriend.isFavourite
        }
    }
}
</script>

provide und inject[Bearbeiten]

Manchmal hat man eine Art "Passthrough" Komponenten.

Dies bedeutet, dass diese Komponenten keine eigene Logik für eingehende props oder ausgehende emits zur Verfügung stellen sondern diese nur an die nächste Ebene (props an die Kind-Komponente und emit an die Elternelemente weitergeben.

Beispiel:

App.vue

<template>
<!-- ... -->
<knowledge-base :topics="topics" @select-topic="activateTopic"></knowledge-base>
<!-- ... -->
</template>
<script>
export default {
  data() {
    return {
      topics: [
        {
          id: 'basics',
          title: 'The Basics',
          description: 'Core Vue basics you have to know',
          fullText:
            'Vue is a great framework and it has a couple of key concepts: Data binding, events, components and reactivity - that should tell you something!',
        },
        {
          id: 'components',
          title: 'Components',
          description:
            'Components are a core concept for building Vue UIs and apps',
          fullText:
            'With components, you can split logic (and markup) into separate building blocks and then combine those building blocks (and re-use them) to build powerful user interfaces.',
        },
      ],
      activeTopic: null,
    };
  },
  methods: {
    activateTopic(topicId) {
      this.activeTopic = this.topics.find((topic) => topic.id === topicId);
    },
  },
};
</script>

KnowledgeBase.vue Hier werden die Props und das event quasi nur durchgeleitet.

<template>
<knowledge-grid :topics="topics" @select-topic="$emit('select-topic', $event)"></knowledge-grid>
</template>
<script>
export default { props: ['topics'], emits: ['select-topic'],};
</script>

KnowledgeGrid.vue Hier werden die Props zwar in der for Schleife genutzt, das select-topic event wird durchgeleitet.

<template>
    <knowledge-element
      v-for="topic in topics"
      :key="topic.id"
      :id="topic.id"
      :topic-name="topic.title"
      :description="topic.description"
      @select-topic="$emit('select-topic', $event)"
    ></knowledge-element>
</template
<script>
export default { props: ['topics'], emits: ['select-topic']};
</script>

KnowledgeElement.vue Hier wird schließlich das Event select-topic erzeugt.

<button @click="$emit('select-topic', id)">Learn More</button>

provide und inject eine Eigenschaft[Bearbeiten]

Mit dem Provide Objekt kannst du Properties bereitstellen und mit inject in einer anderen Komponente nutzbar machen. Hinweis: Du kannst nur in Richtung der Kindelemente injecten. Im Beispiel oben möchten wir die topics direkt zu KnowledgeGrid.vue senden. Das geht folgendermaßen

  • provide Object in App.vue hinzufügen und Daten hinzufügen
  • statt dem props Object die Daten mit inject nutzen

In App.vue ergänzen wir die configuration objects um das provide object (z.b. unter dem data) object

//..
provide() {
    return {topics:this.topics}
},

Wir stellen also eine Eigenschaft "topics" zum injecten (nutzen) bereit. Der Name ist frei wählbar. Hier kopieren wir einfach das schon vorhandene topics Ojekt aus data. Man kann aber auch eigene Objekte definieren.

Um das jetzt in KnowledgeGrid zu nutzen müssen wir dort das inject Objekt nutzen. Es ersetzt in diesem Fall vollständig die props. Daher schreiben wir statt props einfach inject:

inject: ['topics'],

provide und inject für custom events[Bearbeiten]

provide und inject funktionieren auch mit Methoden. So können einfach die Methode (oder besser den Zeiger auf eine Methode) injekten. App.vue - provide object

selectTopic: this.activateTopic // point at the method

In KnowledgeElement injekten wir: KnowledgeElement.vue

inject: ['selectTopic'],

und schreiben den button etwas um:

 <button @click="selectTopic(id)">Learn More</button>

Komplettes Beispiel[Bearbeiten]

App.vue

<template>
  <div>
    <active-element
      :topic-title="activeTopic && activeTopic.title"
      :text="activeTopic && activeTopic.fullText"
    ></active-element>
    <knowledge-base></knowledge-base>
  </div>
</template>

<script>
export default {
  data() {
    return {
      topics: [
        {
          id: 'basics',
          title: 'The Basics',
          description: 'Core Vue basics you have to know',
          fullText:
            'Vue is a great framework and it has a couple of key concepts: Data binding, events, components and reactivity - that should tell you something!',
        },
        {
          id: 'components',
          title: 'Components',
          description:
            'Components are a core concept for building Vue UIs and apps',
          fullText:
            'With components, you can split logic (and markup) into separate building blocks and then combine those building blocks (and re-use them) to build powerful user interfaces.',
        },
      ],
      activeTopic: null,
    };
  },
  provide() {
    return {
      topics: this.topics,
      selectTopic: this.activateTopic // point at the method
    }
  },
  methods: {
    activateTopic(topicId) {
      this.activeTopic = this.topics.find((topic) => topic.id === topicId);
    },
  },
  // use a hook to simulate data changes
  // this proves, that data in provide is updated too
  mounted(){
    setTimeout(
      () => {
        this.topics.push({
          id: 'events',
          title: 'Events',
          description:
            'Events are important in vue',
          fullText:
            'Events allow you to trigger code on demand.',
        })
      },
      3000
    )
  }
};
</script>

<style>
/* styling goes here */
</style>

KnowledgeBase.vue

<template>
  <section>
    <h2>Select a Topic</h2>
    <knowledge-grid></knowledge-grid>
  </section>
</template>

<script>
export default {
};
</script>

KnowledgeGrid.vue

<template>
  <ul>
    <knowledge-element
      v-for="topic in topics"
      :key="topic.id"
      :id="topic.id"
      :topic-name="topic.title"
      :description="topic.description"
    ></knowledge-element>
  </ul>
</template>

<script>
export default {
  inject: ['topics'],
};
</script>

KnowledgeElement.vue

<template>
  <li>
    <h3>{{ topicName }}</h3>
    <p>{{ description }}</p>
    <button @click="selectTopic(id)">Learn More</button>
  </li>
</template>

<script>
export default {
  inject: ['selectTopic'], // we expect a function named selectTopic
  props: ['id', 'topicName', 'description'],
  emits: ['select-topic'],
};
</script>

Global / Local[Bearbeiten]

Bisher haben wir Komponenten in main.js global registriert. Das ist sinnvoll bei "General Purpose Components" oder wenn Sie in vielen anderen Komponenten eingesetzt werden sollen. Global

  • Überall verfügbar, auch wenn nicht genutzt
  • Muss beim ersten Aufruf der Seite vom Browser geladen werden (Performance)

main.js

import BaseBadge from './components/BaseBadge.vue';
const app = createApp(App);
app.component('base-badge', BaseBadge);
app.mount('#app');

Oft ist es besser Komponenten nur dort zu registrieren wo sie benötigt werden. App.vue - <script>

import TheHeader from './components/TheHeader.vue';
import BadgeList from './components/BadgeList.vue';
import UserInfo from './components/UserInfo.vue';
export default {
  components: {
    TheHeader,
    // or 'the-header': TheHeader,
    // or TheHeader : TheHeader
    BadgeList,
    UserInfo,
  },

Diese Komponenten sind nur in App.vue verfügbar. Du kannst Sie natürlich über import auch in weiteren Komponenten nutzen.

scoped styling[Bearbeiten]

Mit dem Attribut scoped werden Styles nur für die Komponente angewendet.

<style scoped>

Best practice: Globale Styles in App.vue

Slots[Bearbeiten]

Slots sind eine Art Wrapper für eine Kind-Komponente.

Komponenten können in einem Template nicht ineinander Verschachtelt werden. Manchmal möchte man aber genau das haben um z.B. ein gestyltes Element (z.b. eine Card) wiederverwenden zu können. Der Inhalt soll aber über eine andere Komponente gerendert werden. So kann man z.B. Styles gut wiederverewnden.

  • Komponente registrieren (oft in main.js da man sie global haben möchte)
  • <slot></slot> Tag in die Slot Komponente einfügen
  • in der Kindkomponente einfach den custom Tag der Komponente als Wrapper nutzen

Für solche Zwecke gibt es Slots. BaseCard.vue

<template>
    <div>
        <slot></slot>
    </div>
</template>
<style scoped>
div {
  margin: 2rem auto;
  max-width: 30rem;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
  padding: 1rem;
}
</style>

ChildComponent.vue

<template>
    <base-card>
      <header>
        <h3>{{ fullName }}</h3>
        <base-badge :type="role" :caption="role.toUpperCase()"></base-badge>
      </header>
      <p>{{ infoText }}</p>
  </base-card>
</template>

<script>
export default {
  props: ['fullName', 'infoText', 'role'],
};
</script>

<style scoped>
div header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
</style>

So wie die props zum Transport von Daten dienen, so dienen die Slots zum Transport von dynamischem HTML Code zwischen den Komponenten.

Named Slots - v-slot[Bearbeiten]

Mit Named Slots kann man mehrere Slots nutzen, und damit auch komplexere Templates erzeugen. Dazu gibt es die Direktive v-slot. Wir nutzen zusätzlich das Standard HTML Tag <template> Das bietet sich an, da es keinen Inhat - also nicht mal eine Leerzeile - ausgibt.

v-slot:[slotname]

statt v-slot: kann man auch den Shortcut # nehmen:

#[slotname]

Beispiel BaseCard.vue

<template>
    <div>
        <header>
            <slot name="headline"></slot>
        </header>
        <slot></slot>
    </div>
</template>
...

Kindkomponente

<template>
    <base-card>
      <template v-slot:headline>
        <h3>{{ fullName }}</h3>
        <base-badge :type="role" :caption="role.toUpperCase()"></base-badge>
      </template>
      <template v-slot:default>
        <p>{{ infoText }}</p>
      </template>
  </base-card>
</template>

Default Content für Slots[Bearbeiten]

Du kannst default Inhalt anzeigen wenn die Kindkomponente keinen Inhalt für einen Slot hat.

 <slot name="headline">
 <h2>This default fallback content be shown if no content is provided</h2>
 </slot>

v-if mit $slots[Bearbeiten]

Manche Slots möchte man nur anzeigen wenn auch Inhalt dafür vorhanden ist. Ansonsten soll auch das umgebende Tag nicht ausgegeben werden. Dies kann man mit v-if und der $slots Variablen erreichen.

$slots enthält die Daten für einen aktuellen Slot. Das Nutzen wir über ein v-if:

<header v-if="slots.headline">
  <slot name="headline">
  </slot>
</header>

So wird die headline nur ausgegeben, wenn sie auch vorhanden ist.

Tipp: Den Inhalt von $slots kannst du dir über einen Hook in der Konsole ausgeben lassen:

   mounted(){
       console.log(this.$slots.headline)
   }

scoped Slots[Bearbeiten]

Scoped Slots erlauben es Daten vom Slot der Komponente zur Parent App zu übertragen.

Du kannst einfach im Slot Tag Attribute definieren, die dann in einem Objekt in der Eltern Komponente genutzt werden können:

Komponente

<template>
    <ul>
        <li v-for="goal in goals" :key="goal">
            <slot :item="goal" another-prop="hello"></slot>
        </li>
    </ul>
</template>

<script>
export default {
    data() {
        return {
            goals: ['Finish the coarse', 'Learn Vue'],
        }
    }
}
</script>

Und sie in der Elternkomponente auswerten.

Main App

    <course-goals>
      <template #default="myProps">
        <h4>{{ myProps.item }}</h4>
        <p>{{ myProps.anotherProp }}</p>
      </template>
    </course-goals>

Im Template Tag kann man den Slot (hier default auslesen und in eine beliebige Variable (hier myProps) schreiben. Hinweis: in aktuellen Vue Versionen kann man camelCase Schreibweise nutzen und Vue konvertiert automatisch.

Dynamic Components <component>[Bearbeiten]

Mit dem component Tag und dem is Attribut kannst du Komponenten dynamisch anzeigen. Das heißt an der Stelle von <component> können unterschiedliche Komponenten angezeigt werden.

<component v-bind:is="selectedComponent"></component>

Hier wird die Komponente mit dem Namen der in der Variablen selectedComponent steckt angezeigt.

<keep-alive> component[Bearbeiten]

    <keep-alive>
      <component v-bind:is="selectedComponent"></component>
    </keep-alive>

KeepAlive ist eine Vue-Interne Komponente, die oft mit dynamischen Komponenten in Verbindung eingesetzt wird. Da Komponenten wenn sie nicht mehr benötigt werden aus dem DOM entfernt werden, sind auch User-Inhalte wie die Eingabe in einem Input Feld gelöscht, wenn sie später wieder angezeigt werden.

Wrapt man die Komponente mit keep-alive werden die Inhalte vor dem Löschen gecacht und können später wieder hergestellt werden.

<teleport>[Bearbeiten]

Teleport ist eine eingebaute Vue Komponente. Mit dieser kannst Du DOM Elemente in andere Bereiche "teleportieren"

Dies kann z.b. für Modal Fenster sinnvoll sein, die man aus semantischen Gründen lieber auf der App oder <body> Ebene haben möchte anstatt tief vergraben im DOM

<teleport to="#selector"></teleport>
 

Teleport benötigt das Attribut to, in dem man einen CSS-Selector angibt, wo das Element eingehängt werden soll.

<teleport to="#app">
  <error-alert v-if="inputIsInvalid">
     <h2>Input is invalid</h2>
     <p>Please enter something.</p>
     <button @click="confirmModal">Okay</button>
  </error-alert>
</teleport>


Custom Form Control[Bearbeiten]

Wenn man v-model in einer eigenen Component nutzt

<my-component v-model="myProp"></my-component>

macht Vue im Prinzip eine Kombi aus event listener und binding mit diesen Objekten:

<my-component :model-value="" @update:modelValue=""></my-component>

Die beiden Snipperts verhalten sich also identisch.

Sobald v-model eingesetzt wird stehen also auch das Emit und das Prop in der Komponente zur Verfügung:

props:['model-value'],
emits: ['update:modelValue'],

Diese können wir nutzen wie hier im Beispiel. modelValue ist immer aktuell, da eine Aktualisierung über $emit an die Parent Komponente geht und von dieser wieder über das prop model-value zurückkommt und so auch in <template> bei modelValue aktualisiert wird.

Beispiel RatingControl.vue

<template>
  <ul>
    <li><button :class="{active: modelValue === 'poor'}" @click="activate('poor')" type="button">Poor</button></li>
    <li><button :class="{active: modelValue === 'average'}" @click="activate('average')" type="button">Average</button></li>
    <li><button :class="{active: modelValue === 'great'}" @click="activate('great')" type="button">Great</button></li>
  </ul>
</template>

<script>
export default {
    props:['model-value'],
    emits: ['update:modelValue'],
    methods:{
        activate(option){
            this.$emit('update:modelValue', option);
        }
    }
}
</script>

Parent

<template>
<rating-control v-model="rating"></rating-control>
</template>
 
<script>
import RatingControl from './RatingControl.vue'
export default {
  components:{
    RatingControl
  },
  data(){
    return {
      rating: null
    }
  },
  methods: {
    submitForm(){
      console.log('Rating: ' + this.rating);
      this.rating = null;
   },
  }
}
</script>