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

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


'''Musterlösung'''<br/>
v04:&nbsp;&nbsp;&nbsp;[https://glossar.hs-augsburg.de/beispiel/tutorium/2025/wk_hello_world/wk_hello_world_vue/web_v04/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_v04%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_v04%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_v04%2Findex.html Google PageSpeed])<br/>
[{{Git-Server}}/kowa/wk_hello_world_vue Git-Repository], <code>git checkout v04</code><br>
v04a:&nbsp;[https://glossar.hs-augsburg.de/beispiel/tutorium/2025/wk_hello_world/wk_hello_world_vue/web_v04a/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_v04a%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_v04a%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_v04a%2Findex.html Google PageSpeed])<br/><br/>
[{{Git-Server}}/kowa/wk_hello_world_vue Git-Repository], <code>git checkout v04</code>/<code>git checkout v04a</code> (JavaScript)


== Anwendungsfälle (Use Cases) ==
== Anwendungsfälle (Use Cases) ==
Zeile 24: Zeile 25:
== Erstellen eines neuen Projektzweigs ==
== Erstellen eines neuen Projektzweigs ==


Erstellen Sie einen neuen Projektzweig (branch) innerhalb von <code>hello_world_vue</code> und fügen Sie das Package <code>uuid</code> hinzu:
Erstellen Sie einen neuen Projektzweig (branch) innerhalb von <code>hello_world_vue</code>:
 
<syntaxhighlight lang="bash">
<source lang="bash">
git checkout v03    # Wechsle in den Branch v03
git checkout v03    # Wechsle in den Branch v03
git checkout -b v04  # Klone v03 in einen neuen Branch v04
git checkout -b v04  # Klone v03 in einen neuen Branch v04
npm i
npm i
npm i uuid
</syntaxhighlight>
</source>


== Erstellen der Hilfskomponenten ==
== Erstellen der Hilfskomponenten ==
Zeile 38: Zeile 37:


Eine parametrisierbare Button-Komponente.
Eine parametrisierbare Button-Komponente.
Ein Button hat zwei Properties: Den Typ (<code>button</code>, <code>reset</code> ...) und den Label, der auf den Button geschrieben werden soll.


<source lang="html">
Ein Button reagiert auf Klick-Ereignisse. Er reicht diese an die Komponente weiter, in der die aktuelle Button-Komponente eingebaut ist.
 
In Vue muss man ein entsprechendes Emit-Elemente definieren (<code>defineEmits</code>) und bei einem Klick
auf den Button die zuvor deinierete Emit-Funktion aufrufen. Als Argument gibt man ihr das Ereignis und gegebenenfalls weitere weitere Werte als Argumente mit.
 
<syntaxhighlight lang="html">
<!-- src/component/form/FormButton.vue -->
<!-- src/component/form/FormButton.vue -->


Zeile 54: Zeile 59:


<template>
<template>
   <input :value="label" :type @click="click"/>
   <input :value="label" :type @click="click">
</template>
</template>


<style scoped>
<style scoped>
   @import '/css/component/form/FormButton';
   @import '/css/component/form/FormButton.css';
</style>
</style>
</source>
</syntaxhighlight>


Das zugehörige CSS-Modul importiert lediglich ein CSS-Modul für Formulare.
Das zugehörige CSS-Modul importiert lediglich ein CSS-Modul für Formulare.


<source lang="css">
<syntaxhighlight lang="css">
// src/css/component/form/FormButton.css
// src/css/component/form/FormButton.css


@import 'Form';
@import 'Form.css';
</source>
</syntaxhighlight>


Das Formular-CSS-Modul definiert Eigenschaften, die sich mehrere Formular-Elemente teilen.
Das Formular-CSS-Modul definiert Eigenschaften, die sich mehrere Formular-Elemente teilen.


<source lang="css">
<syntaxhighlight lang="css">
/* src/css/component/form/_Form.css */
/* src/css/component/form/Form.css */


@import 'config';
@import 'config.css';


label
label
{ font-family: $font-family-serif;
{ font-family: var(--font-family-serif);
   font-size:  $font-size-p !important;
   font-size:  var(--font-size-p);
}
}


Zeile 92: Zeile 97:
label
label
{ text-align: right; }
{ text-align: right; }
</source>
</syntaxhighlight>


=== <code>FormTextfield.vue</code> ===
=== <code>FormTextfield.vue</code> ===


Eine parametrisierbare Textfield-Komponente zum Erfassen von Texteingaben.
Eine parametrisierbare Textfield-Komponente zum Erfassen von Texteingaben.
<source lang="html">
<syntaxhighlight lang="html">
// src/component/form/FormTextfield.vue
// src/component/form/FormTextfield.vue


<script setup>
<script setup>
   import { computed, onMounted, onUnmounted } from 'vue'
   import { useId, computed, onMounted, onUnmounted } from 'vue'
   import { v4 as uuid } from 'uuid'
 
   import ControllerKey  from '@/controller/ControllerKey'
   import storeSession  from '/store/StoreSession'
   import ControllerKeys from '/controller/ControllerKeys'


   const
   const
     props         = defineProps
     props         = defineProps
                    ({ label:     { type: String,  default: '' },
                    ({ label:       { type: String,  default: '' },
                      text:     { type: String,  default: '' },
                        text:       { type: String,  default: '' },
                      autofocus: { type: Boolean, default: false }
                        autofocus:   { type: Boolean, default: false },
                    }),
                        placeholder: { type: String, default: '' },
    id            = uuid(),
                    }),
    emit          = defineEmits(['update:text', 'enter']),
     inputId        = useId(),
    text          = computed
                    ({ get()        { return props.text },
                      set(p_value) { return emit('update:text', p_value) }
                    }),
     enter        = () => emit('enter'),
    controllerKey = new ControllerKey('Enter', enter, id)


   onMounted (() => controllerKey.add())
    session        = storeSession(),
   onUnmounted(() => controllerKey.remove())
 
    emit          = defineEmits(['update:text', 'enter']),
    text          = computed
                    ({ get()      { return props.text },
                        set(p_text) { return emit('update:text', p_text) }
                    }),
    enter          = () => emit('enter'),
    controllerKeys = new ControllerKeys('Enter', inputId, enter)
 
  session.initAutofocus(props.autofocus ? inputId : null)
 
   onMounted   ( () => controllerKeys.add()   )
   onUnmounted ( () => controllerKeys.remove() )
</script>
</script>


<template>
<template>
   <div>
   <div>
     <label :for="id" v-if="label !== ''">{{label}}</label>
     <label :for="inputId" v-if="label !== ''">{{label}}</label>
     <input :id type="text" v-model="text" :autofocus>
     <input :id="inputId" type="text" v-model="text" :autofocus :placeholder>
<!--<input :id="id" type="text" v-model="text" :autofocus="autofocus">-->
   </div>
   </div>
</template>
</template>


<style scoped>
<style scoped>
   @import '/css/component/form/FormTextfield';
   @import '/css/component/form/FormTextfield.css';
</style>
</style>
</source>
</syntaxhighlight>


Mit Hilfe von "computed" wird für die Property "text" bidirektionales Binding ermöglicht.
Mit Hilfe von "computed" wird für die Property "text" bidirektionales Binding ermöglicht.
Zeile 145: Zeile 156:
Wie zuvor wird das Formular-CSS-Modul verwendet.
Wie zuvor wird das Formular-CSS-Modul verwendet.


<source lang="css">
<syntaxhighlight lang="css">
/* src/css/component/form/FormTextfield.css */
/* src/css/component/form/FormTextfield.css */


@import 'Form';
@import 'Form.css';
</source>
</syntaxhighlight>
 
Das PLazhalter-Element, das im leeren Textfeld angezeigt wird, sollte in der Greeting-Datei festgelegt werden werden.
 
<syntaxhighlight lang="javascript">
/* /src/store/StoreGreeting.js */
 
...
const
  storeGreeting =
  defineStore
  ( 'greeting',
 
    () =>
    { const
        state =
          reactive({ placeholder: 'Your name', name: '', stranger: 'Stranger', hello: 'Hello, $1!' }),
 
        ...
 
        placeholder =
          toRef(state, 'placeholder'),
 
        ...
 
      return { init, placeholder, name, helloStranger, hello }
    }
)
 
...
</syntaxhighlight>


== Erstellen der Hello-World-Section-Komponenten ==
== Erstellen der Hello-World-Section-Komponenten ==
<syntaxhighlight lang="css">
/* src/css/component/section/Section.css */
@import 'config.css';
section
{ h1
  { font-size: var(--font-size-h1);
    padding:  0;
    margin:    0;
  }
}
</syntaxhighlight>


=== <code>SectionForm.vue</code> ===
=== <code>SectionForm.vue</code> ===
Zeile 157: Zeile 212:
<code>SectionForm</code> enthält nur noch ein <code>section</code>-Element. Die Buttons und das Textfield werden als Komponenten eingebunden.
<code>SectionForm</code> enthält nur noch ein <code>section</code>-Element. Die Buttons und das Textfield werden als Komponenten eingebunden.


<source lang="html">
<syntaxhighlight lang="html">
// src/component/section/SectionForm.vue
// src/component/section/SectionForm.vue


Zeile 164: Zeile 219:
   import FormTextfield from '/component/form/FormTextfield.vue'
   import FormTextfield from '/component/form/FormTextfield.vue'


   import storeSection from '/store/StoreSection'
   import storeSession from '/store/StoreSession'
   import storeGreeting from '/store/StoreGreeting'
   import storeGreeting from '/store/StoreGreeting'


   const
   const
     section = storeSection(),
     session = storeSession(),
     greeting = storeGreeting(),
     greeting = storeGreeting(),
     sayHello = () => section.change('hello')
    reset    = () => { greeting.reset(); session.setFocus() },
     sayHello = () => session.section = 'hello'
</script>
</script>


<template>
<template>
   <section v-bind:class="section.visibility('form')">
  <!-- const in code: 'form' should be passed via a prop -->
   <section v-if="session.section==='form'">
     <h1>{{greeting.helloStranger}}</h1>
     <h1>{{greeting.helloStranger}}</h1>
     <form>
     <form>
       <div>
       <div>
         <FormTextfield label="What's your name?" autofocus
         <FormTextfield label="What's your name?"
                       v-model:text="greeting.name" @enter="sayHello"
                       v-model:text="greeting.name"
                      @enter="sayHello"
                      autofocus
                      :placeholder="greeting.placeholder"
         />
         />
       </div>
       </div>
       <div>
       <div>
         <FormButton label="Reset" type="reset" />
         <FormButton label="Reset"     type="reset"  @click="reset"/>
         <FormButton label="Say Hello" @click="sayHello"/>
         <FormButton label="Say Hello" type="button" @click="sayHello"/>
       </div>
       </div>
     </form>
     </form>
Zeile 191: Zeile 251:


<style scoped>
<style scoped>
   @import '/css/component/section/SectionForm';
   @import '/css/component/section/SectionForm.css';
</style>
</style>
</source>
</syntaxhighlight>


<source lang="css">
<syntaxhighlight lang="css">
/* src/css/component/section/SectionForm.css */
/* src/css/component/section/SectionForm.css */


@import 'Section';
@import 'Section.css';


section
section
Zeile 208: Zeile 268:
   { margin-bottom: 0.5ex; }
   { margin-bottom: 0.5ex; }
}
}
</source>
</syntaxhighlight>
 
