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

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg
Kowa (Diskussion | Beiträge)
Die Seite wurde neu angelegt: „{{HTML5-Tutorium:JavaScript:HelloWorld:Menü}} {{InBearbeitung}} '''Musterlösung'''<br/> [{{Git-Server}}/kowa/wk_hello_world_vue Git-Repository], <code>git…“
 
Kowa (Diskussion | Beiträge)
 
(82 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 1: Zeile 1:
{{HTML5-Tutorium:JavaScript:HelloWorld:Menü}}
{{HTML5-Tutorium:JavaScript:HelloWorld:Menü}}


{{InBearbeitung}}
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/>
 
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/>
'''Musterlösung'''<br/>
[{{Git-Server}}/kowa/wk_hello_world_vue Git-Repository], <code>git checkout v04</code>/<code>git checkout v04a</code> (JavaScript)
[{{Git-Server}}/kowa/wk_hello_world_vue Git-Repository], <code>git checkout v04</code> (JavaScript)<br>
[{{Git-Server}}/kowa/wk_hello_world_vue_ts Git-Repository (TypeScript)], <code>git checkout v04</code>


== Anwendungsfälle (Use Cases) ==
== Anwendungsfälle (Use Cases) ==
Zeile 17: Zeile 15:
Definieren Sie für die Komponente <code>HelloWorld.vue</code> zwei Subkomponenten:
Definieren Sie für die Komponente <code>HelloWorld.vue</code> zwei Subkomponenten:


* <code>components/section/SectionHello.vue</code>
* <code>component/section/SectionHello.vue</code>
* <code>components/section/SectionQuestion.vue</code>
* <code>component/section/SectionForm.vue</code>


Zuvor sollten Sie zwei wiederverwendbare Hilfskomponenten definieren:
Zuvor sollten Sie zwei wiederverwendbare Hilfskomponenten definieren:


* <code>components/form/Button.vue</code>
* <code>component/form/FormButton.vue</code>
* <code>components/form/Textfield.vue</code>
* <code>component/form/FormTextfield.vue</code>


== 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 uuid
npm i
npm i
</source>
</syntaxhighlight>


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


=== <code>StoreSection</code> ===
=== <code>FormButton.vue</code> ===


Im Section-Store wird gespeichert, welche Section der Single-Page-Anwendung dem Benutzer aktuell präsentiert wird.
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="ecmascript">
Ein Button reagiert auf Klick-Ereignisse. Er reicht diese an die Komponente weiter, in der die aktuelle Button-Komponente eingebaut ist.
// src/store/StoreSection.js


import { defineStore } from 'pinia'
In Vue muss man ein entsprechendes Emit-Elemente definieren (<code>defineEmits</code>) und bei einem Klick
import { ref }        from 'vue'
auf den Button die zuvor deinierete Emit-Funktion aufrufen. Als Argument gibt man ihr das Ereignis und gegebenenfalls weitere weitere Werte als Argumente mit.


const storeSection =
<syntaxhighlight lang="html">
defineStore
<!-- src/component/form/FormButton.vue -->
( 'section', // must be unique


  () =>
<script setup>
   { const
   defineProps
      section = ref(''),
  ({ type:  { type: String, default: 'button' },
 
    label: { type: String, default: 'Button' }
      init =
  })
        p_section =>
        section.value = p_section,


      visability =
  const
        p_section =>
    emit  = defineEmits(['click']),
        section.value !== p_section ? 'hidden' : '',
    click = () => emit('click')
</script>


      change =
<template>
        p_section =>
   <input :value="label" :type @click="click">
        section.value = p_section
</template>
 
    return { init, visability, change }
   }
)
 
export default storeSection
</source>
 
=== <code>StoreGreeting</code> ===
 
Im Greeting-Store werden die Daten zur Begrüßung des Benutzers gespeichert.


<source lang="ecmascript">
<style scoped>
// src/store/StoreGreeting.js
  @import '/css/component/form/FormButton.css';
</style>
</syntaxhighlight>


import { defineStore }              from 'pinia'
Das zugehörige CSS-Modul importiert lediglich ein CSS-Modul für Formulare.
import { reactive, toRef, computed } from 'vue'


const
<syntaxhighlight lang="css">
  storeGreeting =
// src/css/component/form/FormButton.css
  defineStore
  ( 'greeting',


    () =>
@import 'Form.css';
    { const
</syntaxhighlight>
      state =
        reactive({ name: '', stranger: 'Stranger', hello: 'Hello, $1!' }),


      init =
Das Formular-CSS-Modul definiert Eigenschaften, die sich mehrere Formular-Elemente teilen.
        (p_state = {}) =>
        { Object.assign(state, p_state) },


      name = toRef(state, 'name'),
<syntaxhighlight lang="css">
/* src/css/component/form/Form.css */


      sayHello =
@import 'config.css';
        p_name string): string =>
        state.hello.replace('$1', p_name),


      helloStranger =
label
        computed(() => sayHello(state.stranger)),
{ font-family: var(--font-family-serif);
  font-size:  var(--font-size-p);
}


      hello =
label, input
        computed(() => sayHello(state.name))
{ width:      10em;
 
  font-size:  100%;
      return { init, name, helloStranger, hello }
  display:    inline-block;
     }
  box-sizing: border-box;
)
  margin:     0.5ex;
}


