HTML5-Tutorium: JavaScript: Hello World Vue 03: Unterschied zwischen den Versionen

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg
Kowa (Diskussion | Beiträge)
Kowa (Diskussion | Beiträge)
 
(59 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 1: Zeile 1:
{{HTML5-Tutorium:JavaScript:HelloWorld:Menü}}
{{HTML5-Tutorium:JavaScript:HelloWorld:Menü}}


'''Musterlösung'''<br/>
v03:&nbsp;[https://glossar.hs-augsburg.de/beispiel/tutorium/2025/wk_hello_world/wk_hello_world_vue/web_v03/index.html <code>index.html</code>] ([https://validator.nu/?doc=https%3A%2F%2Fglossar.hs-augsburg.de%2Fbeispiel%2Ftutorium%2F2025%2Fwk_hello_world%2Fwk_hello_world_vue%2Fweb_v03%2Findex.html HTML validate], [https://css-validator.org/validator?uri=https%3A%2F%2Fglossar.hs-augsburg.de%2Fbeispiel%2Ftutorium%2F2025%2Fwk_hello_world%2Fwk_hello_world_vue%2Fweb_v03%2Findex.html&profile=css21&usermedium=all&warning=1&lang=de CSS validate], [https://pagespeed.web.dev/analysis?url=https%3A%2F%2Fglossar.hs-augsburg.de%2Fbeispiel%2Ftutorium%2F2025%2Fwk_hello_world%2Fwk_hello_world_vue%2Fweb_v03%2Findex.html Google PageSpeed])<br/><br/>
[{{Git-Server}}/kowa/wk_hello_world_vue Git-Repository], <code>git checkout v03</code> (JavaScript)<br>
[{{Git-Server}}/kowa/wk_hello_world_vue Git-Repository], <code>git checkout v03</code> (JavaScript)
[{{Git-Server}}/kowa/wk_hello_world_vue_ts Git-Repository (TypeScript)], <code>git checkout v03</code>
 
Stores mit Hilfe von Klassen: <br/>
[{{Git-Server}}/kowa/wk_hello_world_vue Git-Repository], <code>git checkout v03b</code> (JavaScript)<br>
[{{Git-Server}}/kowa/wk_hello_world_vue_ts Git-Repository (TypeScript)], <code>git checkout v03b</code>


== Anwendungsfälle (Use Cases) ==
== Anwendungsfälle (Use Cases) ==
Zeile 30: Zeile 25:
== Pinia ==
== Pinia ==


Zitat:
Zitat<ref>https://vuejs.org/guide/scaling-up/state-management</ref>:


''While our hand-rolled state management solution will suffice in simple scenarios, there are many more things to consider in large-scale production applications:
''While our hand-rolled state management solution will suffice in simple scenarios, there are many more things to consider in large-scale production applications:
Zeile 51: Zeile 46:
Erstellen Sie einen neuen Projektzweig (branch) innerhalb von <code>hello_world_vue</code>:
Erstellen Sie einen neuen Projektzweig (branch) innerhalb von <code>hello_world_vue</code>:


<source lang="bash">
<syntaxhighlight lang="bash">
git checkout v02    # Wechsle in den Branch v02
git checkout v02    # Wechsle in den Branch v02
git checkout -b v03  # Klone v02 in einen neuen Branch v03
git checkout -b v03  # Klone v02 in einen neuen Branch v03
npm i
npm i
</source>
</syntaxhighlight>


== Initialisierung von Pinia ==
== Initialisierung von Pinia ==
Zeile 61: Zeile 56:
Pinia muss als reguläre Dependency installiert werden, da das Paket in den produktiven Code integriert wird.
Pinia muss als reguläre Dependency installiert werden, da das Paket in den produktiven Code integriert wird.


<source lang="bash">
<syntaxhighlight lang="bash">
npm i pinia  // NICHT  npm i -D pinia
npm i pinia  // NICHT  npm i -D pinia, da Pinia-Code an den Browser ausgeliefert werden muss
</source>
</syntaxhighlight>


Pinia wird als Plugin in die App integriert. Dabei ist wichtig, dass das Pina-Plugin-Objekt vor dem eigentlichen App-Objekt erzeugt wird, da Komponenten der  
Pinia wird als Plugin in die App integriert. Dabei ist wichtig, dass das Pina-Plugin-Objekt vor dem eigentlichen App-Objekt erzeugt wird, da Komponenten der  
App, die Pinia verwenden, darauf angewiesen sind, dass die Pinia-Erzeugung vor der Komponenten-Initialisierung erfolgt.
App, die Pinia verwenden, darauf angewiesen sind, dass die Pinia-Erzeugung vor der Komponenten-Initialisierung erfolgt.


<source lang="bash">
<syntaxhighlight lang="bash">
// src/main.js
// src/main.js


Zeile 75: Zeile 70:
import App            from './App.vue'
import App            from './App.vue'


const pinia = createPinia(); // before createApp(...)
const pinia = createPinia() // before createApp(...)


window.addEventListener
window.addEventListener
( 'load'
( 'load',
   createApp(App).use(pinia).mount('#app')
   createApp(App).use(pinia).mount('#app')
)
)
</source>
</syntaxhighlight>


== Erstellen der Stores ==
== Erstellen der Stores ==


=== <code>StoreSection</code> ===
Fügen Sie zunächst zwei weitere Aliase in die Datei <code>vite.config.common.js</code> ein:
 
<syntaxhighlight lang="javascript">
'/controller': fileURLToPath(new URL('./src/controller', import.meta.url)),
'/service':    fileURLToPath(new URL('./src/service',    import.meta.url)),
'/component':  fileURLToPath(new URL('./src/component',  import.meta.url)),
'/store':      fileURLToPath(new URL('./src/store',      import.meta.url)),
</syntaxhighlight>
 
Damit kann man Controller, Services. Komponenten und Stores immer mit absoluter Adressierung laden ({{zB}} in <code>App.vue</code>), ohne <code>@</code> verwenden zu müssen.
 
=== <code>StoreSession</code> ===


Im Section-Store wird gespeichert, welche Section der Single-Page-Anwendung dem Benutzer aktuell präsentiert wird.
Im Session-Store wird die Autofocus-ID gespeichert sowie welche Section der Single-Page-Anwendung dem Benutzer aktuell präsentiert wird.


<source lang="ecmascript">
<syntaxhighlight lang="ecmascript">
// src/store/StoreSection.js
// src/store/StoreSession.js


import { defineStore } from 'pinia'
import { defineStore   } from 'pinia'
import { ref }         from 'vue'
import { ref, reactive } from 'vue'


const storeSection =
const storeSession =
defineStore
defineStore
( 'section', // must be unique
( 'session', // must be unique


   () =>
   () =>
   { const
   { const
       section = ref(''),
       section   = ref(''),
 
      autofocus = reactive({ setFocus: null }),
 
       init =
       init =
         p_section =>
         p_section => { section.value = p_section },
        section.value = p_section,


       visability =
       initAutofocus =
         p_section =>
         (p_autofocus_id = null) =>
         section.value !== p_section ? 'hidden' : '',
         { if (p_autofocus_id != null)
          { autofocus.setFocus = () => document.getElementById(p_autofocus_id).focus(),
            window.addEventListener('visibilitychange', autofocus.setFocus);
          }
        },


       change =
       setFocus =
         p_section =>
         () => autofocus?.setFocus()
        section.value = p_section


     return { init, visability, change }
     return { init, initAutofocus, section, setFocus }
   }
   }
)
)


export default storeSection
export default storeSession
</source>
</syntaxhighlight>


=== <code>StoreGreeting</code> ===
=== <code>StoreGreeting</code> ===
Zeile 126: Zeile 135:
Im Greeting-Store werden die Daten zur Begrüßung des Benutzers gespeichert.
Im Greeting-Store werden die Daten zur Begrüßung des Benutzers gespeichert.


<source lang="ecmascript">
<syntaxhighlight lang="ecmascript">
// src/store/StoreGreeting.js
// src/store/StoreGreeting.js


Zeile 139: Zeile 148:
     () =>
     () =>
     { const
     { const
      state =
        state =
        reactive({ name: '', stranger: 'Stranger', hello: 'Hello, $1!' }),
          reactive({ name: '', stranger: 'Stranger', hello: 'Hello, $1!' }),


      init =
        init =
        (p_state = {}) =>
          (p_state = {}) =>
        { Object.assign(state, p_state) },
          { Object.assign(state, p_state) },
 
        name =
          toRef(state, 'name'),


      name = toRef(state, 'name'),
        reset =
          () => { name.value = '' },


      sayHello =
        sayHello =
        p_name string): string =>
          p_name => state.hello.replace('$1', p_name),
        state.hello.replace('$1', p_name),


      helloStranger =
        helloStranger =
        computed(() => sayHello(state.stranger)),
          computed(() => sayHello(state.stranger)),


      hello =
        hello =
        computed(() => sayHello(state.name))
          computed(() => sayHello(state.name))


       return { init, name, helloStranger, hello }
       return { init, name, helloStranger, hello, reset }
     }
     }
)
  )