<source lang="css">
/* src/css/component/section/_Section.css */
 
@import 'config';
 
section
{ h1
  { font-size: $font-size-h1;
    padding:  0;
    margin:    0;
  }
}
</source>


=== <code>SectionHello.vue</code> ===
=== <code>SectionHello.vue</code> ===


<source lang="html">
<syntaxhighlight lang="html">
// src/component/section/SectionHello.vue
// src/component/section/SectionHello.vue


<script setup>
<script setup>
   import storeSection from '@/store/StoreSection'
   import storeSession from '/store/StoreSession'
   import storeGreeting from '@/store/StoreGreeting'
   import storeGreeting from '/store/StoreGreeting'


   const
   const
     section = storeSection(),
     session = storeSession(),
     greeting = storeGreeting()
     greeting = storeGreeting()
</script>
</script>


<template>
<template>
   <section v-bind:class="section.visibility('hello')">
  <!-- const in code: 'hello' should be passed via a prop -->
   <section v-if="session.section==='hello'">
     <h1>{{greeting.hello}}</h1>
     <h1>{{greeting.hello}}</h1>
     <p>Welcome to Web Programming!</p>
     <p>Welcome to Web Programming!</p>
Zeile 246: Zeile 293:


<style scoped>
<style scoped>
   @import '/css/component/section/SectionHello';
   @import '/css/component/section/SectionHello.css';