export default storeGreeting
label
</source>
{ text-align: right; }
</syntaxhighlight>


== Erstellen der Stores in einer Komponente==
=== <code>FormTextfield.vue</code> ===


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


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


   const
   const
     section = storeSection(),
     props          = defineProps
     greeting = storeGreeting(),
                    ({ label:      { type: String,  default: '' },
                        text:        { type: String,  default: '' },
                        autofocus:  { type: Boolean, default: false },
                        placeholder: { type: String, default: '' },
                    }),
    inputId        = useId(),
 
     session        = storeSession(),


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


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


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


Zum Vergleich noch einmal das Skript ohne Store
<template>
<source lang="ecmascript">
  <div>
// src/components/HelloWorld.vue without store
    <label :for="inputId" v-if="label !== ''">{{label}}</label>
    <input :id="inputId" type="text" v-model="text" :autofocus :placeholder>
  </div>
</template>


<script setup >
<style scoped>
   import { ref, computed, onMounted, onUnmounted } from 'vue'
   @import '/css/component/form/FormTextfield.css';
  import ControllerKey from '@/controller/ControllerKey'
</style>
</syntaxhighlight>


  const
Mit Hilfe von "computed" wird für die Property "text" bidirektionales Binding ermöglicht.
    section      = ref('question'),
    helloStranger = ref('Hello, Stranger'),
    name          = ref(''),


    hello      = computed(() => `Hello, ${name.value}!`),
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
<code>'update:<Propertyname>'</code> lauten.


    visability = (p_section: string) =>
Wie zuvor wird das Formular-CSS-Modul verwendet.
                  p_section !== section.value
                  ? 'hidden'
                  : '',


    sayHello  = () => { section.value = 'hello' },
<syntaxhighlight lang="css">
    controllerKey =
/* src/css/component/form/FormTextfield.css */
      new ControllerKey('Enter', sayHello, 'input_name')


  onMounted  (() => controllerKey.add())
@import 'Form.css';
  onUnmounted(() => controllerKey.remove())
</syntaxhighlight>
</script>
</source>


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


Das Template muss angepasst werden. Man muss nun auf die Store-Objekte zugreifen.
<syntaxhighlight lang="javascript">
/* /src/store/StoreGreeting.js */


<source lang="ecmascript">
...
// src/components/HelloWorld.vue
const
  storeGreeting =
  defineStore
  ( 'greeting',


<template>
    () =>
  <section id="section_question" v-bind:class="section.visability('question')">
     { const
     <h1>{{greeting.helloStranger}}</h1>
         state =
    <form>
          reactive({ placeholder: 'Your name', name: '', stranger: 'Stranger', hello: 'Hello, $1!' }),
      <div>
         <label for="input_name">What's your name?</label>
        <input id="input_name" type="text" autofocus
              v-model="greeting.name"
        />
      </div>
      <div>
        <input id="button_reset"  type="reset"  value="Reset"/>
        <input id="button_submit" type="button" value="Say hello"
              v-on:click="sayHello"
        />
      </div>
    </form>
  </section>
  <section id="section_hello" v-bind:class="section.visability('hello')">
    <h1>{{greeting.hello}}</h1>
    <p>Welcome to Web Programming!</p>
  </section>
</template>
</source>


== 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.
        placeholder =
          toRef(state, 'placeholder'),


=== Basisklasse <code>Store</code> ===
        ...


<source lang="ecmascript">
      return { init, placeholder, name, helloStranger, hello }
// src/common/Store/Store.js
    }
)


// cmp. https://medium.com/@mario.brendel1990/vue-3-the-new-store-a7569d4a546f
...
import { reactive } from 'vue'
</syntaxhighlight>


class Store
== Erstellen der Hello-World-Section-Komponenten ==
{ #initState
  state


  reset()
<syntaxhighlight lang="css">
  { if (this.state == null)
/* src/css/component/section/Section.css */
    { this.state = reactive({...this.initState}) }
    else
    { Object.assign(this.state, this.initState) }
  }


  init(p_state)
@import 'config.css';
  { Object.assign(this.initState, p_state);
    this.reset();
  }


   constructor(p_state)
section
   { this.initState = p_state;
{ h1
     this.reset();
   { font-size: var(--font-size-h1);
    padding:   0;
     margin:    0;
   }
   }
}
}
</syntaxhighlight>


