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)
Keine Bearbeitungszusammenfassung
Zeile 36: Zeile 36:
</source>
</source>


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


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


Im Section-Store wird gespeichert, welche Section der Single-Page-Anwendung dem Benutzer aktuell präsentiert wird.
Eine parametrisierbare Button-Komponente.
 
<source lang="html">
<source lang="ecmascript">
// src/components/form/Button.vue
// src/store/StoreSection.js
 
import { defineStore } from 'pinia'
import { ref }        from 'vue'
 
const storeSection =
defineStore
( 'section', // must be unique
 
  () =>
  { const
      section = ref(''),
 
      init =
        p_section =>
        section.value = p_section,
 
      visability =
        p_section =>
        section.value !== p_section ? 'hidden' : '',
 
      change =
        p_section =>
        section.value = p_section
 
    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">
// 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'),
 
      sayHello =
        p_name string): string =>
        state.hello.replace('$1', p_name),
 
      helloStranger =
        computed(() => sayHello(state.stranger)),
 
      hello =
        computed(() => sayHello(state.name))
 
      return { init, name, helloStranger, hello }
    }
)
 
export default storeGreeting
</source>
 
== Erstellen der Stores in einer Komponente==
 
<source lang="ecmascript">
// src/components/HelloWorld.vue


<script setup>
<script setup>
   import { onMounted, onUnmounted } from 'vue'
   defineProps
  import ControllerKey from '@/controller/ControllerKey'
  ({ type:  { type: String, default: 'button' },
   import storeSection  from '@/store/StoreSection'
    label: { type: String, default: 'Button' }
  import storeGreeting from '@/store/StoreGreeting'
   })


   const
   const
     section = storeSection(),
     emit = defineEmits(['click']),
    greeting = storeGreeting(),
     click = () => emit('click')
 
    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>
 
Zum Vergleich noch einmal das Skript ohne Store
<source lang="ecmascript">
// src/components/HelloWorld.vue without store
 
<script setup >
  import { ref, computed, onMounted, onUnmounted } from 'vue'
  import ControllerKey from '@/controller/ControllerKey'
 
  const
    section      = ref('question'),
    helloStranger = ref('Hello, Stranger'),
    name          = ref(''),
 
    hello      = computed(() => `Hello, ${name.value}!`),
 
    visability = (p_section: string) =>
                  p_section !== section.value
                  ? 'hidden'
                  : '',
 
     sayHello  = () => { section.value = 'hello' },
    controllerKey =
      new ControllerKey('Enter', sayHello, 'input_name')
 
  onMounted  (() => controllerKey.add())
  onUnmounted(() => controllerKey.remove())
</script>
</script>
</source>
== Anpassung des Templates ==
Das Template muss angepasst werden. Man muss nun auf die Store-Objekte zugreifen.
<source lang="ecmascript">
// src/components/HelloWorld.vue


<template>
<template>
   <section id="section_question" v-bind:class="section.visability('question')">
   <input :value="label" :type="type" @click="click"/>
    <h1>{{greeting.helloStranger}}</h1>
    <form>
      <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>
</template>
</source>


== Stores als Klassen ==
<style scoped lang="scss">
 
  @import '@/css/form/Button.scss'
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.
</style>
 
=== 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>
</source>


=== Definition der Store-Klasse StoreSection ===
Das zugehörige SCSS-Modul importiert lediglich ein SCSS-Modul für Formulare.
 
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 lang="css">// src/css/form/Button.scss
@import './_Form.scss'
</source>
</source>


=== Definition der Store-Klasse StoreGreeting ===
Das Formular-SCSS-Modul definiert Eigenschaften, die sich mehrere Formular-Elemente teilen.
 
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)
<source lang="css">
  { return this.state.hello.replace('$1', p_name); }
// src/css/form/_Form.scss
@import 'config';


  get helloStranger()
label
  { return computed(() => this.#sayHello(this.state.stranger)).value }
{ font-family: $font-family-serif;
 
   font-size:   $font-size-p !important;
   get hello()
   { return computed(() => this.#sayHello(this.state.name !== ''
                                        ? this.state.name
                                        : this.state.stranger
                                        )
                  ).value
  }
}
}


const stateGreeting = new StoreGreeting()
label, input
 