export default storeGreeting
export default storeGreeting
</source>
</syntaxhighlight>


== Erstellen der Stores in einer Komponente==
== Einbinden der Stores in eine Komponente==
 
<source lang="ecmascript">
<!-- src/components/HelloWorld.vue -->


<syntaxhighlight lang="ecmascript">
<script setup>
<script setup>
   import { onMounted, onUnmounted } from 'vue'
   import { useId, onMounted, onUnmounted } from 'vue'
   import ControllerKey from '@/controller/ControllerKey'
   import storeSession  from '/store/StoreSession'
   import storeSection from '@/store/StoreSection'
   import storeGreeting from '/store/StoreGreeting'
   import storeGreeting from '@/store/StoreGreeting'
   import ControllerKeys from '/controller/ControllerKeys'


   const
   const
     section  = storeSection(),
     props          = defineProps ({ autofocus: { type: Boolean } }),
     greeting = storeGreeting(),
    inputId        = useId(),
 
    session        = storeSession(),
     greeting       = storeGreeting(),


     sayHello = () => { section.change('hello') },
     reset          = () => { greeting.reset(); session.setFocus() },
     controllerKey =
    sayHello        = () => session.section = 'hello',
      new ControllerKey('Enter', sayHello, 'input_name')
     sayHelloOnEnter = new ControllerKeys('Enter', inputId, sayHello)


   section.init('question')
   session.init('form', props.autofocus ? inputId : null)