</style>
</style>
</source>
</syntaxhighlight>


<source lang="css">
<syntaxhighlight lang="css">
/* src/css/component/section/SectionHello.css */
/* src/css/component/section/SectionHello.css */


@import 'Section';
@import 'Section.css';


section
section
{ p
{ p
   { font-size: $font-size-p-large !important; }
   { font-size: var(--font-size-p-large); }
}
}
</source>
</syntaxhighlight>


== Modularisieren der Hello-World-Komponente ==
== Modularisieren der Hello-World-Komponente ==
Zeile 269: Zeile 316:
Dies kann später entfallen, wenn die Stores mit Hilfe von JSON-Dateien initialisiert werden.
Dies kann später entfallen, wenn die Stores mit Hilfe von JSON-Dateien initialisiert werden.


<source lang="html">
<syntaxhighlight lang="html">
// src/component/HelloWorld.vue
 
<script setup>
<script setup>
   import SectionForm  from '/component/section/SectionForm.vue'
   import SectionForm  from '/component/section/SectionForm.vue'
   import SectionHello from '/component/section/SectionHello.vue'
   import SectionHello from '/component/section/SectionHello.vue'
  import storeSession from '/store/StoreSession'


   import storeSection from '/store/StoreSection'
   const
  const section = storeSection()
    session = storeSession()
   section.init('form')
 
  // const in code: session should be initialized via a config file
   session.init('form')
