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

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg
Kowa (Diskussion | Beiträge)
Kowa (Diskussion | Beiträge)
Keine Bearbeitungszusammenfassung
 
(94 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 2: Zeile 2:


'''Musterlösung'''<br/>
'''Musterlösung'''<br/>
[{{Git-Server}}/kowa/wk_hello_world_vue Git-Repository], <code>git checkout v01</code> (JavaScript)<!--<br>
v01:&nbsp;&nbsp;&nbsp;[https://glossar.hs-augsburg.de/beispiel/tutorium/2025/wk_hello_world/wk_hello_world_vue/web_v01/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_01%2Fweb_v01%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_01%2Fweb_v01%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_01%2Fweb_v01%2Findex.html Google PageSpeed])<br/>
[{{Git-Server}}/kowa/wk_hello_world_vue_ts Git-Repository (TypeScript, )], <code>git checkout v01</code>-->
v01a: [https://glossar.hs-augsburg.de/beispiel/tutorium/2025/wk_hello_world/wk_hello_world_vue/web_v01a/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_01%2Fweb_v01a%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_01%2Fweb_v01a%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_01%2Fweb_v01a%2Findex.html Google PageSpeed])<br/>
 
v01b: [https://glossar.hs-augsburg.de/beispiel/tutorium/2025/wk_hello_world/wk_hello_world_vue/web_v01b/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_01%2Fweb_v01b%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_01%2Fweb_v01b%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_01%2Fweb_v01b%2Findex.html Google PageSpeed])<br/>
v01c: [https://glossar.hs-augsburg.de/beispiel/tutorium/2025/wk_hello_world/wk_hello_world_vue/web_v01c/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_01%2Fweb_v01c%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_01%2Fweb_v01c%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_01%2Fweb_v01c%2Findex.html Google PageSpeed])<br/><br/>
[{{Git-Server}}/kowa/wk_hello_world_vue Git-Repository]:<br/>
<code>git checkout v01</code>, <code>git checkout v01a</code>, <code>git checkout v01b</code>, <code>git checkout v01c</code>
== Anwendungsfälle (Use Cases) ==
== Anwendungsfälle (Use Cases) ==
Gegenüber dem [[HTML5-Tutorium:_JavaScript:_Hello_World_06|dritten, vierten, fünften und sechsten Teil des Tutoriums]]
Gegenüber dem [[HTML5-Tutorium:_JavaScript:_Hello_World_06|dritten, vierten, fünften und sechsten Teil des Tutoriums]]
Zeile 17: Zeile 20:
Erstellen Sie ein neues Projekt <code>hello_world_vue</code>:
Erstellen Sie ein neues Projekt <code>hello_world_vue</code>:


<source lang="bash">
<syntaxhighlight lang="bash">
cd <somewhere>
cd <somewhere>


npm init vue@latest
npm init vue@latest
  Ok to proceed? (y)
   Project name: » hello_world_vue
   Project name: » hello_world_vue
   TypeScript? » No (Yes mittels Space-Taste)
   > Nun können Sie ESLint und Prettier auswählen.
   ...
   > Die Module Pinia und Router behandeln wir später.
  ESLint? » Yes
  > Für die Praktikumsaufgaben sollten Sie TypeScript nicht aktivieren.
  ...
</syntaxhighlight>
 
Nun können Sie das Prorjekt in VSC öffnen und dort in einem zugehörigen Terminal weiterarbeiten.


cd hello_world_vue
<syntaxhighlight lang="bash">
npm install
cd hello_world_vue # In VSC nicht mehr notwendig.
npm i
npm run dev  # Im Browser: http://localhost:5173/
npm run dev  # Im Browser: http://localhost:5173/


Zeile 54: Zeile 59:
git checkout -b v00
git checkout -b v00
git push --set-upstream origin v00
git push --set-upstream origin v00
</source>
</syntaxhighlight>


=== Anmerkungen ===
=== Anmerkungen ===
Vue ist ein Framework zum Erstellen von Single-Page-Web-Anwendungen.
Vue ist ein Framework zum Erstellen von Single-Page-Web-Anwendungen.
Allerdings gibt es eine Router-Erweiterung: Multi-Page-Web-Anwendungen.
Es ist allerdings möglich, mehrere Web-Seiten unter die Kontrolle von Vite zu stellen.
 
Beispiel: [https://gitlab.multimedia.hs-augsburg.de/kowa/wk_ecmascript_01 wk_ecmascript_01]
 
'''Achtung:''' Bauen Sie diesen Code '''NICHT''' in Ihre vue-Anwendung ein.
Diese basiert als Single-PAge-Anwendung vollständig auf der Datei <code>index.html</code>.
 
<syntaxhighlight lang="javascript">
// vite.config.js
 
...
  build:
  { ...
    rollupOptions:
    { input:
      { main:    resolve(__dirname, './src/index.html'),
        solution: resolve(__dirname, './src/index_solution.html')
      }
    }
  },
...
</syntaxhighlight>


Vue basierte bis Version 3.0 auf Webpack.
Vue basierte bis Version 3.0 auf Webpack.
Webpack gilt allerdings schon wieder als Dinosaurier (existiert seit 2012)
Webpack gilt allerdings schon wieder als Dinosaurier (existiert seit 2012).


Seit Version 3.2 verwendet Vue  Vite (französisch „schnell“).
Seit Version 3.2 verwendet Vue  Vite (französisch „schnell“).
Zeile 68: Zeile 94:


Vue 3 unterstützt im Gegensatz zu Vue 2 TypeScript.
Vue 3 unterstützt im Gegensatz zu Vue 2 TypeScript.
Allerdings wir im Tutorium Typescript nicht eingesetzt.
Allerdings wird im Tutorium Typescript nicht eingesetzt.


Vue-Anwendungen sind aus Komponenten aufgebaut.
Vue-Anwendungen sind aus Komponenten aufgebaut.
Das passt sehr gut [https://atomicdesign.bradfrost.com/chapter-2/ Atomic-Design-Prinzip].
Das passt sehr gut zum [https://atomicdesign.bradfrost.com/chapter-2/ Atomic-Design-Prinzip].


== Überführung von <code>hello-world-06</code> nach vue ==
== Überführung von <code>hello-world-06</code> nach vue ==


<source lang="bash">
<syntaxhighlight lang="bash">
git checkout -b v01  # neuen Git-Zweig (branch) anlegen
git checkout -b v01  # neuen Git-Zweig (branch) anlegen


Zeile 83: Zeile 109:
vite.config.js        # mit vite.config.js von hello_world_06 mergen
vite.config.js        # mit vite.config.js von hello_world_06 mergen
                       # Ausnahmen: root, publicDir, und outDir
                       # Ausnahmen: root, publicDir, und outDir
index.html            # gemäß hello_world_06 definieren; Import von 'main.js' anpassen (src="src/main.js")
index.html            # gemäß hello_world_06 definieren; Import von 'main.js' anpassen:
                       # Der Inhalt des Body-Elements wird ersetzt: <main id="app"></main>
                      # * <code>@import url("/css/head.css");</code>
src/css              # Ordner von hello_world_06 übernehmen
                      # * <code>src="src/main.js"</code>)
                       # * Der Inhalt des Body-Elements wird ersetzt: <main id="app"></main>
src/css              # Ordner <code>src/css</code> von hello_world_06 übernehmen
public                # favicon ersetzen
public                # favicon ersetzen
README.md            # ansehen und verbessern
README.md            # ansehen und verbessern


npm run dev          # Vite-Server neu starten
npm run dev          # Vite-Server neu starten
</source>
</syntaxhighlight>


'''Tipp'''<br/>
'''Tipp'''<br/>
Wenn Sie Code von mir verwenden, sollten Sie in die Datei
Passen Sie die Datei <code>eslint.config.js</code> an.
<code>eslintrc.cjs</code> nach Zeile 10 folgenden Code einfügen:
Wenn Sie Code von mir verwenden, benötigen Sie die Regel <code>'no-unexpected-multiline': 0</code>.
<source lang="javascript">
Sonst bekommen Sie bei Code von mir regelmäßig Fehlermeldungen.
   ignorePatterns:
 
   [ 'dist', 'public'],  
<syntaxhighlight lang="javascript">
   rules:  
import js              from '@eslint/js'
  { 'no-unexpected-multiline': 0 }
import pluginVue        from 'eslint-plugin-vue'
</source>
import globals          from 'globals'
import { defineConfig } from "eslint/config";
 
export default defineConfig([
   { name: 'app/files-to-lint',
    files: [ '**/*.{js,mjs,jsx,vue}' ],
   },
 
  { name: 'app/files-to-ignore',
    ignores: ['**/node_modules/**', '**/dist/**', '**/public/**', '**/dist-ssr/**', '**/coverage/**'],
   },
 
  { languageOptions:
    { globals:
      { ...globals.browser,
      },
    },
  },
 
  js.configs.recommended,
  ...pluginVue.configs['flat/essential'],
 
  { rules:
    { 'no-unexpected-multiline': 0,
    },
  },
])
</syntaxhighlight>


'''Relative Pfade'''<br/>
'''Relative Pfade'''<br/>
Um relative Pfade beim Building zu erzwingen, reicht es nicht, die Option <code>base: ''</code> in vite.config.js einzufügen. Sie müssen dies zusätzlich in vue.config.js festlegen:
Um relative Pfade beim Building zu erzwingen, reicht es nicht, die Option <code>base: '<nowiki>'</nowiki></code> in vite.config.js einzufügen. Sie müssen dies zusätzlich in vue.config.js festlegen:
<source lang="javascript">
<syntaxhighlight lang="javascript">
import { defineConfig } from '@vue/cli-service'
export default
 
{ publicPath: '' }
export default defineConfig ({ publicPath: '' })
</syntaxhighlight>
</source>


Passen Sie auch noch die Datei <code>jsconfig.json</code> an, damit in Visual Studio Code die Pfadverfolgung mittels Point and Click ermöglicht wird.  
Passen Sie auch noch die Datei <code>jsconfig.json</code> an, damit in Visual Studio Code die Pfadverfolgung mittels Point and Click ermöglicht wird.  
Leider funktioniert dies nicht für den Alias <code>config</code>.
Leider funktioniert dies nicht für den Alias <code>config</code>.


<source lang="json">
<syntaxhighlight lang="json">
{ "compilerOptions":  
{ "compilerOptions":  
   {  "baseUrl": ".",
   {  "baseUrl": ".",
     "paths":  
     "paths":  
       {  "@/*":    ["./src/*"],
       {  "@/*":          ["./src/*"],
         "/css/*": ["./src/css/*.scss"]
        "config.css":    ["./src/css/_config.css"],
         "/css/*":       ["./src/css/*.css"],
        "/js/*":        ["./src/js/*"],
        "/controller/*": ["./src/controller/*"],
        "/service/*":    ["./src/service/*"],
        "/component/*":  ["./src/component/*"],
        "/store/*":      ["./src/store/*"]
       }
       }
   },
   },
Zeile 124: Zeile 184:
   "extensions": [".js", ".vue", ".json"]
   "extensions": [".js", ".vue", ".json"]
}
}
</source>
</syntaxhighlight>


=== <code>src/main.js</code> ===
=== <code>src/main.js</code> ===
Zeile 132: Zeile 192:
und gemounted werden, wenn alle Dateien der App vollständig geladen wurden.
und gemounted werden, wenn alle Dateien der App vollständig geladen wurden.


<source lang="ecmascript">
<syntaxhighlight lang="ecmascript">
import { createApp } from 'vue'
import { createApp } from 'vue'
import App from './App.vue'
import App           from './App.vue'


window.addEventListener
window.addEventListener
Zeile 140: Zeile 200:
   () => createApp(App).mount('#app')
   () => createApp(App).mount('#app')
)
)
</source>
</syntaxhighlight>


=== <code>index.html</code> ===
=== <code>index.html</code> ===
Zeile 148: Zeile 208:
Wenn Sie eine Datei <code>favicon.ico</code> zur Hand haben, können Sie die Vite-Version dieser Datei ersetzen.
Wenn Sie eine Datei <code>favicon.ico</code> zur Hand haben, können Sie die Vite-Version dieser Datei ersetzen.


<source lang="html">
<syntaxhighlight lang="html">
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en">
<html lang="en">
Zeile 172: Zeile 232:


</html>
</html>
</source>
</syntaxhighlight>


=== <code>src/App.vue</code> ===
=== <code>src/App.vue</code> ===
* Die Hello-World-Komponente und das Style-Element laden.
* Die Hello-World-Komponente und das Style-Element laden.
* Nur die Hello-World-Komponente ins Template einfügen.
* Nur die Hello-World-Komponente ins Template einfügen.
* <code>body.scss</code> importieren (ohne das Attribut <code>scoped</code>, damit der Inhalt der CSS-Datei allen Komponenten zur Verfügung steht).
* <code>body.css</code> importieren (ohne das Attribut <code>scoped</code>, damit der Inhalt der CSS-Datei allen Komponenten zur Verfügung steht).


<source lang="html">
<syntaxhighlight lang="html">
<script setup>
<script setup>
   import HelloWorld from './components/HelloWorld.vue'
   import HelloWorld from './component/HelloWorld.vue'
</script>
</script>


Zeile 188: Zeile 248:
</template>
</template>


<style lang="scss">
<style lang="css">
   @import '/css/body.css';
   @import '/css/body.css';
</style>
</style>
</source>
</syntaxhighlight>


Der Ordner <code>src/assets</code> wird nun nicht mehr benötigt und kann gelöscht werden ebenso wie die anderen beiden Komponenten.
Der Ordner <code>src/assets</code> wird nun nicht mehr benötigt und kann gelöscht werden ebenso wie die anderen beiden Komponenten.


=== <code>src/components/HelloWorld.vue</code> ===  
=== <code>src/component/HelloWorld.vue</code> ===  
* Sections aus index.html in Template-Element einfügen.
* Sectionspezifische CSS-Anweisungen in <code>src/css/HelloWorld.css</code> verschieben und diese Datei in <code>src/components/HelloWorld.vue</code> importieren (<code>@import '@/css/components/HelloWorld';</code> ).
* Skript-Element befüllen.


'''CSS für die Hello-World-Komponente'''<br/>
In die HelloWorld-Komponente werden die beiden Sections aus der Hello-World-06-App eingefügt.
<source lang="css">
/* src/css/components/HelloWorld.css */


/* Hier wird alles aus der Datei body.css übernommen,
'''Sections aus index.html in Template-Element einfügen.'''<br />
* bis auf html {...} und body {...}.
<syntaxhighlight lang="html">
*/
<!-- src/component/HelloWorld.vue -->
</source>
 
'''Hello-World-Komponente'''<br/>
<source lang="javascript">
<!-- src/components/HelloWorld.vue -->
 
<script setup>
  import { ref, computed } from 'vue'


  const
    section      = ref('from'),
    helloStranger = ref('Hello, Stranger'),
    name          = ref(''),
    hello      = computed(() => `Hello, ${name.value}!`),
    visibility = p_section =>
                p_section !== section.value ? 'hidden' : '',
    gotToHello = () => section.value = 'hello',
    autofocus  = () => { document.getElementById('input_name').value = '';
                        document.getElementById('input_name').focus();
                      }
</script>
...
<style scoped lang="scss">
  @import '@/css/components/HelloWorld';
</style>
</source>
Im Template von HelloWorld.vue sollten Sie alle Attribute <code>"id"</code> mit Ausnahme von <code>"section_from"</code>, <code>"section_hello"</code> und <code>"input_name"</code> löschen.
Der Grund ist, dass ID-Attribute im HTML-Dokument eindeutig sein müssen. Wenn in einer Komponente ein ID-Attribut definiert wird, kann man diese nicht mehrfach in eine andere Komponente einfügen.
Ein Ausnahme bildet das Attribut <code>"input_name"</code>, da dieser Identifikator von zugehörigen laben referenziert wird: <code><label for="input_name"></code>. Bei der Erstellung einer eigenständigen Komponente Textfield muss man daher darauf achten, dass bei jeder Verwendung dieser Komponente ein neuer eindeutiger Identifikator (z. B. mittels <code>uuid</code>) erzeugt wird.  (Siehe [[Version HTML5-Tutorium: JavaScript: Hello World Vue 04|Version v04]].)
Außerdem benötig man diese ID in der [[Version HTML5-Tutorium: JavaScript: Hello World Vue 02|Version v02]] für den Enter-Key-Controller.
Darüber hinaus benötigen wir die Section-IDs für CSS. Diese werden erst später gelöscht.
<source lang="html">
<template>
<template>
   <section id="section_form" v-bind:class="visibility('form')">
   <section id="section_form" class="hidden">
     <h1>{{helloStranger}}</h1>
     <h1>Hello, Stranger!</h1>
     <form>
     <form>
       <div>
       <div>
         <label for="input_name">What's your name?</label>
         <label for="input_name">What's your name?</label>
         <input id="input_name" type="text" v-model="name" autofocus>
         <input id="input_name" autofocus>
       </div>
       </div>
       <div>
       <div>
         <input type="reset"  value="Reset">
         <input id="button_reset"  type="reset"  value="Reset">
         <input type="button" value="Say hello" v-on:click="sayHello">
         <input id="button_submit" type="button" value="Say hello">
       </div>
       </div>
     </form>
     </form>
   </section>
   </section>
   <section id="section_hello" v-bind:class="visibility('hello')">
   <section id="section_hello" class="hidden">
     <h1>{{hello}}</h1>
     <h1 id="heading_hello">Hello, ...!</h1>
     <p>Welcome to Web Programming!</p>
     <p>Welcome to Full-Stack Web Development!</p>
   </section>
   </section>
</template>
</template>
</syntaxhighlight>


...
Sectionspezifische CSS-Anweisungen aus der Hello-World-06-App werden in die Datei <code>src/css/HelloWorld.css</code> verschoben.  
</source>
Diese Datei wird in der Komponetne in <code>src/component/HelloWorld.vue</code> als Style importiert (<code>import '@/css/component/HelloWorld';</code> ).
 
'''CSS für die Hello-World-Komponente'''<br/>
<syntaxhighlight lang="css">
/* src/css/component/HelloWorld.css */
 
/* Hier wird alles aus der Datei body.css übernommen,
* bis auf html {...} und body {...}.
* Die Config-Datei wird in beiden CSS-Dateien importiert.
*/
</syntaxhighlight>


=== <code>src/components/HelloWorld.vue</code> ===
Anschließend wird diese CSS-Datei in der Datei <code>HelloWorld.vue</code> importiert.
* Sections aus index.html in Template-Element einfügen.
Mittels <code>scoped</code> wird angegeben, dass die CSS-Anweisungen nur Auswirkungen auf den HTML-Code
* Sectionspezifische SCSS-Anweisungen in <code>src/css/HelloWorld.scss</code> verschieben und diese Datei in <code>src/components/HelloWorld.vue</code> importieren (<code>@import '@/css/components/HelloWorld';</code> ).
innerhalb der Datei <code>HelloWorld.vue</code> hat. Beachten Sie: In der Datei <code>App.vue</code> wurde bewusst
* Skript-Element befüllen.
auf <code>scoped</code> verzichtet. Das hat zu Folge, dass die dort importierten CSS-Anweisungen auf den gesamten HTML-Code
der App auswirkungen haben.  


'''SCSS für die Hello-World-Komponente'''<br/>
<syntaxhighlight lang="html">
<source lang="css">
<!-- src/component/HelloWorld.vue -->
/* src/css/components/HelloWorld.scss */


#section_form
<style scoped lang="css">
{ text-align:   center;
   @import '/css/component/HelloWorld.css';
  margin-left:  auto;
</style>
  margin-right: auto;
</syntaxhighlight>


  h1
Die wesentlichen Hello-World-Attribute und -Funktionen aus den JavaScript-Dateien der Hello-World-06-App werden in den Script-Bereich eingefügt.
  { margin-bottom: 0.5ex;
    margin-top:    auto;
  }
}


#section_hello
Diese Elemente werden mittel <code>ref</code> und </code>computed</code> im nachfolgenden Schritt bidirektional mit den HTML-Elementen im Template-Element verknüpft.
{ p
  { font-size: $font-size-p-large !important; }
}
</source>


'''Hello-World-Komponente'''<br/>
'''Script für die HelloWorld-Komponenten'''<br/>
<source lang="javascript">
<syntaxhighlight lang="javascript">
<!-- src/components/HelloWorld.vue -->
<!-- src/component/HelloWorld.vue -->


<script setup>
<script setup>
Zeile 309: Zeile 327:
     name          = ref(''),
     name          = ref(''),


     hello     = computed(() => `Hello, ${name.value}!`),
     hello   = computed(() => name.value === '' ? helloStranger.value : `Hello, ${name.value}!`),
    visibility = p_section =>
     sayHello = () => section.value = 'hello'
                p_section !== section.value ? 'hidden' : '',
     sayHello   = () => section.value = 'hello'
</script>
</script>
</syntaxhighlight>


...
Im Template von HelloWorld.vue sollten Sie alle Attribute <code>"id"</code> mit Ausnahme von <code>"input_name"</code> löschen.
 
<style scoped lang="scss">
  @import '@/css/components/HelloWorld';
</style>
 
</source>


Im Template von HelloWorld.vue sollten Sie alle Attribute <code>"id"</code> mit Ausnahme von <code>"section_form"</code>, <code>"section_hello"</code> und <code>"input_name"</code> löschen.
Der Grund ist, dass ID-Attribute im HTML-Dokument eindeutig sein müssen. Wenn in einer Komponente ein ID-Attribut definiert wird, kann man diese nicht mehrfach in eine andere Komponente einfügen. Man sollte in Komponenten, die mehrfach verwendet werden sollte, keine
fest definierten IDs einfügen.


Der Grund ist, dass ID-Attribute im HTML-Dokument eindeutig sein müssen. Wenn in einer Komponente ein ID-Attribut definiert wird, kann man diese nicht mehrfach in eine andere Komponente einfügen.
Ein Ausnahme bildet zunächst noch das Attribut <code>"input_name"</code>, da dieser Identifikator vom zugehörigen Label referenziert wird: <code><label for="input_name"></code>.  


Ein Ausnahme bildet das Attribut <code>"input_name"</code>, da dieser Identifikator von zugehörigen laben referenziert wird: <code><label for="input_name"></code>. Bei der Erstellung einer eigenständigen Komponente Textfield muss man daher darauf achten, dass bei jeder Verwendung dieser Komponente ein neuer eindeutiger Identifikator (z. B. mittels <code>uuid</code>) erzeugt wird.  (Siehe [[Version HTML5-Tutorium: JavaScript: Hello World Vue 04|Version v04]].)
Verknüpfen Sie nun das Template bidirectional mit den Script-Elementen:


Außerdem benötig man diese ID in der [[Version HTML5-Tutorium: JavaScript: Hello World Vue 02|Version v02]] für den Enter-Key-Controller.
<ul>
<li><code>v-if="section==='form'"</code></li>
<li><code>v-if="section==='hello'"</code></li>
<li><code>v-model="name"</code></li>
<li><code>v-on:click="sayHello"</code></li>
<li><code><nowiki>{{</nowiki>helloStranger}}</code></li>
<li><code><nowiki>{{</nowiki>hello}}</code></li>
</ul>


Darüber hinaus benötigen wir die Section-IDs für CSS. Diese werden erst später gelöscht.
<syntaxhighlight lang="html">
<source lang="html">
<template>
<template>
   <section id="section_form" v-bind:class="visibility('form')">
   <section v-if="section==='form'">
     <h1>{{helloStranger}}</h1>
     <h1>{{helloStranger}}</h1>
     <form>
     <form>
       <div>
       <div>
         <label for="input_name">What's your name?</label>
         <label for="input_name">What's your name?</label>
         <input id="input_name" type="text" v-model="name" />
         <input id="input_name" autofocus v-model="name" >
       </div>
       </div>
       <div>
       <div>
         <input type="reset"  value="Reset"/>
         <input type="reset"  value="Reset">
         <input type="button" value="Say hello" v-on:click="sayHello"/>
         <input type="button" value="Say hello" v-on:click="sayHello">
       </div>
       </div>
     </form>
     </form>
   </section>
   </section>
   <section id="section_hello" v-bind:class="visibility('hello')">
   <section v-if="section==='hello'">
     <h1>{{hello}}</h1>
     <h1>{{hello}}</h1>
     <p>Welcome to Web Programming!</p>
     <p>Welcome to Full-Stack Web Development!</p>
   </section>
   </section>
</template>
</template>
 
</syntaxhighlight>
...
</source>


'''Autofokus-Aktivierung per JavaScript'''<br/>
'''Autofokus-Aktivierung per JavaScript'''<br/>
Wir können auch noch den Autofokus-Mechanismus von Hello-World-06 nachbilden.  
Wir können auch noch den Autofokus-Mechanismus von Hello-World-06 nachbilden.  
Dazu verwenden wird zwei Vue-Observerfunktionen <code>onMounted</code> und <code>onUnmounted</code>.
Dazu verwenden wird zwei Vue-Observerfunktionen <code>onMounted</code> und <code>onUnmounted</code>.
Zeile 363: Zeile 378:
„Komponente wurde aus dem DOM-Baum entfernt“ eintritt. Wenn die Kompontente eingefügt wird, werden
„Komponente wurde aus dem DOM-Baum entfernt“ eintritt. Wenn die Kompontente eingefügt wird, werden
die bekannten Autofokus-Eventlistener registriert und der Autofokus aktiviert.  
die bekannten Autofokus-Eventlistener registriert und der Autofokus aktiviert.  
Wenn die Komponenten entfernt wird, werden die Autofokus-Eventlistener leöscht, da evtl.  
Wenn die Komponenten entfernt wird, werden die Autofokus-Eventlistener gelöscht, da evtl.  
andere Komponenten einen anderen Autofokus aktivieren möchten.
andere Komponenten einen anderen Autofokus aktivieren möchten.


<source lang="javascript">
<syntaxhighlight lang="javascript">
<script setup>
<script setup>
   import { ref, computed, onMounted, onUnmounted } from 'vue'
   import { ref, computed, onMounted, onUnmounted } from 'vue'
Zeile 375: Zeile 390:
     name          = ref(''),
     name          = ref(''),


     hello     = computed(() => `Hello, ${name.value}!`),
     hello     = computed(() => name.value === '' ? helloStranger.value : `Hello, ${name.value}!`),
     visibility = p_section =>
     sayHello  = () => section.value = 'hello',
                p_section !== section.value ? 'hidden' : '',
     autofocus = () => document.getElementById('input_name').focus(),
     sayHello  = () => section.value = 'hello',
     visOn    = () => window.addEventListener('visibilitychange', autofocus),
     autofocus  = () => document.getElementById('input_name').focus()
    visOff    = () => window.removeEventListener('visibilitychange', autofocus)


   onMounted
   onMounted (visOn);
  ( () =>
   onUnmounted(visOff);
    { window.addEventListener('focus', autofocus);
    //document.getElementById('button_reset').addEventListener('click', autofocus);
      autofocus();
    }
  )
 
   onUnmounted
  ( () =>
    { window.removeEventListener('visibilitychange', autofocus);
    //document.getElementById('button_reset').removeEventListener('click', autofocus); 
    }
  )
</script>
</script>
</source>
</syntaxhighlight>


Darüber hinaus wird <code>autofocus</code> bei der Aktivierung des Resetbuttons ausgeführt.  
Darüber hinaus wird <code>autofocus</code> bei der Aktivierung des Resetbuttons ausgeführt.  
Damit wird der Fokus bei einem Rest auf das Eingaefeld gesetzt. Da der Event automatisch an den Default-Handler weitergeleitet wird, funktioniert die eigentliche Reset-Funktion (löschen der Feldinhalte) weiterhin.
Damit wird der Fokus bei einem Rest auf das Eingaefeld gesetzt. Da der Event automatisch an den Default-Handler weitergeleitet wird, funktioniert die eigentliche Reset-Funktion (löschen der Feldinhalte) weiterhin.
<source lang="html">
<syntaxhighlight lang="html">
<template>
<template>
...
...
Zeile 408: Zeile 411:
       </div>
       </div>
...
...
</syntaxhighlight>
== Mounting in Vue ==
Man beachte, dass <code>onMounted</code> und <code>onUnmounted</code> Callback-Funktionen als Eingabeparameter erwarten. Diese werden ausgeführt, sobald die entsprechenden Ereignisse von Vue signalisiert werden.
Es gibt noch diverse weitere Eventhandler für verschiedene Vue-Ereignisse.
'''Komponenten-Zustände'''<br/>
Vue-Komponenten durchlaufen verschiedene Zustände. Bei jedem Zustandswechsel wird ein Event ausgelöst:
* <code>beforeCreate</code>:        Zustand, bevor eine Komponente initialisiert wird
* <code>created</code>:              Zustand, nachdem eine Komponente vollständig initialisiert wurde
* <code>beforeMount</code>:          Zustand, bevor eine Komponente in den DOM-Baum des Browsers eingefügt wird
* <code>mounted</code>:              Zustand, nachdem eine Komponente in den DOM-Baum des Browsers eingefügt wurde
* <code>beforeUpdate</code>:        Zustand, bevor eine Komponente im DOM-Baum des Browsers modifiziert wird (wegen einer Änderung des zugehörigen Modells)
* <code>updated</code>:              Zustand, nachdem eine Komponente im DOM-Baum des Browsers modifiziert wurde
* <code>beforeUnmount</code>:        Zustand, bevor eine Komponente aus dem DOM-Baum des Browsers gelöscht wird
* <code>unmounted</code>:            Zustand, nachdem eine Komponente aus dem DOM-Baum des Browsers gelöscht wurde
In der Composition API und der Setup-API kann man sich für diese Ereignisse (mit Ausnahme der ersten beiden) als Observer mittels der Methoden
  <code>onBeforeMount</code>, <code>onMounted</code>, <code>onBeforeUpdate</code>, <code>onUpdated</code>, <code>onBeforeUnmount</code>, <code>onUnmounted</code>
registrieren. Daneben gibt es in der Composition API noch ein paar weitere Ereignisse, für die man sich als Observer registrieren kann.


== Testen ==
== Testen ==
Zeile 417: Zeile 442:
Im Folgenden sehen Sie drei Skript-Varianten (ohne Behandlung von Autofocus). Bitte benutzen Sie in Ihrem Prhekte die erste Variante (<code><script setup</code>).
Im Folgenden sehen Sie drei Skript-Varianten (ohne Behandlung von Autofocus). Bitte benutzen Sie in Ihrem Prhekte die erste Variante (<code><script setup</code>).


=== <code><script setup></code> (Vue 3.2, Vue 3.4)</code> ===
=== <code><script setup></code> (Vue 3.2, Vue 3.4, Vue 3.5)</code> ===
<source lang="javascript">
<syntaxhighlight lang="javascript">
<script setup>
<script setup>
   import { ref, computed } from 'vue'
   import { ref, computed, onMounted, onUnmounted } from 'vue'


   const
   const
Zeile 427: Zeile 452:
     name          = ref(''),
     name          = ref(''),


     hello     = computed(() => `Hello, ${name.value}!`),
     hello     = computed(() => name.value === '' ? helloStranger.value : `Hello, ${name.value}!`),
     visibility = p_section =>
     sayHello  = () => section.value = 'hello',
                p_section !== section.value ? 'hidden' : '',
    autofocus = () => document.getElementById('input_name').focus(),
     sayHello  = () => section.value = 'hello'
    visOn    = () => window.addEventListener('visibilitychange', autofocus),
     visOff    = () => window.removeEventListener('visibilitychange', autofocus)
 
  onMounted  (visOn);
  onUnmounted(visOff);
</script>
</script>
</source>
</syntaxhighlight>
 
Musterlösung [https://gitlab.multimedia.hs-augsburg.de/kowa/wk_hello_world_vue/ wk_hello_world_vue], <code>git checkout v01a</code>


=== Composition API (Vue 3.0)</code> ===
=== Composition API (Vue 3.0)</code> ===
<source lang="javascript">
<syntaxhighlight lang="javascript">
<script>
<script>
   import { ref, computed } from 'vue'
   import { ref, computed, onMounted, onUnmounted } from 'vue'


   export default
   export default
Zeile 446: Zeile 477:
         name          = ref(''),
         name          = ref(''),


         hello     = computed(() => `Hello, ${name.value}!`),
         hello     = computed(() => name.value === '' ? helloStranger.value : `Hello, ${name.value}!`),
         visibility = p_section =>
         sayHello  = () => section.value = 'hello',
                    p_section !== section.value ? 'hidden' : '',
        autofocus = () => document.getElementById('input_name').focus(),
         sayHello  = () => section.value = 'hello'
        visOn    = () => window.addEventListener('visibilitychange', autofocus),
         visOff    = () => window.removeEventListener('visibilitychange', autofocus)
 
      onMounted  (visOn);
      onUnmounted(visOff);


       return {
       return {
         section, helloStranger, name,
         section, helloStranger, name,
         hello, visibility, sayHello
         hello, sayHello, autofocus
       }
       }
     }
     }
   }
   }
</script>
</script>
</source>
</syntaxhighlight>


Diese Variante kann ohne weitere Anpassungen an Stelle der Script-Setup-Variante verwendet werden.
Diese Variante kann ohne weitere Anpassungen an Stelle der Script-Setup-Variante verwendet werden.
Zeile 466: Zeile 501:


=== Options API (Vue 2.0)</code> ===
=== Options API (Vue 2.0)</code> ===
<source lang="javascript">
<syntaxhighlight lang="javascript">
<script>
<script>
   export default
   export default
Zeile 482: Zeile 517:
     computed:
     computed:
     { hello()
     { hello()
       { return `Hello, ${this.name}!` }
       { return this.name === '' ? this.helloStranger : `Hello, ${this.name}!` }
     },
     },


Zeile 489: Zeile 524:
       { this.section = 'hello' },
       { this.section = 'hello' },


       visibility(p_section)
       autofocus()
       { return this.section !== p_section ? 'hidden' : '' }
       { document.getElementById('input_name').focus() },
     }
    },
 
    mounted()
    { window.addEventListener('visibilitychange', this.autofocus).bind(this) },
 
     unmounted()
    { window.removeEventListener('visibilitychange', this.autofocus).bind(this) }
   }
   }