//greeting.init({ hello: 'Hallo, $1!', stranger: 'Fremder'})
  //greeting.init({ hello: 'Hi, $1!', stranger: 'Nobody'})


   onMounted (() => controllerKey.add())
   onMounted   ( () => sayHelloOnEnter.add()   )
   onUnmounted(() => controllerKey.remove())
   onUnmounted ( () => sayHelloOnEnter.remove() )
</script>
</script>
</source>
</syntaxhighlight>
 
Zum Vergleich noch einmal das Skript ohne Store
<source lang="ecmascript">
<!-- src/components/HelloWorld.vue without store -->


Zum Vergleich noch einmal das Skript ohne Store:
<syntaxhighlight lang="ecmascript">
<!-- src/component/HelloWorld.vue without store -->
<script setup >
<script setup >
   import { ref, computed, onMounted, onUnmounted } from 'vue'
   import { useId, ref, computed, onMounted, onUnmounted } from 'vue'
   import ControllerKey from '@/controller/ControllerKey'
   import ControllerKeys from '@/controller/ControllerKeys'


   const
   const
     section      = ref('question'),
    props        = defineProps ({ autofocus: { type: Boolean } }),
    inputId      = useId(),
     section      = ref('form'),
     helloStranger = ref('Hello, Stranger'),
     helloStranger = ref('Hello, Stranger'),
     name          = ref(''),
     name          = ref(''),


     hello     = computed(() => `Hello, ${name.value}!`),
     hello     = computed(() => name.value === '' ? helloStranger.value : `Hello, ${name.value}!`),
 
     sayHello  = () => section.value = 'hello',
     visability = (p_section: string) =>
    autoFocus = () => { if (props.autofocus)
                  p_section !== section.value
                          document.getElementById(inputId).focus()
                  ? 'hidden'
                      },
                  : '',
     reset    = () => { document.getElementById(inputId).value = '';
 
                        autoFocus();
     sayHello  = () => { section.value = 'hello' },
                      },
     controllerKey =
     sayHelloOnEnter = new ControllerKeys('Enter', inputId, sayHello)
      new ControllerKey('Enter', sayHello, 'input_name')


   onMounted  (() => controllerKey.add())
   onMounted  (...)
   onUnmounted(() => controllerKey.remove())
   onUnmounted(...)