</script>
</script>


Zeile 289: Zeile 337:
   @import '/css/component/HelloWorld.css';
   @import '/css/component/HelloWorld.css';
</style>
</style>
</source>
</syntaxhighlight>


Das Folgende ist nur eine Dummy-CSS-Datei, die aus Symmetriegründen
Das Folgende sind nur Dummy-CSS-Dateien, die aus Symmetriegründen
angelegt wurde: Für jede Komponente gibt es
angelegt wurde: Für jede Komponente gibt es
gemäß WK-Konvention eine eigene CSS-Datei.
gemäß WK-Konvention eine eigene CSS-Datei.


<source lang="css">
<syntaxhighlight lang="css">
/* src/css/component/HelloWorld.css */
/* src/css/component/component.css */


@import 'config';
@import 'config.css';
</source>
</syntaxhighlight>


== Bereinigen der Datei <code>body.css</code> ==
<syntaxhighlight lang="css">
 
/* src/css/component/HelloWorld.css */
Die in den obigen CSS-Modulen enthaltenen Inhalte können und sollten
aus der Datei <code>body.css</code> gelöscht werden.


== Vereinfachung von <code>StoreSection</code> ==
@import 'component.css';
</syntaxhighlight>


Die Funktion <code>visibility</code> zum Überprüfen, ob eine Section angezeigt werden soll, kann durch Vue-Attribute <code>v-if</code> ersetzt werden. Dadurch vereinfacht sich der code von deutlich.
== Dynamisches Laden von Komponenten “below the fold” ==


<source lang="javascript">
In der Hello-World Komponente sollten nur die Komponenten statisch geladen werden, die
import { defineStore } from 'pinia'
“above the fold” liegen. Das ist hier die Formular-Komponente.
import { ref }         from 'vue'
Die Hello-Komponente sollte dagegen asynchron geladen werden.
<syntaxhighlight lang="html">
<script setup>
  import { defineAsyncComponent, computed } from 'vue'


const storeSection =
  import SectionForm  from '/component/section/SectionForm.vue'