export default Store
=== <code>SectionForm.vue</code> ===
</source>


=== Definition der Store-Klasse StoreSection ===
<code>SectionForm</code> enthält nur noch ein <code>section</code>-Element. Die Buttons und das Textfield werden als Komponenten eingebunden.


Nun kann der Section-Store als Klasse realisiert werden.
<syntaxhighlight lang="html">
// src/component/section/SectionForm.vue


<source lang="ecmascript">
<script setup>
// src/store/StoreSection.js
  import FormButton    from '/component/form/FormButton.vue'
  import FormTextfield from '/component/form/FormTextfield.vue'


import Store from '@/common/store/Store'
  import storeSession  from '/store/StoreSession'
  import storeGreeting from '/store/StoreGreeting'


class StoreSection extends Store
  const
{ constructor()
    session  = storeSession(),
   { super({ name: '' }) }
    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>


  visability(p_section)
<style scoped>
   { return this.state.name !== p_section ? 'hidden' : '' }
   @import '/css/component/section/SectionForm.css';
</style>
</syntaxhighlight>


  change(p_section)
<syntaxhighlight lang="css">
  { this.state.name = p_section }
/* src/css/component/section/SectionForm.css */
}


const storeSection = new StoreSection();
@import 'Section.css';


export default storeSection;
section
</source>
{ text-align:  center;
  margin-left:  auto;
  margin-right: auto;


=== Definition der Store-Klasse StoreGreeting ===
  h1
  { margin-bottom: 0.5ex; }
}
</syntaxhighlight>


Den Greeting-Store kann man analog definieren.
=== <code>SectionHello.vue</code> ===


<source lang="ecmascript">
<syntaxhighlight lang="html">
// src/store/StoreGreeting.js
// src/component/section/SectionHello.vue


import Store        from '@/common/store/Store'
<script setup>
import { computed } from 'vue'
  import storeSession  from '/store/StoreSession'
  import storeGreeting from '/store/StoreGreeting'


