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)
 
(19 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 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, da Pinia-Code an den Browser ausgeliefert werden muss
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 81: Zeile 76:
   createApp(App).use(pinia).mount('#app')
   createApp(App).use(pinia).mount('#app')
)
)
</source>
</syntaxhighlight>


== Erstellen der Stores ==
== Erstellen der Stores ==
Zeile 100: Zeile 95:
Im Session-Store wird die Autofocus-ID gespeichert sowie 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/StoreSession.js
// src/store/StoreSession.js


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


const storeSession =
const storeSession =
Zeile 112: Zeile 107:
   () =>
   () =>
   { const
   { const
       section     = ref(''),
       section   = ref(''),
       autofocusId = ref(null),
       autofocus = reactive({ setFocus: null }),


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


       setFocus =
       setFocus =
         () =>
         () => autofocus?.setFocus()
        { console.log(autofocusId.value)
          if (autofocusId.value != null)
            document.getElementById(autofocusId.value).focus()
        }


     return { init, setFocus, section, autofocusId: readonly(autofocusId) }
     return { init, initAutofocus, section, setFocus }
   }
   }
)
)


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


=== <code>StoreGreeting</code> ===
=== <code>StoreGreeting</code> ===
Zeile 139: 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 161: Zeile 157:
         name =  
         name =  
           toRef(state, 'name'),
           toRef(state, 'name'),
        reset =
          () => { name.value = '' },


         sayHello =
         sayHello =
Zeile 171: Zeile 170:
           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>


== Einbinden der Stores in einer Komponente==
== Einbinden der Stores in eine Komponente==


<source lang="ecmascript">
<syntaxhighlight lang="ecmascript">
<script setup>
<script setup>
import { useId, onMounted, onUnmounted } from 'vue'
  import { useId, onMounted, onUnmounted } from 'vue'
   import storeSession  from '/store/StoreSession'
   import storeSession  from '/store/StoreSession'
   import storeGreeting  from '/store/StoreGreeting'
   import storeGreeting  from '/store/StoreGreeting'
Zeile 194: Zeile 193:
     greeting        = storeGreeting(),
     greeting        = storeGreeting(),


    reset          = () => { greeting.reset(); session.setFocus() },
     sayHello        = () => session.section = 'hello',
     sayHello        = () => session.section = 'hello',
     sayHelloOnEnter = new ControllerKeys('Enter', inputId, sayHello)
     sayHelloOnEnter = new ControllerKeys('Enter', inputId, sayHello)
Zeile 200: Zeile 200:
   //greeting.init({ hello: 'Hi, $1!', stranger: 'Nobody'})
   //greeting.init({ hello: 'Hi, $1!', stranger: 'Nobody'})


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


Zum Vergleich noch einmal das Skript ohne Store
Zum Vergleich noch einmal das Skript ohne Store:
<source lang="ecmascript">
<syntaxhighlight lang="ecmascript">
<!-- src/components/HelloWorld.vue without store -->
<!-- src/component/HelloWorld.vue without store -->
<script setup >
<script setup >
   import { useId, ref, computed, onMounted, onUnmounted } from 'vue'
   import { useId, ref, computed, onMounted, onUnmounted } from 'vue'
Zeile 232: Zeile 232:
   onUnmounted(...)
   onUnmounted(...)
</script>
</script>
</source>
</syntaxhighlight>


== Anpassung des Templates ==
== Anpassung des Templates ==
Zeile 238: 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_form" v-bind:class="section.visibility('form')">
   <section v-if="session.section==='form'">
     <h1>{{greeting.helloStranger}}</h1>
     <h1>{{greeting.helloStranger}}</h1>
     <form>
     <form>
       <div>
       <div>
         <label for="input_name">What is 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.visibility('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>


==Fortsetzung des Tutoriums==
==Fortsetzung des Tutoriums==

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)