defineStore
//import SectionHello from '/component/section/SectionHello.vue'
( 'section',
  import storeSession from '/store/StoreSession'


   () =>
   const
  { const
    session        = storeSession(),
      currentSection =  
    helloComponent =
        ref(''),
       computed(() => defineAsyncComponent(() => import('/component/section/SectionHello.vue')))
     
       init =  
        p_section => currentSection.value = p_section


    return { init, currentSection }
  // const in code: session should be initialized via a config file
   }
   session.init('form')
)
</script>
 
export default storeSection
</source>


'''Anmerkung''': Sobal die Initialisierung über eine globale Konfigurationsdatei erfolgt, können wir die Initialisierungsfuktion auch noch entfernen.
Die Kompontente <code>HelloWorld.vue</code>, die bislang auf die Arbeit der Funktion <code>visibility</code> vertraut, muss entsprechend angepasst werden. Sie testet mittels <code>v_if</code> welche Section angezeigt werden muss.
<source lang="html">
<template>
<template>
   <SectionForm v-if="section.currentSection==='form'"/>
   <SectionForm/>
   <SectionHello v-if="section.currentSection==='hello'"/>
   <component :is="helloComponent"/>
</template>
</template>
</source>


Nun kann <code>visiblity</code> aus den beiden Komponenten <code>SectionForm</code> und <code>SectionHello</code> entfernt werden.
<style scoped>
<source lang="html">
  @import '/css/component/HelloWorld.css';
<script setup>
</style>
  ...
</syntaxhighlight>
  sayHello = () => section.change('hello')
</script>


<template>
Beim Build-Prozess werden nun zwei JavaScript-Dateien erstellt.
  <section v-bind:class="section.visibility('form')">
Die Hauptdatei wird zu Beginn der Anwendung geladen, die zweite wird asynchron nachgeladen.
  ...
</template>
</source>


wird zu  
In diesem Fall bringt diese Art der Optimierung nichts, da der Code zum dynamischen NAchladen von JavaScript-Komponenten die Hauptdatei um ca. 3 KByte vergrößert, der JavaScript-Code für die Hello-Komponenten aber nur wenige Hundert Byte groß ist. Bei komplexeren Beispielen ist dieses Vorgehen allerdings zu empfehlen.


<source lang="html">
== Bereinigen der Datei <code>body.css</code> ==
<script setup>
  ...
  sayHello = () => section.currentSection = 'hello'
</script>


<template>
Die in den obigen CSS-Modulen enthaltenen Inhalte können und sollten
  <section>
aus der Datei <code>body.css</code> gelöscht werden.
  ...
</template>
</source>
 
Die zweite Section <code>SectionHello</code> muss entsprechend angepasst werden. Hier wird <code>StoreSection</code> gar nicht mehr benötigt.


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

Aktuelle Version vom 12. Mai 2025, 12:45 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

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

Git-Repository, git checkout v04/git checkout v04a (JavaScript)

Anwendungsfälle (Use Cases)

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

Aufgabe

Aufgabe: Modularisieren Sie HelloWorld.vue Definieren Sie für die Komponente HelloWorld.vue zwei Subkomponenten:

  • component/section/SectionHello.vue
  • component/section/SectionForm.vue

Zuvor sollten Sie zwei wiederverwendbare Hilfskomponenten definieren:

  • component/form/FormButton.vue
  • component/form/FormTextfield.vue

Erstellen eines neuen Projektzweigs

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

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

Erstellen der Hilfskomponenten

FormButton.vue

Eine parametrisierbare Button-Komponente. Ein Button hat zwei Properties: Den Typ (button, reset ...) und den Label, der auf den Button geschrieben werden soll.

Ein Button reagiert auf Klick-Ereignisse. Er reicht diese an die Komponente weiter, in der die aktuelle Button-Komponente eingebaut ist.

In Vue muss man ein entsprechendes Emit-Elemente definieren (defineEmits) und bei einem Klick auf den Button die zuvor deinierete Emit-Funktion aufrufen. Als Argument gibt man ihr das Ereignis und gegebenenfalls weitere weitere Werte als Argumente mit.

<!-- src/component/form/FormButton.vue -->

<script setup>
  defineProps
  ({ type:  { type: String, default: 'button' },
     label: { type: String, default: 'Button' }
  })

  const
    emit  = defineEmits(['click']),
    click = () => emit('click')
</script>

<template>
  <input :value="label" :type @click="click">