class StoreGreeting extends Store
  const
{ constructor(p_init = { hello: 'Hello, $1!', stranger: 'Stranger' })
    session  = storeSession(),
  { super({ name: '', ...p_init }); }
    greeting = storeGreeting()
</script>


   get name()
<template>
   { return this.state.name; }
   <!-- 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>


  set name(p_name)
<style scoped>
   { this.state.name = p_name; }
   @import '/css/component/section/SectionHello.css';
</style>
</syntaxhighlight>


  #sayHello(p_name)
<syntaxhighlight lang="css">
  { return this.state.hello.replace('$1', p_name); }
/* src/css/component/section/SectionHello.css */


  get helloStranger()
@import 'Section.css';
  { return computed(() => this.#sayHello(this.state.stranger)).value }


  get hello()
section
   { return computed(() => this.#sayHello(this.state.name !== ''
{ p
                                        ? this.state.name
   { font-size: var(--font-size-p-large); }
                                        : this.state.stranger
                                        )
                  ).value
  }
}
}
</syntaxhighlight>


const stateGreeting = new StoreGreeting()
== Modularisieren der Hello-World-Komponente ==


export default stateGreeting
Nun besteht die Hello-World-Komponenten nur noch aus zwei Section-Komponenten.
</source>


Störend ist hier, dass man für jedes Attribut wie {{zB}} <code>name</code> eine Getter- und eine Setter-Methode definieren muss.
Allerdings muss noch die Startkomponente, das heißt die Komponenten, die zunächst sichtbar sein soll, initialisiert werden.
Dieses Problem kann man mit einer weiteren Superklasse abmildern.


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


Man definiert die Klasse <code>StoreExtendible</code>, die es ermöglicht, bei der Initialisierung des Stores dynamisch reaktive Getter- und Setter-Methoden hinzuzufügen.
<syntaxhighlight lang="html">
<script setup>
  import SectionForm  from '/component/section/SectionForm.vue'
  import SectionHello from '/component/section/SectionHello.vue'
  import storeSession from '/store/StoreSession'


<source lang="ecmascript">
  const
// src/common/Store/StoreExtendible.js
    session = storeSession()


import Store from './Store'
  // const in code: session should be initialized via a config file
  session.init('form')
</script>
 
<template>
  <SectionForm/>
  <SectionHello/>
</template>


class StoreExtendible extends Store
<style scoped>
{ #add(p_properties, p_getter, p_setter)
   @import '/css/component/HelloWorld.css';
   { if (typeof p_properties === 'string')
</style>
    { Object.defineProperty
</syntaxhighlight>
      ( this.constructor.prototype,
 
        p_properties,
Das Folgende sind nur Dummy-CSS-Dateien, die aus Symmetriegründen
        { ...( p_getter ? { get: () => this.state[p_properties] } : {} ),
angelegt wurde: Für jede Komponente gibt es
          ...( p_setter ? { set: (value) =>
gemäß WK-Konvention eine eigene CSS-Datei.
                                { 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)
<syntaxhighlight lang="css">
  { this.#add(p_properties, true, false) }
/* src/css/component/component.css */


  addSetter(p_properties)
@import 'config.css';
  { this.#add(p_properties, false, true) }
</syntaxhighlight>


  addGetterSetter(p_properties)
<syntaxhighlight lang="css">
  { this.#add(p_properties, true, true) }
/* src/css/component/HelloWorld.css */
}


export default StoreExtendible
@import 'component.css';
</source>
</syntaxhighlight>


=== Verwendung der alternativen Stores in einer Komponente ===
== Dynamisches Laden von Komponenten “below the fold” ==


<source lang="ecmascript">
In der Hello-World Komponente sollten nur die Komponenten statisch geladen werden, die
// src/components/HelloWorld.vue with store class
“above the fold” liegen. Das ist hier die Formular-Komponente.
Die Hello-Komponente sollte dagegen asynchron geladen werden.
<syntaxhighlight lang="html">
<script setup>
  import { defineAsyncComponent, computed } from 'vue'


<script setup >
   import SectionForm  from '/component/section/SectionForm.vue'
   import { onMounted, onUnmounted } from 'vue'
//import SectionHello from '/component/section/SectionHello.vue'
  import ControllerKey from '@/controller/ControllerKey'
   import storeSession from '/store/StoreSession'
  import section from '@/store/StoreSectionClass'
//import greeting from '@/store/StoreGreetingClass'
   import greeting from '@/store/StoreGreetingClassExtendible'


   const
   const
     sayHello = () => { section.change('hello') },
     session        = storeSession(),
    controllerKey =
    helloComponent =
      new ControllerKey('Enter', sayHello, 'input_name')
      computed(() => defineAsyncComponent(() => import('/component/section/SectionHello.vue')))


   section.init({ name: 'question'})
   // const in code: session should be initialized via a config file
//greeting.init({ hello: 'Hallo, $1!', stranger: 'Fremder'})
  session.init('form')
 
  onMounted  (() => controllerKey.add())
  onUnmounted(() => controllerKey.remove())
</script>
</script>
</source>


Zum Vergleich: Die Verwendung der zuerst definierten Stores.
<template>
  <SectionForm/>
<source lang="ecmascript">
  <component :is="helloComponent"/>
// src/components/HelloWorld.vue
</template>
 
<script setup>
  import { onMounted, onUnmounted } from 'vue'
  import ControllerKey from '@/controller/ControllerKey'
  import storeSection  from '@/store/StoreSection'
  import storeGreeting from '@/store/StoreGreeting'


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


  const
Beim Build-Prozess werden nun zwei JavaScript-Dateien erstellt.
    section  = storeSection(),
Die Hauptdatei wird zu Beginn der Anwendung geladen, die zweite wird asynchron nachgeladen.
    greeting = storeGreeting(),


    sayHello = () => { section.change('hello') },
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.
    controllerKey =
      new ControllerKey('Enter', sayHello, 'input_name')


  section.init('question')
== Bereinigen der Datei <code>body.css</code> ==
//greeting.init({ hello: 'Hallo, $1!', stranger: 'Fremder'})


  onMounted  (() => controllerKey.add())
Die in den obigen CSS-Modulen enthaltenen Inhalte können und sollten
  onUnmounted(() => controllerKey.remove())
aus der Datei <code>body.css</code> gelöscht werden.
</script>
</source>


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


Sie sollten nun [[HTML5-Tutorium: JavaScript: Hello World Vue 04|Teil 4 des Vue-Tutoriums]] bearbeiten.
Sie sollten nun [[HTML5-Tutorium: JavaScript: Hello World Vue 05|Teil 5 des Vue-Tutoriums]] bearbeiten.
Nun wird die Anwendung modularisiert.
In diesem Tutorium werden die Daten extern aus JSON-Dateien geladen, erst statisch, dann dynamisch.
Zum Schluss wird die Anwendung internationalisiert.


==Quellen==
==Quellen==
<!--references/-->
<!--references/-->
<ol>
<ol>
<li value="1"> {{Quelle|Kowarschick, W.: Multimedia-Programmierung}}</li>
<li value="1"> {{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: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)