</script>
</script>
</source>
</syntaxhighlight>


Diese Variante kann ohne weitere Anpassungen an Stelle der Script-Setup-Variante verwendet werden.
Diese Variante kann ohne weitere Anpassungen an Stelle der Script-Setup-Variante verwendet werden.
Zeile 501: Zeile 542:
Musterlösung [https://gitlab.multimedia.hs-augsburg.de/kowa/wk_hello_world_vue/ wk_hello_world_vue], <code>git checkout v01c</code>
Musterlösung [https://gitlab.multimedia.hs-augsburg.de/kowa/wk_hello_world_vue/ wk_hello_world_vue], <code>git checkout v01c</code>


==Fortsetzung des Tutoriums==
==Entfernen der letzten ID==
 
Zurzeit existiert in der Komponente noch die ID <code>input_name</code>. In den älteren Vue-Versionen musste man eine zufällige ID erzeugen (z.B. milltel dem Paket <code>uuid</code>, um sicherzustellen, dass die Komponenten mehrfach verwendet werden kann, ohne dass diese ID mehrfach im HTML-Dokument vorkommt.
 
Seit Vue 3.5 gibt es für diese Augabe die Funktion <code>useId</code>.
 
<syntaxhighlight lang="javascript">
<script setup>
  import { useId, ref, computed, onMounted, onUnmounted } from 'vue'
 
  const
    inputId      = useId(),
    ...
...
</script>
</syntaxhighlight>
 
<syntaxhighlight lang="html">
<template>
...
  <div>
    <label :for="inputId">What's your name?</label>
    <input :id="inputId" v-model="name" autofocus>
  </div>
...
</template>
</syntaxhighlight>
 
Nun sollte es klappen, die Komponente zweimal in eine HTML-Seite einzubauen.
 
Musterlösung [https://gitlab.multimedia.hs-augsburg.de/kowa/wk_hello_world_vue/ wk_hello_world_vue], <code>git checkout v01</code>
 
== Properties ==
 
Wenn man den HTML-Code der App mit zwei Modulen <code>HelloWorld</code> untersucht, stellt man fest, dass die Property <code>autofocus</code> zweimal auf der HTML-Seite vorkommt, in jeder Komponenteninstanz einmal. Das ist laut HTML-Spezifikation nicht erlaubt.
 
Abhilfe schaffen die Template-Properties, die man mittels <code>defineProps</code> im Script-Bereich der Komponenten-Datei deklarieren kann.
 
<syntaxhighlight lang="javascript">
const
  props = defineProps ({ autofocus: { type: Boolean } }),
  ...
  autoFocus = () => { if (props.autofocus)
                        document.getElementById(inputId).focus()
                    },
  ...
</syntaxhighlight>
 
Nun kann man das Attribut dynamisch ins Template einbinden:
 
<syntaxhighlight lang="html">
<input :id="inputId" v-model="name" :autofocus>
</syntaxhighlight>
 
Ersetzen Sie außerdem in der gesamten Datei <code>HelloWorld.vue</code> den Strin <code>'input_name'</code>
durch die Konstante <code>inputId</code> (ohne Anführungsstriche!)
 
Fügen Sie folgenden Code in die Datei <code>App.vue</code> ein, um das Funktionieren der Property
<code>autofocus</code> zu überprüfen.
 
<syntaxhighlight lang="html">
<template>
  <HelloWorld autofocus />
  <HelloWorld />
</template>
</syntaxhighlight>
 
Perfekt ist diese Lösung noch nicht, da der Autofokus-Mechanischmus nur für das erste HelloWorld-Element korrekt funktioniert. Wenn ein Dokument aus mehreren verschiedenen Komponenten besteht und die Seite ein auf der Seite in einer Komponete enthlatenes Autofokus-Element unterstützen soll, benötigt man einen Store, in dem die ID dieses Elements für alle Komponenten zugänglich ist. Mit Stores befassen wir uns später.
 
== Vite für Development- und Produktionsmodus ==
 
Ein Problem gibt es bei der aktuellen Vue-Version nocht. Im Development-Modus integriert
Vue irgendwelche <code>__devtools__</code> in die Test-Anwendung. Die kommen mit dem Vite-Modul
<code>postcss-var-replace</code> nicht zurecht.
 
Als Lösung teilen wir die Datei <code>vite.config.js</code> in drei Dateien auf:
 
* <code>vite.config.common.js</code>: Enthält alles bis auf <code>postcss-var-replace</code>.
* <code>vite.config.js</code>: Importiert <code>vite.config.common.js</code> und exportiert das importierte Objekt. Hier könnten weitere Konfigurationsanweisungen stehen, die speziell für den Development-Modus von Bedeutung sind. Das werden wir in einem späteren Tutorium noch ausnutzen.
* <code>vite.config.build.js</code>: Importiert ebenfalls <code>vite.config.common.js</code>, bindet aber  <code>postcss-var-replace</code> vor dem Export in das Konfigurationsobjekt ein.
 
<syntaxhighlight lang="javascript">
//vite.config.js
 
import common from './vite.config.common'
 
export default common
</syntaxhighlight>
 
<syntaxhighlight lang="javascript">
//vite.config.build.js
 
import common                from './vite.config.common'
import { postcssVarReplace } from 'postcss-var-replace';
 
common.css.postcss.plugins = [ postcssVarReplace() ];
 
export default common
</syntaxhighlight>
 
Nun müssen wir in der Datei <code>package.json</code> das Build-Script noch anpasse, damit Vite die neue Konfigurationsdatei verwendet.
 
<syntaxhighlight lang="javascript">
"scripts": {
  "dev": "vite",
  "build": "vite build --config ./vite.config.build.js --emptyOutDir",
  "prepreview": "vite build --config ./vite.config.build.js --emptyOutDir",
  "preview": "vite preview",
  "lint": "eslint . --fix"
},
</syntaxhighlight>
 
== Fortsetzung des Tutoriums ==


Sie sollten nun [[HTML5-Tutorium: JavaScript: Hello World Vue 02|Teil 2 des Vue-Tutoriums]] bearbeiten.
Sie sollten nun [[HTML5-Tutorium: JavaScript: Hello World Vue 02|Teil 2 des Vue-Tutoriums]] bearbeiten.

Aktuelle Version vom 17. April 2025, 07:56 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

Musterlösung
v01:   index.html (HTML validate, CSS validate, Google PageSpeed)
v01a: index.html (HTML validate, CSS validate, Google PageSpeed)
v01b: index.html (HTML validate, CSS validate, Google PageSpeed)
v01c: index.html (HTML validate, CSS validate, Google PageSpeed)

Git-Repository:
git checkout v01, git checkout v01a, git checkout v01b, git checkout v01c

Anwendungsfälle (Use Cases)

Gegenüber dem dritten, vierten, fünften und sechsten Teil des Tutoriums ändern sich die die Anwendungsfälle nicht. Die Anwendung leistet also genau dasselbe wie zuvor.

Aufgabe

In diesem Teil geht es zunächst darum, die Lösung aus Teil 6 des Tutoriums mit vue zu realisieren.

Erstellen eines neuen Projekts

Erstellen Sie ein neues Projekt hello_world_vue:

cd <somewhere>

npm init vue@latest
  Project name: » hello_world_vue
  > Nun können Sie ESLint und Prettier auswählen.
  > Die Module Pinia und Router behandeln wir später.
  > Für die Praktikumsaufgaben sollten Sie TypeScript nicht aktivieren.

Nun können Sie das Prorjekt in VSC öffnen und dort in einem zugehörigen Terminal weiterarbeiten.

cd hello_world_vue # In VSC nicht mehr notwendig.
npm i
npm run dev  # Im Browser: http://localhost:5173/

# Linefeed ändern (unter Windows => Unix-Zeilenende)
cat <<EOF > .vscode/settings.json
{ "files.eol": "\n" }
EOF
# Tragen Sie
#     !.vscode/settings.json
# in die Datei .gitignore ein.
# VSC (neu) starten, damit settings.json gelesen wird
# und dann ein Terminal für das Projekt öffnen

# initialize git
git init
git add -A
git commit -m "initial commit"

git remote add origin https://gitlab.multimedia.hs-augsburg.de/ACCOUNT/hello_world_vue
git remote -v
git push --set-upstream origin master

# create a new git branch
git checkout -b v00
git push --set-upstream origin v00

Anmerkungen

Vue ist ein Framework zum Erstellen von Single-Page-Web-Anwendungen. Es ist allerdings möglich, mehrere Web-Seiten unter die Kontrolle von Vite zu stellen.

Beispiel: wk_ecmascript_01

Achtung: Bauen Sie diesen Code NICHT in Ihre vue-Anwendung ein. Diese basiert als Single-PAge-Anwendung vollständig auf der Datei index.html.

// vite.config.js

...
  build: 
  { ...
    rollupOptions: 
    { input: 
      { main:     resolve(__dirname, './src/index.html'),
        solution: resolve(__dirname, './src/index_solution.html')
      }
    }
  },
...

Vue basierte bis Version 3.0 auf Webpack. Webpack gilt allerdings schon wieder als Dinosaurier (existiert seit 2012).

Seit Version 3.2 verwendet Vue Vite (französisch „schnell“). Dies hat zur Konsequenz, dass das Initialisieren und Testen einer Vue-Anwendung anders erfolgt als früher.

Vue 3 unterstützt im Gegensatz zu Vue 2 TypeScript. Allerdings wird im Tutorium Typescript nicht eingesetzt.

Vue-Anwendungen sind aus Komponenten aufgebaut. Das passt sehr gut zum Atomic-Design-Prinzip.

Überführung von hello-world-06 nach vue

git checkout -b v01   # neuen Git-Zweig (branch) anlegen

.gitignore            # ansehen
LICENSE               # wird aus einem anderen Projekt kopiert
package.json          # mit package.json von hello_world_06 mergen
vite.config.js        # mit vite.config.js von hello_world_06 mergen
                      # Ausnahmen: root, publicDir, und outDir
index.html            # gemäß hello_world_06 definieren; Import von 'main.js' anpassen:
                      # * <code>@import url("/css/head.css");</code>
                      # * <code>src="src/main.js"</code>)
                      # * Der Inhalt des Body-Elements wird ersetzt: <main id="app"></main>
src/css               # Ordner <code>src/css</code> von hello_world_06 übernehmen
public                # favicon ersetzen
README.md             # ansehen und verbessern

npm run dev           # Vite-Server neu starten

Tipp
Passen Sie die Datei eslint.config.js an. Wenn Sie Code von mir verwenden, benötigen Sie die Regel 'no-unexpected-multiline': 0. Sonst bekommen Sie bei Code von mir regelmäßig Fehlermeldungen.

import js               from '@eslint/js'
import pluginVue        from 'eslint-plugin-vue'
import globals          from 'globals'
import { defineConfig } from "eslint/config";

export default defineConfig([
  { name: 'app/files-to-lint',
    files: [ '**/*.{js,mjs,jsx,vue}' ],
  },

  { name: 'app/files-to-ignore',
    ignores: ['**/node_modules/**', '**/dist/**', '**/public/**', '**/dist-ssr/**', '**/coverage/**'],
  },

  { languageOptions:
    { globals:
      { ...globals.browser,
      },
    },
  },

  js.configs.recommended,
  ...pluginVue.configs['flat/essential'],

  { rules:
    { 'no-unexpected-multiline': 0,
    },
  },
])

Relative Pfade
Um relative Pfade beim Building zu erzwingen, reicht es nicht, die Option base: '' in vite.config.js einzufügen. Sie müssen dies zusätzlich in vue.config.js festlegen:

export default
{ publicPath: '' }

Passen Sie auch noch die Datei jsconfig.json an, damit in Visual Studio Code die Pfadverfolgung mittels Point and Click ermöglicht wird. Leider funktioniert dies nicht für den Alias config.

{ "compilerOptions": 
  {  "baseUrl": ".",
     "paths": 
      {  "@/*":           ["./src/*"],
         "config.css":    ["./src/css/_config.css"],
         "/css/*":        ["./src/css/*.css"],
         "/js/*":         ["./src/js/*"],
         "/controller/*": ["./src/controller/*"],
         "/service/*":    ["./src/service/*"],
         "/component/*":  ["./src/component/*"],
         "/store/*":      ["./src/store/*"]
      }
  },
  "exclude":    [ "node_modules", "dist" ],
  "extensions": [".js", ".vue", ".json"]
}

src/main.js

Da wir in diesem Tutorium die JavaScript-Dateien asynchron laden, müssen sie das Main-Skript entsprechenden anpassen. Die App darf erst erstellt und gemounted werden, wenn alle Dateien der App vollständig geladen wurden.

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

window.addEventListener
( 'load', 
  () => createApp(App).mount('#app')
)

index.html

  • Die Single-Page-Datei index-html wird gemäß Hello World 06 definiert.

Allerdings wird der Inhalt des Bodies in die Komponenten-Datei App.vue verlagert. Wenn Sie eine Datei favicon.ico zur Hand haben, können Sie die Vite-Version dieser Datei ersetzen.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset = "UTF-8">
  <meta name    = "viewport"
        content = "width=device-width, initial-scale=1.0, user-scalable=yes"
  >

  <title>Hello World Vue (v01)</title>
  
  <style>
    @import url("/css/head.css");
  </style>
 
  <script src="src/main.js" async type="module"></script>
</head>

<body>
  <main id="app"></main>
</body>

</html>

src/App.vue

  • Die Hello-World-Komponente und das Style-Element laden.
  • Nur die Hello-World-Komponente ins Template einfügen.
  • body.css importieren (ohne das Attribut scoped, damit der Inhalt der CSS-Datei allen Komponenten zur Verfügung steht).
<script setup>
  import HelloWorld from './component/HelloWorld.vue'
</script>

<template>
  <HelloWorld />
</template>

<style lang="css">
  @import '/css/body.css';
</style>

Der Ordner src/assets wird nun nicht mehr benötigt und kann gelöscht werden ebenso wie die anderen beiden Komponenten.

src/component/HelloWorld.vue

In die HelloWorld-Komponente werden die beiden Sections aus der Hello-World-06-App eingefügt.

Sections aus index.html in Template-Element einfügen.

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

<template>
  <section id="section_form" class="hidden">
    <h1>Hello, Stranger!</h1>
    <form>
      <div>
        <label for="input_name">What's your name?</label>
        <input id="input_name" autofocus>
      </div>
      <div>
        <input id="button_reset"  type="reset"  value="Reset">
        <input id="button_submit" type="button" value="Say hello">
      </div>
    </form>
  </section>
  <section id="section_hello" class="hidden">
    <h1 id="heading_hello">Hello, ...!</h1>
    <p>Welcome to Full-Stack Web Development!</p>
  </section>
</template>

Sectionspezifische CSS-Anweisungen aus der Hello-World-06-App werden in die Datei src/css/HelloWorld.css verschoben. Diese Datei wird in der Komponetne in src/component/HelloWorld.vue als Style importiert (import '@/css/component/HelloWorld'; ).

CSS für die Hello-World-Komponente

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

/* Hier wird alles aus der Datei body.css übernommen,
 * bis auf html {...} und body {...}.
 * Die Config-Datei wird in beiden CSS-Dateien importiert.
 */

Anschließend wird diese CSS-Datei in der Datei HelloWorld.vue importiert. Mittels scoped wird angegeben, dass die CSS-Anweisungen nur Auswirkungen auf den HTML-Code innerhalb der Datei HelloWorld.vue hat. Beachten Sie: In der Datei App.vue wurde bewusst auf scoped verzichtet. Das hat zu Folge, dass die dort importierten CSS-Anweisungen auf den gesamten HTML-Code der App auswirkungen haben.

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

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

Die wesentlichen Hello-World-Attribute und -Funktionen aus den JavaScript-Dateien der Hello-World-06-App werden in den Script-Bereich eingefügt.

Diese Elemente werden mittel ref und computed im nachfolgenden Schritt bidirektional mit den HTML-Elementen im Template-Element verknüpft.

Script für die HelloWorld-Komponenten

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

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

  const
    section       = ref('form'),
    helloStranger = ref('Hello, Stranger'),
    name          = ref(''),

    hello    = computed(() => name.value === '' ? helloStranger.value : `Hello, ${name.value}!`),
    sayHello = () => section.value = 'hello'
</script>

Im Template von HelloWorld.vue sollten Sie alle Attribute "id" mit Ausnahme von "input_name" löschen.

Der Grund ist, dass ID-Attribute im HTML-Dokument eindeutig sein müssen. Wenn in einer Komponente ein ID-Attribut definiert wird, kann man diese nicht mehrfach in eine andere Komponente einfügen. Man sollte in Komponenten, die mehrfach verwendet werden sollte, keine fest definierten IDs einfügen.

Ein Ausnahme bildet zunächst noch das Attribut "input_name", da dieser Identifikator vom zugehörigen Label referenziert wird: <label for="input_name">.

Verknüpfen Sie nun das Template bidirectional mit den Script-Elementen:

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

Autofokus-Aktivierung per JavaScript
Wir können auch noch den Autofokus-Mechanismus von Hello-World-06 nachbilden. Dazu verwenden wird zwei Vue-Observerfunktionen onMounted und onUnmounted. Diese werden jeweils aufgerufen, wenn das Ergeignis „Komponente wurde in den DOM-Baum eingefügt“ bzw. „Komponente wurde aus dem DOM-Baum entfernt“ eintritt. Wenn die Kompontente eingefügt wird, werden die bekannten Autofokus-Eventlistener registriert und der Autofokus aktiviert. Wenn die Komponenten entfernt wird, werden die Autofokus-Eventlistener gelöscht, da evtl. andere Komponenten einen anderen Autofokus aktivieren möchten.

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

  const
    section       = ref('form'),
    helloStranger = ref('Hello, Stranger'),
    name          = ref(''),

    hello     = computed(() => name.value === '' ? helloStranger.value : `Hello, ${name.value}!`),
    sayHello  = () => section.value = 'hello',
    autofocus = () => document.getElementById('input_name').focus(),
    visOn     = () => window.addEventListener('visibilitychange', autofocus),
    visOff    = () => window.removeEventListener('visibilitychange', autofocus)

  onMounted  (visOn);
  onUnmounted(visOff);
</script>

Darüber hinaus wird autofocus bei der Aktivierung des Resetbuttons ausgeführt. Damit wird der Fokus bei einem Rest auf das Eingaefeld gesetzt. Da der Event automatisch an den Default-Handler weitergeleitet wird, funktioniert die eigentliche Reset-Funktion (löschen der Feldinhalte) weiterhin.

<template>
...
      <div>
        <input type="reset"  value="Reset"     v-on:click="autofocus"/>
        <input type="button" value="Say hello" v-on:click="sayHello"/>
      </div>
...

Mounting in Vue

Man beachte, dass onMounted und onUnmounted Callback-Funktionen als Eingabeparameter erwarten. Diese werden ausgeführt, sobald die entsprechenden Ereignisse von Vue signalisiert werden. Es gibt noch diverse weitere Eventhandler für verschiedene Vue-Ereignisse.

Komponenten-Zustände
Vue-Komponenten durchlaufen verschiedene Zustände. Bei jedem Zustandswechsel wird ein Event ausgelöst:

  • beforeCreate: Zustand, bevor eine Komponente initialisiert wird
  • created: Zustand, nachdem eine Komponente vollständig initialisiert wurde
  • beforeMount: Zustand, bevor eine Komponente in den DOM-Baum des Browsers eingefügt wird
  • mounted: Zustand, nachdem eine Komponente in den DOM-Baum des Browsers eingefügt wurde
  • beforeUpdate: Zustand, bevor eine Komponente im DOM-Baum des Browsers modifiziert wird (wegen einer Änderung des zugehörigen Modells)
  • updated: Zustand, nachdem eine Komponente im DOM-Baum des Browsers modifiziert wurde
  • beforeUnmount: Zustand, bevor eine Komponente aus dem DOM-Baum des Browsers gelöscht wird
  • unmounted: Zustand, nachdem eine Komponente aus dem DOM-Baum des Browsers gelöscht wurde

In der Composition API und der Setup-API kann man sich für diese Ereignisse (mit Ausnahme der ersten beiden) als Observer mittels der Methoden

 onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted

registrieren. Daneben gibt es in der Composition API noch ein paar weitere Ereignisse, für die man sich als Observer registrieren kann.

Testen

Rufen Sie nun npm run dev oder npm run build sowie npm run prebuild auf.

Alternative Skript-Varianten

Im Folgenden sehen Sie drei Skript-Varianten (ohne Behandlung von Autofocus). Bitte benutzen Sie in Ihrem Prhekte die erste Variante (<script setup).

<script setup> (Vue 3.2, Vue 3.4, Vue 3.5)

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

  const
    section       = ref('form'),
    helloStranger = ref('Hello, Stranger'),
    name          = ref(''),

    hello     = computed(() => name.value === '' ? helloStranger.value : `Hello, ${name.value}!`),
    sayHello  = () => section.value = 'hello',
    autofocus = () => document.getElementById('input_name').focus(),
    visOn     = () => window.addEventListener('visibilitychange', autofocus),
    visOff    = () => window.removeEventListener('visibilitychange', autofocus)

  onMounted  (visOn);
  onUnmounted(visOff);
</script>

Musterlösung wk_hello_world_vue, git checkout v01a

Composition API (Vue 3.0)

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

  export default
  { setup()
    { const
        section       = ref('form'),
        helloStranger = ref('Hello, Stranger'),
        name          = ref(''),

        hello     = computed(() => name.value === '' ? helloStranger.value : `Hello, ${name.value}!`),
        sayHello  = () => section.value = 'hello',
        autofocus = () => document.getElementById('input_name').focus(),
        visOn     = () => window.addEventListener('visibilitychange', autofocus),
        visOff    = () => window.removeEventListener('visibilitychange', autofocus)

      onMounted  (visOn);
      onUnmounted(visOff);

      return {
        section, helloStranger, name,
        hello, sayHello, autofocus
      }
    }
  }
</script>

Diese Variante kann ohne weitere Anpassungen an Stelle der Script-Setup-Variante verwendet werden. Allerdings ist die Script-Setup-Variante leichter zu schreiben und zu lesen.

Musterlösung wk_hello_world_vue, git checkout v01b

Options API (Vue 2.0)

<script>
  export default
  { name: 'HelloWorld',

    data:
    function()
    { return {
        section:       'form',
        helloStranger: 'Hello, Stranger!',
        name:          ''
      }
    },

    computed:
    { hello()
      { return this.name === '' ? this.helloStranger : `Hello, ${this.name}!` }
    },

    methods:
    { sayHello()
      { this.section = 'hello' },

      autofocus()
      { document.getElementById('input_name').focus() },
    },

    mounted()
    { window.addEventListener('visibilitychange', this.autofocus).bind(this) },

    unmounted()
    { window.removeEventListener('visibilitychange', this.autofocus).bind(this) }
  }
</script>

Diese Variante kann ohne weitere Anpassungen an Stelle der Script-Setup-Variante verwendet werden. Allerdings ist die Script-Setup-Variante leichter zu schreiben und zu lesen. Die Options-API gilt als veraltet.

Musterlösung wk_hello_world_vue, git checkout v01c

Entfernen der letzten ID

Zurzeit existiert in der Komponente noch die ID input_name. In den älteren Vue-Versionen musste man eine zufällige ID erzeugen (z.B. milltel dem Paket uuid, um sicherzustellen, dass die Komponenten mehrfach verwendet werden kann, ohne dass diese ID mehrfach im HTML-Dokument vorkommt.

Seit Vue 3.5 gibt es für diese Augabe die Funktion useId.

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

  const
    inputId       = useId(),
    ...
...
</script>
<template>
...
  <div>
    <label :for="inputId">What's your name?</label>
    <input :id="inputId" v-model="name" autofocus>
  </div>
...
</template>

Nun sollte es klappen, die Komponente zweimal in eine HTML-Seite einzubauen.

Musterlösung wk_hello_world_vue, git checkout v01

Properties

Wenn man den HTML-Code der App mit zwei Modulen HelloWorld untersucht, stellt man fest, dass die Property autofocus zweimal auf der HTML-Seite vorkommt, in jeder Komponenteninstanz einmal. Das ist laut HTML-Spezifikation nicht erlaubt.

Abhilfe schaffen die Template-Properties, die man mittels defineProps im Script-Bereich der Komponenten-Datei deklarieren kann.

const
  props = defineProps ({ autofocus: { type: Boolean } }),
  ...
  autoFocus = () => { if (props.autofocus)
                        document.getElementById(inputId).focus()
                    },
  ...

Nun kann man das Attribut dynamisch ins Template einbinden:

<input :id="inputId" v-model="name" :autofocus>

Ersetzen Sie außerdem in der gesamten Datei HelloWorld.vue den Strin 'input_name' durch die Konstante inputId (ohne Anführungsstriche!)

Fügen Sie folgenden Code in die Datei App.vue ein, um das Funktionieren der Property autofocus zu überprüfen.

<template>
  <HelloWorld autofocus />
  <HelloWorld />
</template>

Perfekt ist diese Lösung noch nicht, da der Autofokus-Mechanischmus nur für das erste HelloWorld-Element korrekt funktioniert. Wenn ein Dokument aus mehreren verschiedenen Komponenten besteht und die Seite ein auf der Seite in einer Komponete enthlatenes Autofokus-Element unterstützen soll, benötigt man einen Store, in dem die ID dieses Elements für alle Komponenten zugänglich ist. Mit Stores befassen wir uns später.

Vite für Development- und Produktionsmodus

Ein Problem gibt es bei der aktuellen Vue-Version nocht. Im Development-Modus integriert Vue irgendwelche __devtools__ in die Test-Anwendung. Die kommen mit dem Vite-Modul postcss-var-replace nicht zurecht.

Als Lösung teilen wir die Datei vite.config.js in drei Dateien auf:

  • vite.config.common.js: Enthält alles bis auf postcss-var-replace.
  • vite.config.js: Importiert vite.config.common.js und exportiert das importierte Objekt. Hier könnten weitere Konfigurationsanweisungen stehen, die speziell für den Development-Modus von Bedeutung sind. Das werden wir in einem späteren Tutorium noch ausnutzen.
  • vite.config.build.js: Importiert ebenfalls vite.config.common.js, bindet aber postcss-var-replace vor dem Export in das Konfigurationsobjekt ein.
//vite.config.js

import common from './vite.config.common'

export default common
//vite.config.build.js

import common                from './vite.config.common'
import { postcssVarReplace } from 'postcss-var-replace';

common.css.postcss.plugins = [ postcssVarReplace() ]; 

export default common

Nun müssen wir in der Datei package.json das Build-Script noch anpasse, damit Vite die neue Konfigurationsdatei verwendet.

"scripts": {
  "dev": "vite",
  "build": "vite build --config ./vite.config.build.js --emptyOutDir",
  "prepreview": "vite build --config ./vite.config.build.js --emptyOutDir",
  "preview": "vite preview",
  "lint": "eslint . --fix"
},

Fortsetzung des Tutoriums

Sie sollten nun Teil 2 des Vue-Tutoriums bearbeiten. Es wird ein Keyup-Event-Handler ergänzt, zu Wahrung der Barrierefreiheit.

Quellen

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