</template>

<style scoped>
  @import '/css/component/form/FormButton.css';
</style>

Das zugehörige CSS-Modul importiert lediglich ein CSS-Modul für Formulare.

// src/css/component/form/FormButton.css

@import 'Form.css';

Das Formular-CSS-Modul definiert Eigenschaften, die sich mehrere Formular-Elemente teilen.

/* src/css/component/form/Form.css */

@import 'config.css';

label
{ font-family: var(--font-family-serif);
  font-size:   var(--font-size-p);
}

label, input
{ width:      10em;
  font-size:  100%;
  display:    inline-block;
  box-sizing: border-box;
  margin:     0.5ex;
}

label
{ text-align: right; }

FormTextfield.vue

Eine parametrisierbare Textfield-Komponente zum Erfassen von Texteingaben.

// src/component/form/FormTextfield.vue

<script setup>
  import { useId, computed, onMounted, onUnmounted } from 'vue'

  import storeSession   from '/store/StoreSession'
  import ControllerKeys from '/controller/ControllerKeys'

  const
    props          = defineProps
                     ({ label:       { type: String,  default: '' },
                        text:        { type: String,  default: '' },
                        autofocus:   { type: Boolean, default: false },
                        placeholder: { type: String,  default: '' },
                     }),
    inputId        = useId(),

    session        = storeSession(),

    emit           = defineEmits(['update:text', 'enter']),
    text           = computed
                     ({ get()       { return props.text },
                        set(p_text) { return emit('update:text', p_text) }
                     }),
    enter          = () => emit('enter'),
    controllerKeys = new ControllerKeys('Enter', inputId, enter)

  session.initAutofocus(props.autofocus ? inputId : null)

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

<template>
  <div>
    <label :for="inputId" v-if="label !== ''">{{label}}</label>
    <input :id="inputId" type="text" v-model="text" :autofocus :placeholder>
  </div>
</template>

<style scoped>
  @import '/css/component/form/FormTextfield.css';
</style>

Mit Hilfe von "computed" wird für die Property "text" bidirektionales Binding ermöglicht.

Die Getterfunktion liest den Wert aus der Property des Aufrufers. Die Setterfunktion erzeugt bei jeder Änderung ein passendes Ereignis. Der Name des Ereignisse muss laut Vue-Konvention 'update:<Propertyname>' lauten.

Wie zuvor wird das Formular-CSS-Modul verwendet.

/* src/css/component/form/FormTextfield.css */

@import 'Form.css';

Das PLazhalter-Element, das im leeren Textfeld angezeigt wird, sollte in der Greeting-Datei festgelegt werden werden.

/* /src/store/StoreGreeting.js */