</script>
</script>
</source>
</syntaxhighlight>


== Anpassung des Templates ==
== Anpassung des Templates ==
Zeile 225: Zeile 238:
Das Template muss angepasst werden. Man muss nun auf die Store-Objekte zugreifen.
Das Template muss angepasst werden. Man muss nun auf die Store-Objekte zugreifen.


<source lang="ecmascript">
Achtung, die Funktion <code>reset</code> aktivier nicht nur das Autofokus-Element,
sondern löscht auch das NAmensfeld im <code>StoreGreeting</code>. Würde man in der
Funktion <code>reset</code> auf den Aufruf von <code>storeGreeting.reset()</code> verzichten,
würde der Input-Type <code>reset</code> im unten angegebenen Input-Buttons zwar
den Inhalt Textfelds im Formular löschen, der zuvor eingegebenen Name stünde aber weiterhin im Greeting-Store. Die bereitet spätestens dann Probleme, wenn man den im Greeting-Store gespeicherten Namen auch a anderer Stelle verwendet.
 
<syntaxhighlight lang="ecmascript">
<!-- src/components/HelloWorld.vue -->
<!-- src/components/HelloWorld.vue -->


<template>
<template>
   <section id="section_question" v-bind:class="section.visability('question')">
   <section v-if="session.section==='form'">
     <h1>{{greeting.helloStranger}}</h1>
     <h1>{{greeting.helloStranger}}</h1>
     <form>
     <form>
       <div>
       <div>
         <label for="input_name">What's your name?</label>
         <label :for="inputId">What´s your name?</label>
         <input id="input_name" type="text" autofocus
         <input :id="inputId" v-model="greeting.name" :autofocus>
              v-model="greeting.name"
        />
       </div>
       </div>
       <div>
       <div>
         <input id="button_reset"  type="reset"  value="Reset"/>
         <input type="reset"  value="Reset"     @click="reset">
         <input id="button_submit" type="button" value="Say hello"
         <input type="button" value="Say hello" @click="sayHello">
              v-on:click="sayHello"
        />
       </div>
       </div>
     </form>
     </form>
   </section>
   </section>
   <section id="section_hello" v-bind:class="section.visability('hello')">
   <section v-if="session.section==='hello'">
     <h1>{{greeting.hello}}</h1>
     <h1>{{greeting.hello}}</h1>
     <p>Welcome to Web Programming!</p>
     <p>Welcome to Full-Stack Web Development!</p>
   </section>
   </section>
</template>
</template>
</source>
</syntaxhighlight>
 
== Stores als Klassen ==
 
Stores können auch mit Hilfe von Klassen definiert werden. Dazu benötigt man zunächst eine Basisklasse, die von jedem Store wiederverwendet werden kann.
 
=== Basisklasse <code>Store</code> ===
 
<source lang="ecmascript">
// src/common/Store/Store.js
 
// cmp. https://medium.com/@mario.brendel1990/vue-3-the-new-store-a7569d4a546f
import { reactive } from 'vue'
 
class Store
{ #initState
  state
 
  reset()
  { if (this.state == null)
    { this.state = reactive({...this.initState}) }
    else
    { Object.assign(this.state, this.initState) }
  }
 