{ width:      10em;
export default stateGreeting
   font-size: 100%;
</source>
  display:   inline-block;
 
  box-sizing: border-box;
Störend ist hier, dass man für jedes Attribut wie {{zB}} <code>name</code> eine Getter- und eine Setter-Methode definieren muss.
  margin:    0.5ex;
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
label
{ text-align: right; }
</source>
</source>


=== Verwendung der alternativen Stores in einer Komponente ===
=== <code>Textfield.vue</code> ===


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


<script setup >
<script setup>
   import { onMounted, onUnmounted } from 'vue'
   import { computed, onMounted, onUnmounted } from 'vue'
   import ControllerKey from '@/controller/ControllerKey'
   import { v4 as uuid } from 'uuid'
   import section from '@/store/StoreSectionClass'
   import ControllerKey from '@/controller/ControllerKey'
//import greeting from '@/store/StoreGreetingClass'
  import greeting from '@/store/StoreGreetingClassExtendible'


   const
   const
     sayHello = () => { section.change('hello') },
     props        = defineProps
     controllerKey =
                    ({ label:    { type: String,  default: '' },
      new ControllerKey('Enter', sayHello, 'input_name')
                      text:      { type: String,  default: '' },
 
                      autofocus: { type: Boolean, default: false }
  section.init({ name: 'question'})
                    }),
//greeting.init({ hello: 'Hallo, $1!', stranger: 'Fremder'})
     id            = uuid(),
    emit          = defineEmits(['update:text', 'enter']),
    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())
   onMounted  (() => controllerKey.add())
   onUnmounted(() => controllerKey.remove())
   onUnmounted(() => controllerKey.remove())
</script>
</script>
<template>
  <div>
    <label :for="id" v-if="label !== ''">{{label}}</label>
    <input :id="id" type="text" v-model="text" :autofocus="autofocus" />
  </div>
</template>
<style scoped lang="scss">
  @import '@/css/form/Textfield.scss'
</style>
</source>
</source>


Zum Vergleich: Die Verwendung der zuerst definierten Stores.
Mit Hilfe von "computed" wird für die Property "text" bidirektionales Binding ermöglicht.
<source lang="ecmascript">
// src/components/HelloWorld.vue


<script setup>
Die Getterfunktion liest den Wert aus der Property des Aufrufers. Die Setterfunktion
  import { onMounted, onUnmounted } from 'vue'
erzeugt bei jeder Änderung ein passendes Ereignis. Der Name des Ereignisse muss laut Vue-Konvention
  import ControllerKey from '@/controller/ControllerKey'
<code>'update:<Propertyname>'</code> lauten.
  import storeSection  from '@/store/StoreSection'
 
  import storeGreeting from '@/store/StoreGreeting'
Wie zuvor wird das Formular-SCSS-Modul verwendet.


<source lang="css">
@import './_Form.scss'
</source>


  const
Die in diesem SCSS-Modul enthaltenen Inhalte können, sobald die Modularisierung abgeschlossen ist,
    section  = storeSection(),
aus der Datei <code>body.scss</code> gelöscht werden.
    greeting = storeGreeting(),


    sayHello = () => { section.change('hello') },
== Erstellen der Hello-World-Section-Komponenten ==
    controllerKey =
      new ControllerKey('Enter', sayHello, 'input_name')


  section.init('question')
=== <code>SectionQuestion.vue</code> ===
//greeting.init({ hello: 'Hallo, $1!', stranger: 'Fremder'})


  onMounted  (() => controllerKey.add())
=== <code>SectionHello.vue</code> ===
  onUnmounted(() => controllerKey.remove())
</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==

Version vom 18. April 2023, 10:58 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

Vorlage:InBearbeitung

Musterlösung
Git-Repository, git checkout v04 (JavaScript)
Git-Repository (TypeScript), git checkout v04

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:

  • components/section/SectionHello.vue
  • components/section/SectionQuestion.vue

Zuvor sollten Sie zwei wiederverwendbare Hilfskomponenten definieren:

  • components/form/Button.vue
  • components/form/Textfield.vue

Erstellen eines neuen Projektzweigs

Erstellen Sie einen neuen Projektzweig (branch) innerhalb von hello_world_vue und fügen Sie das Package uuid hinzu:

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

Erstellen der Hilfskomponenten

Button.vue

Eine parametrisierbare Button-Komponente.

// src/components/form/Button.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="type" @click="click"/>
</template>

<style scoped lang="scss">
  @import '@/css/form/Button.scss'
</style>

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

// src/css/form/Button.scss
@import './_Form.scss'

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

// src/css/form/_Form.scss
@import 'config';

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

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

label
{ text-align: right; }

Textfield.vue

Eine parametrisierbare Textfield-Komponente zum Erfassen von Texteingaben.

// src/components/form/Textfield.vue

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

  const
    props         = defineProps
                    ({ label:     { type: String,  default: '' },
                       text:      { type: String,  default: '' },
                       autofocus: { type: Boolean, default: false }
                    }),
    id            = uuid(),
    emit          = defineEmits(['update:text', 'enter']),
    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())
  onUnmounted(() => controllerKey.remove())
</script>

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

<style scoped lang="scss">
  @import '@/css/form/Textfield.scss'
</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-SCSS-Modul verwendet.

@import './_Form.scss'

Die in diesem SCSS-Modul enthaltenen Inhalte können, sobald die Modularisierung abgeschlossen ist, aus der Datei body.scss gelöscht werden.

Erstellen der Hello-World-Section-Komponenten

SectionQuestion.vue

SectionHello.vue

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 (MMProg): Wolfgang Kowarschick; Vorlesung „Multimedia-Programmierung“; Hochschule: Hochschule Augsburg; Adresse: Augsburg; Web-Link; 2018; Quellengüte: 3 (Vorlesung)