...
const
  storeGreeting =
  defineStore
  ( 'greeting',

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

        ...

        placeholder =
          toRef(state, 'placeholder'),

        ...

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

...

Erstellen der Hello-World-Section-Komponenten

/* src/css/component/section/Section.css */

@import 'config.css';

section
{ h1
  { font-size: var(--font-size-h1);
    padding:   0;
    margin:    0;
  }
}

SectionForm.vue

SectionForm enthält nur noch ein section-Element. Die Buttons und das Textfield werden als Komponenten eingebunden.

// src/component/section/SectionForm.vue

<script setup>
  import FormButton    from '/component/form/FormButton.vue'
  import FormTextfield from '/component/form/FormTextfield.vue'

  import storeSession  from '/store/StoreSession'
  import storeGreeting from '/store/StoreGreeting'

  const
    session  = storeSession(),
    greeting = storeGreeting(),
    reset    = () => { greeting.reset(); session.setFocus() },
    sayHello = () => session.section = 'hello'
</script>

<template>
  <!-- const in code: 'form' should be passed via a prop -->
  <section v-if="session.section==='form'">
    <h1>{{greeting.helloStranger}}</h1>
    <form>
      <div>
        <FormTextfield label="What's your name?"
                       v-model:text="greeting.name"
                       @enter="sayHello"
                       autofocus
                       :placeholder="greeting.placeholder"
        />
      </div>
      <div>
        <FormButton label="Reset"     type="reset"  @click="reset"/>
        <FormButton label="Say Hello" type="button" @click="sayHello"/>
      </div>
    </form>
  </section>
</template>

<style scoped>
  @import '/css/component/section/SectionForm.css';
</style>
/* src/css/component/section/SectionForm.css */

@import 'Section.css';

section
{ text-align:   center;
  margin-left:  auto;
  margin-right: auto;

  h1
  { margin-bottom: 0.5ex; }
}

SectionHello.vue

// src/component/section/SectionHello.vue

<script setup>
  import storeSession  from '/store/StoreSession'
  import storeGreeting from '/store/StoreGreeting'

  const
    session  = storeSession(),
    greeting = storeGreeting()
</script>

<template>
  <!-- const in code: 'hello' should be passed via a prop -->
  <section v-if="session.section==='hello'">
    <h1>{{greeting.hello}}</h1>
    <p>Welcome to Web Programming!</p>
  </section>
</template>

<style scoped>
  @import '/css/component/section/SectionHello.css';
</style>
/* src/css/component/section/SectionHello.css */

@import 'Section.css';

section
{ p
  { font-size: var(--font-size-p-large); }
}

Modularisieren der Hello-World-Komponente

Nun besteht die Hello-World-Komponenten nur noch aus zwei Section-Komponenten.

Allerdings muss noch die Startkomponente, das heißt die Komponenten, die zunächst sichtbar sein soll, initialisiert werden.

Dies kann später entfallen, wenn die Stores mit Hilfe von JSON-Dateien initialisiert werden.

<script setup>
  import SectionForm  from '/component/section/SectionForm.vue'
  import SectionHello from '/component/section/SectionHello.vue'
  import storeSession from '/store/StoreSession'

  const
    session = storeSession()

  // const in code: session should be initialized via a config file
  session.init('form')
</script>

<template>
  <SectionForm/>
  <SectionHello/>
</template>

<style scoped>
  @import '/css/component/HelloWorld.css';
</style>

Das Folgende sind nur Dummy-CSS-Dateien, die aus Symmetriegründen angelegt wurde: Für jede Komponente gibt es gemäß WK-Konvention eine eigene CSS-Datei.

/* src/css/component/component.css */

@import 'config.css';
/* src/css/component/HelloWorld.css */

@import 'component.css';

Dynamisches Laden von Komponenten “below the fold”

In der Hello-World Komponente sollten nur die Komponenten statisch geladen werden, die “above the fold” liegen. Das ist hier die Formular-Komponente. Die Hello-Komponente sollte dagegen asynchron geladen werden.

<script setup>
  import { defineAsyncComponent, computed } from 'vue'

  import SectionForm  from '/component/section/SectionForm.vue'
//import SectionHello from '/component/section/SectionHello.vue'
  import storeSession from '/store/StoreSession'

  const
    session        = storeSession(),
    helloComponent =
      computed(() => defineAsyncComponent(() => import('/component/section/SectionHello.vue')))

  // const in code: session should be initialized via a config file
  session.init('form')
</script>

<template>
  <SectionForm/>
  <component :is="helloComponent"/>
</template>

<style scoped>
  @import '/css/component/HelloWorld.css';
</style>

Beim Build-Prozess werden nun zwei JavaScript-Dateien erstellt. Die Hauptdatei wird zu Beginn der Anwendung geladen, die zweite wird asynchron nachgeladen.

In diesem Fall bringt diese Art der Optimierung nichts, da der Code zum dynamischen NAchladen von JavaScript-Komponenten die Hauptdatei um ca. 3 KByte vergrößert, der JavaScript-Code für die Hello-Komponenten aber nur wenige Hundert Byte groß ist. Bei komplexeren Beispielen ist dieses Vorgehen allerdings zu empfehlen.

Bereinigen der Datei body.css

Die in den obigen CSS-Modulen enthaltenen Inhalte können und sollten aus der Datei body.css gelöscht werden.

Fortsetzung des Tutoriums

Sie sollten nun Teil 5 des Vue-Tutoriums bearbeiten. In diesem Tutorium werden die Daten extern aus JSON-Dateien geladen, erst statisch, dann dynamisch. Zum Schluss wird die Anwendung internationalisiert.

Quellen

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