  init(p_state)
  { Object.assign(this.initState, p_state);
    this.reset();
  }
 
  constructor(p_state)
  { this.initState = p_state;
    this.reset();
  }
}
 
export default Store
</source>
 
=== Definition der Store-Klasse StoreSection ===
 
Nun kann der Section-Store als Klasse realisiert werden.
 
<source lang="ecmascript">
// src/store/StoreSection.js
 
import Store from '@/common/store/Store'
 
class StoreSection extends Store
{ constructor()
  { super({ name: '' }) }
 
  visability(p_section)
  { return this.state.name !== p_section ? 'hidden' : '' }
 
  change(p_section)
  { this.state.name = p_section }
}
 
const storeSection = new StoreSection();
 
export default storeSection;
</source>
 
=== Definition der Store-Klasse StoreGreeting ===
 
Den Greeting-Store kann man analog definieren.
 
<source lang="ecmascript">
// src/store/StoreGreeting.js
 
import Store        from '@/common/store/Store'
import { computed } from 'vue'
 
class StoreGreeting extends Store
{ constructor(p_init = { hello: 'Hello, $1!', stranger: 'Stranger' })
  { super({ name: '', ...p_init }); }
 
  get name()
  { return this.state.name; }
 
  set name(p_name)
  { this.state.name = p_name; }
 
  #sayHello(p_name)
  { return this.state.hello.replace('$1', p_name); }
 
  get helloStranger()
  { return computed(() => this.#sayHello(this.state.stranger)).value }
 
  get hello()
  { return computed(() => this.#sayHello(this.state.name !== ''
                                        ? this.state.name
                                        : this.state.stranger
                                        )
                  ).value
  }
}
 
const stateGreeting = new StoreGreeting()
 
export default stateGreeting
</source>
 
Störend ist hier, dass man für jedes Attribut wie {{zB}} <code>name</code> eine Getter- und eine Setter-Methode definieren muss.
Dieses Problem kann man mit einer weiteren Superklasse abmildern.
 
=== StoreExtendible ===
 
Man definiert die Klasse <code>StoreExtendible</code>, die es ermöglicht, bei der Initialisierung des Stores dynamisch reaktive Getter- und Setter-Methoden hinzuzufügen.
 
<source lang="ecmascript">
// src/common/Store/StoreExtendible.js
 
import Store from './Store'
 
class StoreExtendible extends Store
{ #add(p_properties, p_getter, p_setter)
  { if (typeof p_properties === 'string')
    { Object.defineProperty
      ( this.constructor.prototype,
        p_properties,
        { ...( p_getter ? { get: () => this.state[p_properties] } : {} ),
          ...( p_setter ? { set: (value) =>
                                { if (this.state[p_properties] !== value)
                                  { this.state[p_properties] = value; }
                                },
                        }
                      : {}
            ),
          configurable: false, // the property cannot be redefined
          enumerable:  true
        },
      );
    }
    else
    { p_properties.forEach(e => this.addGetter(e)) }
  }
 
  addGetter(p_properties)
  { this.#add(p_properties, true, false) }
 
  addSetter(p_properties)
  { this.#add(p_properties, false, true) }
 
  addGetterSetter(p_properties)
  { this.#add(p_properties, true, true) }
}
 
export default StoreExtendible
</source>
 
=== Verwendung der alternativen Stores in einer Komponente ===
 
<source lang="ecmascript">
<!-- src/components/HelloWorld.vue with store class -->
 
<script setup >
  import { onMounted, onUnmounted } from 'vue'
  import ControllerKey from '@/controller/ControllerKey'
  import section  from '@/store/StoreSectionClass'
//import greeting from '@/store/StoreGreetingClass'
  import greeting from '@/store/StoreGreetingClassExtendible'
 
  const
    sayHello = () => { section.change('hello') },
    controllerKey =
      new ControllerKey('Enter', sayHello, 'input_name')
 
  section.init({ name: 'question'})
//greeting.init({ hello: 'Hallo, $1!', stranger: 'Fremder'})
 
  onMounted  (() => controllerKey.add())
  onUnmounted(() => controllerKey.remove())
</script>
</source>
 
Zum Vergleich: Die Verwendung der zuerst definierten Stores.
<source lang="ecmascript">
<!-- src/components/HelloWorld.vue -->
 
<script setup>
  import { onMounted, onUnmounted } from 'vue'
  import ControllerKey from '@/controller/ControllerKey'
  import storeSection  from '@/store/StoreSection'
  import storeGreeting from '@/store/StoreGreeting'
 
 
  const
    section  = storeSection(),
    greeting = storeGreeting(),
 
    sayHello = () => { section.change('hello') },
    controllerKey =
      new ControllerKey('Enter', sayHello, 'input_name')
 
  section.init('question')
//greeting.init({ hello: 'Hallo, $1!', stranger: 'Fremder'})
 
  onMounted  (() => controllerKey.add())
  onUnmounted(() => controllerKey.remove())
</script>
</source>


==Fortsetzung des Tutoriums==
==Fortsetzung des Tutoriums==
Zeile 462: Zeile 274:


==Quellen==
==Quellen==
<!--references/-->
<references/>
<ol>
<ol>
<li value="1"> {{Quelle|Kowarschick, W.: Web-Programmierung}}</li>
<li value="2"> {{Quelle|Kowarschick, W.: Web-Programmierung}}</li>
</ol>
</ol>
<noinclude>[[Kategorie: HTML5-Tutorium: JavaScript: Hello World]][[Kategorie: HTML5-Beispiel]][[Kategorie:Kapitel:Multimedia-Programmierung:Beispiele]]</noinclude>
<noinclude>[[Kategorie: HTML5-Tutorium: JavaScript: Hello World]][[Kategorie: HTML5-Beispiel]][[Kategorie:Kapitel:Multimedia-Programmierung:Beispiele]]</noinclude>

Aktuelle Version vom 12. Mai 2025, 12:57 Uhr

Dieser Artikel erfüllt die GlossarWiki-Qualitätsanforderungen nur teilweise:

Korrektheit: 3
(zu größeren Teilen überprüft)
Umfang: 4
(unwichtige Fakten fehlen)
Quellenangaben: 3
(wichtige Quellen vorhanden)
Quellenarten: 5
(ausgezeichnet)
Konformität: 3
(gut)

Vorlesung WebProg

Inhalt | Teil 1 | Teil 2 | Teil 3 | Teil 4 | Teil 5 | Teil 6 | Vue 1 | Vue 2 | Vue 3 | Vue 4 | Vue 5 | Vue 6

v03: index.html (HTML validate, CSS validate, Google PageSpeed)

Git-Repository, git checkout v03 (JavaScript)

Anwendungsfälle (Use Cases)

Gegenüber dem ersten und zweiten Teil des Vue-Tutoriums ändern sich die die Anwendungsfälle nicht. Die Anwendung leistet also genau dasselbe wie zuvor.

Aufgabe

Aufgabe: Speichere den Zustand der Anwendung (Model) in Store-Objekten, um die künftige Modularisierung (Version v04) zu unterstützen.

In diesem Tutorium sollen die Daten in einem Store außerhalb der Komponenten gespeichert werden.

Mögliche Storekonzepte

  • vuex (veraltet, wird nur noch gewartet, angeblich nicht mehr weiterentwickelt, aber Version 4 existiert)
  • Pinia (der Nachfolger von vuex; einfacher zu benutzen; Vue-3- und Vue-2-Paradigmen werden unterstützt)
  • ohne Bibliothek direkt mit ref, reactive und computed aus Vue 3.2 (vgl. Vue 3 — The New Store)

Wir verwenden Pinia.

Pinia

Zitat[1]:

While our hand-rolled state management solution will suffice in simple scenarios, there are many more things to consider in large-scale production applications:

  1. Stronger conventions for team collaboration
  2. Integrating with the Vue DevTools, including timeline, in-component inspection, and time-travel debugging
  3. Hot Module Replacement
  4. Server-Side Rendering support

Pinia is a state management library that implements all of the above. It is maintained by the Vue core team, and works with both Vue 2 and Vue 3.

Existing users may be familiar with Vuex, the previous official state management library for Vue. With Pinia serving the same role in the ecosystem, Vuex is now in maintenance mode. It still works, but will no longer receive new features. It is recommended to use Pinia for new applications.

Pinia started out as an exploration of what the next iteration of Vuex could look like, incorporating many ideas from core team discussions for Vuex 5. Eventually, we realized that Pinia already implements most of what we wanted in Vuex 5, and decided to make it the new recommendation instead.

Compared to Vuex, Pinia provides a simpler API with less ceremony, offers Composition-API-style APIs, and most importantly, has solid type inference support when used with TypeScript.

Erstellen eines neuen Projektzweigs

Erstellen Sie einen neuen Projektzweig (branch) innerhalb von hello_world_vue:

git checkout v02     # Wechsle in den Branch v02
git checkout -b v03  # Klone v02 in einen neuen Branch v03
npm i

Initialisierung von Pinia

Pinia muss als reguläre Dependency installiert werden, da das Paket in den produktiven Code integriert wird.

npm i pinia  // NICHT  npm i -D pinia, da Pinia-Code an den Browser ausgeliefert werden muss

Pinia wird als Plugin in die App integriert. Dabei ist wichtig, dass das Pina-Plugin-Objekt vor dem eigentlichen App-Objekt erzeugt wird, da Komponenten der App, die Pinia verwenden, darauf angewiesen sind, dass die Pinia-Erzeugung vor der Komponenten-Initialisierung erfolgt.

// src/main.js

import { createPinia } from 'pinia'
import { createApp }   from 'vue'
import App             from './App.vue'

const pinia = createPinia() // before createApp(...)

window.addEventListener
( 'load',
  createApp(App).use(pinia).mount('#app')
)

Erstellen der Stores

Fügen Sie zunächst zwei weitere Aliase in die Datei vite.config.common.js ein:

 '/controller': fileURLToPath(new URL('./src/controller', import.meta.url)),
 '/service':    fileURLToPath(new URL('./src/service',    import.meta.url)),
 '/component':  fileURLToPath(new URL('./src/component',  import.meta.url)),
 '/store':      fileURLToPath(new URL('./src/store',      import.meta.url)),

Damit kann man Controller, Services. Komponenten und Stores immer mit absoluter Adressierung laden (z. B. in App.vue), ohne @ verwenden zu müssen.

StoreSession

Im Session-Store wird die Autofocus-ID gespeichert sowie welche Section der Single-Page-Anwendung dem Benutzer aktuell präsentiert wird.

// src/store/StoreSession.js

import { defineStore   } from 'pinia'
import { ref, reactive } from 'vue'

const storeSession =
defineStore
( 'session', // must be unique

  () =>
  { const
      section   = ref(''),
      autofocus = reactive({ setFocus: null }),

      init =
        p_section => { section.value = p_section },

      initAutofocus =
        (p_autofocus_id = null) =>
        { if (p_autofocus_id != null)
          { autofocus.setFocus = () => document.getElementById(p_autofocus_id).focus(),
            window.addEventListener('visibilitychange', autofocus.setFocus);
          }
        },

      setFocus =
        () => autofocus?.setFocus()

    return { init, initAutofocus, section, setFocus }
  }
)

export default storeSession

StoreGreeting

Im Greeting-Store werden die Daten zur Begrüßung des Benutzers gespeichert.

// src/store/StoreGreeting.js

import { defineStore }               from 'pinia'
import { reactive, toRef, computed } from 'vue'

const
  storeGreeting =
  defineStore
  ( 'greeting',

    () =>
    { const
        state =
          reactive({ name: '', stranger: 'Stranger', hello: 'Hello, $1!' }),

        init =
          (p_state = {}) =>
          { Object.assign(state, p_state) },

        name = 
          toRef(state, 'name'),

        reset =
          () => { name.value = '' },

        sayHello =
          p_name => state.hello.replace('$1', p_name),

        helloStranger =
          computed(() => sayHello(state.stranger)),

        hello =
          computed(() => sayHello(state.name))

      return { init, name, helloStranger, hello, reset }
    }
  )

export default storeGreeting

Einbinden der Stores in eine Komponente

<script setup>
  import { useId, onMounted, onUnmounted } from 'vue'
  import storeSession   from '/store/StoreSession'
  import storeGreeting  from '/store/StoreGreeting'
  import ControllerKeys from '/controller/ControllerKeys'

  const
    props           = defineProps ({ autofocus: { type: Boolean } }),
    inputId         = useId(),

    session         = storeSession(),
    greeting        = storeGreeting(),

    reset           = () => { greeting.reset(); session.setFocus() },
    sayHello        = () => session.section = 'hello',
    sayHelloOnEnter = new ControllerKeys('Enter', inputId, sayHello)

  session.init('form', props.autofocus ? inputId : null)
  //greeting.init({ hello: 'Hi, $1!', stranger: 'Nobody'})

  onMounted   ( () => sayHelloOnEnter.add()    )
  onUnmounted ( () => sayHelloOnEnter.remove() )
</script>

Zum Vergleich noch einmal das Skript ohne Store:

<!-- src/component/HelloWorld.vue without store -->
<script setup >
  import { useId, ref, computed, onMounted, onUnmounted } from 'vue'
  import ControllerKeys from '@/controller/ControllerKeys'

  const
    props         = defineProps ({ autofocus: { type: Boolean } }),
    inputId       = useId(),
    section       = ref('form'),
    helloStranger = ref('Hello, Stranger'),
    name          = ref(''),

    hello     = computed(() => name.value === '' ? helloStranger.value : `Hello, ${name.value}!`),
    sayHello  = () => section.value = 'hello',
    autoFocus = () => { if (props.autofocus)
                          document.getElementById(inputId).focus()
                      },
    reset     = () => { document.getElementById(inputId).value = '';
                        autoFocus();
                      },
    sayHelloOnEnter = new ControllerKeys('Enter', inputId, sayHello)

  onMounted  (...)
  onUnmounted(...)
</script>

Anpassung des Templates

Das Template muss angepasst werden. Man muss nun auf die Store-Objekte zugreifen.

Achtung, die Funktion reset aktivier nicht nur das Autofokus-Element, sondern löscht auch das NAmensfeld im StoreGreeting. Würde man in der Funktion reset auf den Aufruf von storeGreeting.reset() verzichten, würde der Input-Type reset im unten angegebenen Input-Buttons zwar den Inhalt Textfelds im Formular löschen, der zuvor eingegebenen Name stünde aber weiterhin im Greeting-Store. Die bereitet spätestens dann Probleme, wenn man den im Greeting-Store gespeicherten Namen auch a anderer Stelle verwendet.

<!-- src/components/HelloWorld.vue -->

<template>
  <section v-if="session.section==='form'">
    <h1>{{greeting.helloStranger}}</h1>
    <form>
      <div>
        <label :for="inputId">What´s your name?</label>
        <input :id="inputId" v-model="greeting.name" :autofocus>
      </div>
      <div>
        <input type="reset"  value="Reset"     @click="reset">
        <input type="button" value="Say hello" @click="sayHello">
      </div>
    </form>
  </section>
  <section v-if="session.section==='hello'">
    <h1>{{greeting.hello}}</h1>
    <p>Welcome to Full-Stack Web Development!</p>
  </section>
</template>

Fortsetzung des Tutoriums

Sie sollten nun Teil 4 des Vue-Tutoriums bearbeiten. Nun wird die Anwendung modularisiert.

Quellen

  1. Kowarschick (WebProg): Wolfgang Kowarschick; Vorlesung „Web-Programmierung“; Hochschule: Hochschule Augsburg; Adresse: Augsburg; Web-Link; 2024; Quellengüte: 3 (Vorlesung)