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

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg
Kowa (Diskussion | Beiträge)
Kowa (Diskussion | Beiträge)
 
(52 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 6: Zeile 6:
== Anwendungsfälle (Use Cases) ==
== Anwendungsfälle (Use Cases) ==
Die Version des [[HTML5-Tutorium:_JavaScript:_Hello_World_Vue_05|fünften Teils des Vue-Tutoriums]]
Die Version des [[HTML5-Tutorium:_JavaScript:_Hello_World_Vue_05|fünften Teils des Vue-Tutoriums]]
wird so geändert, dass die Sprache zur Laufzeit geändert werden kann.  
wird so angepasst, dass die Sprache zur Laufzeit geändert werden kann.  


Zunächst wird jedoch eine Navigationskomponente eingeführt. Dies hat den Zweck, das Routing an einem Beispiel zu erklären.
Zunächst wird jedoch eine Navigationskomponente eingeführt. Dies hat den Zweck, das Routing an einem Beispiel zu erklären.
Zeile 18: Zeile 18:
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> und fügen Sie das Package <code>uuid</code> hinzu:


<source lang="bash">
<syntaxhighlight lang="bash">
git checkout v05b    # Wechsle in den Branch v05b
git checkout v05d    # Wechsle in den Branch v05d mit Express-Server
git checkout -b v06  # Klone v05b in einen neuen Branch v06
git checkout -b v06  # Klone v05d in einen neuen Branch v06
rm -rf express_Hello_world
cd frontend
npm i
npm i
npm i vue-router
npm i vue-router
</source>
</syntaxhighlight>
 
In der Datei <code>public/json/config.jso</code>
aktivieren wir wieder den Zugriff auf die lokalen JSON-Dateien.
Den Zugriff auf den Backend-Server, um die Bilder von dort zu holen, behandeln wir später.
 
<syntaxhighlight lang="json">
{ "startSection":    "form",
  "apiRoot":        "/json/i18n_$1.json",
  "XapiRoot":        "/api/$1",
  "defaultLanguage": "de"
}
</syntaxhighlight>
 
Fügen Sie außerdem in die Datei <code>vite.config.common.js</code> zwei geeignete Aliase für
<code>view</code> und </code>router</code> ein.


=== Views ===
=== Views ===


Anstelle von Sections werden Views verwendet.
Anstelle von Sections werden im Frontend Views verwendet.
Dazu werden die Section-Komponenten in den Ordner View verschoben und umbenannt:
Dazu werden die Section-Komponenten in den Ordner View verschoben und umbenannt:


<source lang="bash">
<syntaxhighlight lang="bash">
src/view/ViewHello.vue
src/view/ViewHello.vue
src/view/ViewHome.vue    // an Stelle von ViewForm.vue
src/view/ViewHome.vue    // an Stelle von ViewForm.vue


src/view/ViewError404.vue // neu
src/view/ViewError404.vue // neu
</source>
</syntaxhighlight>


Die SCSS-Dateien müssen natürlich auch entsprechend verschoben und abgepasst werden.
Die CSS-Dateien müssen natürlich auch entsprechend verschoben und abgepasst werden.


ViewForm.vue wird in ViewHome.vue unbenannt,
ViewForm.vue wird in ViewHome.vue unbenannt,
Zeile 44: Zeile 61:
Zusätzlich wird eine Seite ViewError404.vue angelegt, die immer dann ausgeliefert wird, wenn der Benutzer auf eine URL zugreift, der keine View-Datei zugeordnet ist.
Zusätzlich wird eine Seite ViewError404.vue angelegt, die immer dann ausgeliefert wird, wenn der Benutzer auf eine URL zugreift, der keine View-Datei zugeordnet ist.


Die SCSS-Dateien sollten analog umbenannt und unter den neuen Namen importiert werden. Für <code>ViewError404.vue</code> muss natürlich auch eine SCSS-Datei angelegt werden. Diese verweist allerdings nur auf <code>_View.scss</code>.
Die CSS-Dateien sollten analog umbenannt und unter den neuen Namen importiert werden. Für <code>ViewError404.vue</code> muss natürlich auch eine CSS-Datei angelegt werden. Diese verweist allerdings nur auf <code>View.css</code>.


<source lang="html">
<syntaxhighlight lang="html">
<!-- src/view/ViewError404.vue -->
<!-- src/view/ViewError404.vue -->


<script setup>
<script setup>
   import router    from '@/router'
   import router    from '/router'
   import storeI18n from '@/store/StoreI18n'
   import storeI18n from '/store/StoreI18n'


   const phrases = storeI18n().phrases
   const phrases = storeI18n().phrases
Zeile 61: Zeile 78:
</template>
</template>


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


=== Router-Plugin ===
=== Router-Plugin ===
Zeile 71: Zeile 88:
Dieses muss  zunächst händisch auf Basis von <code>vue-router</code> implementiert werden.
Dieses muss  zunächst händisch auf Basis von <code>vue-router</code> implementiert werden.


<source lang="ecmascript">
<syntaxhighlight lang="ecmascript">
// src/router/index.js
// src/router/index.js


import { createRouter, createWebHistory } from 'vue-router'
import { createRouter, createWebHistory } from 'vue-router'
import ViewHome                          from '@/view/ViewHome.vue'
import ViewHome                          from '/view/ViewHome.vue'


const
const
Zeile 85: Zeile 102:
       { path: '/hello',
       { path: '/hello',
         name: 'hello',
         name: 'hello',
         component: () => import('@/view/ViewHello.vue'),
         component: () => import('/view/ViewHello.vue'),
       },
       },
       { path:      '/:pathMatch(.*)',
       { path:      '/:pathMatch(.*)',
         component: () => import('@/view/ViewError404.vue'),
         component: () => import('/view/ViewError404.vue'),
       }
       }
     ],
     ],
Zeile 96: Zeile 113:


export default router
export default router
</source>
</syntaxhighlight>


Nun muss man das neu erstellte Plugin in die App integrieren.
Nun muss man das neu erstellte Plugin in die App integrieren.


<source lang="ecmascript">
<syntaxhighlight lang="ecmascript">
// src/main.js
// src/main.js


import { createPinia } from 'pinia'
import { createPinia } from 'pinia'
import { createApp }  from 'vue'
import { createApp }  from 'vue'
import getJson        from '@/service/getJson'
import getJson        from '/service/getJson'
import router          from './router'  // './router/index.js'
import router          from './router'  // './router/index.js'
import App            from './App.vue'
import App            from './App.vue'
Zeile 112: Zeile 129:
   app    = createApp(App),
   app    = createApp(App),
   init    = async () =>
   init    = async () =>
             { const
             { app
                 config = await getJson('/json/config.json')
                 .provide('config', await getJson('/json/config.json'))
              app
                .provide('config', config)
                 .use(pinia)
                 .use(pinia)
                 .use(router)
                 .use(router)
Zeile 123: Zeile 137:


window.addEventListener('load', init)
window.addEventListener('load', init)
</source>
</syntaxhighlight>


=== index.html ===
=== index.html ===
Zeile 130: Zeile 144:
wird es aus der Datei <code>index.html</code> entfernt.
wird es aus der Datei <code>index.html</code> entfernt.


<source lang="html">
<syntaxhighlight lang="html">
...
...
   <body id="app">
   <body id="app">
   </body>
   </body>
...
...
</source>
</syntaxhighlight>


=== App.vue ===
=== App.vue ===
Zeile 141: Zeile 155:
Nun kann in die App eine Navigation eingebaut werden.
Nun kann in die App eine Navigation eingebaut werden.


<source lang="html">
<syntaxhighlight lang="html">
<!-- /src/App.vue -->
<!-- /src/App.vue -->


<script setup>
<script setup>
   import AppNav      from '@/components/AppNav.vue'
   import AppNav      from '/component/AppNav.vue'
   import storeSession from '@/store/StoreSession'
   import storeSession from '/store/StoreSession'
   import storeI18n    from '@/store/StoreI18n'
   import storeI18n    from '/store/StoreI18n'
 
 
   const session = storeSession()
   const session = storeSession()


   storeI18n(); // activate initialization of storeI18n
   storeI18n();
 
   session.initialize();
   session.initialize();
</script>
</script>
Zeile 163: Zeile 178:
</template>
</template>


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


</source>
</syntaxhighlight>


Die zugehörige Navigationskomponente muss auch noch implementiert werden.
Die zugehörige Navigationskomponente muss auch noch implementiert werden.


<source lang="html">
<syntaxhighlight lang="html">
<!-- /src/components/AppNav.vue -->
<!-- /src/component/AppNav.vue -->


<script setup>
<script setup>
Zeile 186: Zeile 201:
</template>
</template>


<style scoped lang="scss">
<style scoped>
   @import '/css/components/AppNav';
   @import '/css/component/AppNav.css';
</style>
</style>
</source>
</syntaxhighlight>


<source lang="css">
<syntaxhighlight>
/* src/css/_config.scss */
/* src/css/_config.css */
...
...
$background-color:  #C5EFFC;
--background-color:  #C5EFFC;
$link-color:        #0027d4;
--link-color:        #0027d4;
...
...
$nav-dist:          0.5em;
--nav-dist:          0.5em;




/* src/css/AppNav.scss */
/* src/css/AppNav.css */


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


nav
nav
Zeile 211: Zeile 226:
   ul
   ul
   { list-style-type: none;
   { list-style-type: none;
     margin: 0 $nav-dist;
     margin: 0 var(--nav-dist);
     padding: 0;
     padding: 0;


     li
     li
     { display: inline;
     { display: inline;
       a { padding: 0 $nav-dist 0.2em; }
       a { padding: 0 var(--nav-dist); }
     }
     }


     a
     a
     { text-decoration: none;
     { text-decoration: none;
       color:          $link-color;
       color:          var(--link-color);
       font-weight:    bold;
       font-weight:    bold;
     }
     }
Zeile 227: Zeile 242:
     a:hover, .router-link-exact-active
     a:hover, .router-link-exact-active
     { text-decoration: underline;
     { text-decoration: underline;
       color:          $link-color;
       color:          var(--link-color);
       font-weight:    bold;
       font-weight:    bold;
     }
     }
Zeile 236: Zeile 251:
   }
   }
}
}
</source>
</syntaxhighlight>


=== Navigation ===
=== Navigation ===
Zeile 243: Zeile 258:
Das ist in der App.vue der Fall (siehe zuvor) sowie in der ViewHome.vue.
Das ist in der App.vue der Fall (siehe zuvor) sowie in der ViewHome.vue.


<source lang="html">
<syntaxhighlight lang="html">
<!-- /src/vie/ViewHome.vue  -->
<!-- /src/vie/ViewHome.vue  -->


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


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


   const
   const
     session    = storeSession(),
     session   = storeSession(),
     sayHello   = () => router.push('/hello'),
     greeting  = storeGreeting(),
   
    dictionary = greeting.dictionary,
    greeting    = storeGreeting(),
     sayHello   = () => router.push('/hello')
    dictionary  = greeting.dictionary
</script>
</script>
</source>
</syntaxhighlight>


Die Section-Verwaltung der ehemaligen Single-Page-Anwendung kann und sollte gelöscht werden:
Die Section-Verwaltung der ehemaligen Single-Page-Anwendung kann und sollte gelöscht werden:
Zeile 268: Zeile 283:
* <code>ViewHello.vue</code>
* <code>ViewHello.vue</code>


Die Datei <code>HelloWorld.vue</code> kann (samt SCSS-Datei) ganz gelöscht werden.
Die Datei <code>HelloWorld.vue</code> kann (samt CSS-Datei) ganz gelöscht werden.


== Hello World Vue 06a ==
== Hello World Vue 06a ==
Zeile 274: Zeile 289:
Aufgabe: Die Ausgabesprache soll vom Benutzer zur Laufzeit geändert werden können. Die Sprachen sollen durch Bilder (Flaggen) symbolisiert werden.
Aufgabe: Die Ausgabesprache soll vom Benutzer zur Laufzeit geändert werden können. Die Sprachen sollen durch Bilder (Flaggen) symbolisiert werden.


<source lang="bash">
<syntaxhighlight lang="bash">
git checkout v06      # Wechsle in den Branch v06
git checkout v06      # Wechsle in den Branch v06
git checkout -b v06a  # Klone v06 in einen neuen Branch v06a
git checkout -b v06a  # Klone v06 in einen neuen Branch v06a
</source>
</syntaxhighlight>


=== <code>config.json</code> ===
=== <code>config.json</code> ===
Zeile 283: Zeile 298:
Um die vorhandenen Sprachen durchblättern zu können, wird ein einfacher Automat verwendet.
Um die vorhandenen Sprachen durchblättern zu können, wird ein einfacher Automat verwendet.


<source lang="json">
<syntaxhighlight lang="json">
// src/public/json/config.json
// src/public/json/config.json


Zeile 291: Zeile 306:
   "nextLanguage":    {"de": "en", "en": "de"}
   "nextLanguage":    {"de": "en", "en": "de"}
}
}
</source>
</syntaxhighlight>


=== StoreI18n.js: Integration von nextLanguage ===
=== StoreI18n.js: Integration von nextLanguage ===
Zeile 298: Zeile 313:
Dazu sollte zunächst die zunächst Konstante <code>nextLanguage</code> definiert werden.
Dazu sollte zunächst die zunächst Konstante <code>nextLanguage</code> definiert werden.


<source lang="ecmascript">
<syntaxhighlight lang="ecmascript">
...
...
lang =
lang =
Zeile 307: Zeile 322:


...
...
</source>
</syntaxhighlight>


Diese neue Konstante wird verwendet, um die Sprache mittels <code>changeLang</code> zu definieren, sofern
Diese neue Konstante <code>nextLanguage</code> wird verwendet, um die Sprache mittels <code>changeLang</code> zu definieren, sofern
kein Argument übergeben wurde.
kein Argument übergeben wurde.


<source lang="ecmascript">
Die Funktion <code>changeLang</code> soll bei einer Änderung der Sprache sowohl die notwendige JSON-Sprach-Datei laden als auch die in der HTML-Datei <code>index.html</code> ausgewiesene Sprache anpassen:
 
<syntaxhighlight lang="ecmascript">
// /src/store/storeI18N.js
 
changeLang =
changeLang =
   async (p_lang = null) =>
   async (p_lang = null) =>
Zeile 322: Zeile 341:
initialize =  
initialize =  
   async resolve =>  
   async resolve =>  
  { await changeLang(config.defaultLanguage);  
        { await changeLang(config.defaultLanguage);  
    resolve();  
          resolve();  
  }
        }
</source>
</syntaxhighlight>


=== Bilder von Flaggen ===
=== Bilder von Flaggen ===


Zur Visualisierung der gewählten Sprache werden Bilder von Flaggen eingesetzt.
Zur Visualisierung der gewählten Sprache werden Bilder von Flaggen eingesetzt.
Wir kopieren die Bilder im SVG-Format aus dem NPM-Paket "https://github.com/lipis/flag-icon-css".
Wir kopieren die Bilder im SVG-Format aus dem NPM-Paket "https://github.com/lipis/flag-icons".


<source lang="bash">
<syntaxhighlight lang="bash">
npm i -D flag-icon-css
npm i -D flag-icons
</source>
</syntaxhighlight>


Im Ordner "node_modules/flag-icon-css/flags/4x3" finden Sie Flaggen zahlreicher Länder im 4-zu-3-Format.
Im Ordner "node_modules/flag-icons/flags/4x3" finden Sie Flaggen zahlreicher Länder im 4-zu-3-Format.
Kopieren Sie die Flaggen der Länder Deutschland, Großbritanien und Frankreich in den Ordner <code>public/img</code>.
Kopieren Sie die Flaggen der Länder Deutschland und Großbritanien in den Ordner <code>public/img</code>.


In die Datei config.js wird der Pfad, unter dem die Bilder zu finden sind, eingefügt.
In die Datei config.js wird der Pfad, unter dem die Bilder zu finden sind, eingefügt.


<source lang="ecmascript">
<syntaxhighlight lang="ecmascript">
//  public/config.js
//  public/config.js
...
...
   "apiRoot":  "/json/i18n_$1.json",
   "apiRoot":  "/json/i18n_$1.json",
   "imgPath":  "/img/$1.svg",
   "imgPath":  "/img/$1.svg",
   "XapiRoot": "/api/$1",
   "XapiRoot": "/api/$1"
  "XimgPath": "/api/$1/img",
...
...
</source>
</syntaxhighlight>


=== <code>FormButtonIcon</code> ===
=== <code>FormButtonIcon</code> ===
Zeile 357: Zeile 375:
Das Bild kann zur Laufzeit geändert werden.
Das Bild kann zur Laufzeit geändert werden.


<source lang="ecmascript">
<syntaxhighlight lang="ecmascript">
<script setup>
<script setup>
   import { computed, inject } from 'vue'
   import { computed, inject } from 'vue'
Zeile 380: Zeile 398:
</template>
</template>


<style scoped lang="scss">
<style scoped>
   @import '/css/components/form/FormButtonIcon.scss';
   @import '/css/component/form/FormButtonIcon.css';
</style>
</style>
</source>
</syntaxhighlight>


<source lang="ecmascript">
<syntaxhighlight lang="ecmascript">
/* src/css/components/FormButtonIcon.scss */
/* src/css/component/FormButtonIcon.css */


@import 'FormButton';
@import 'FormButton.css';


input
input
Zeile 394: Zeile 412:
   margin-left: $nav-dist          !important;
   margin-left: $nav-dist          !important;
}
}
</source>
</syntaxhighlight>


Fügen Sie in <code>src/css/_config.scss</code> noch  
Fügen Sie in <code>src/css/_config.css</code> noch  


<source lang="ecmascript">
<syntaxhighlight lang="ecmascript">
$button-icon-width: 1em;
--button-icon-width: 1em;
</source>
</syntaxhighlight>


ein.
ein.
Zeile 411: Zeile 429:
folgende Attribute:
folgende Attribute:


<source lang="html">
<syntaxhighlight lang="html">
     ...        ...,
     ...        ...,
     "navHome":  "Start",
     "navHome":  "Start",
     "navHello": "Hallo"
     "navHello": "Hallo"
</source>
</syntaxhighlight>


bzw.
bzw.


<source lang="html">
<syntaxhighlight lang="html">
     ...        ...,
     ...        ...,
     "navHome":  "Home",
     "navHome":  "Home",
     "navHello": "Hello"
     "navHello": "Hello"
</source>
</syntaxhighlight>


Damit können Sie das Navigationsmenü internationalisieren (einschließlich Auswahl der Sprache per Button).
Damit können Sie das Navigationsmenü internationalisieren (einschließlich Auswahl der Sprache per Button).


<source lang="html">
<syntaxhighlight lang="html">
<!-- src/components/AppNav -->
<!-- src/component/AppNav.vue -->


<script setup>
<script setup>
   import FormButtonIcon from '@/components/form/FormButtonIcon.vue'
   import FormButtonIcon from '/component/form/FormButtonIcon.vue'
   import storeI18n      from '@/store/StoreI18n'
   import storeI18n      from '/store/StoreI18n'
 
   const i18n = storeI18n()
   const i18n = storeI18n()
</script>
</script>
Zeile 446: Zeile 465:
</template>
</template>


<style scoped lang="scss">
<style scoped>
   @import '/css/components/AppNav';
   @import '/css/component/AppNav.css';
</style>
</style>
</source>
</syntaxhighlight>


== Hello World Vue 06b ==
== Hello World Vue 06b ==


Aufgabe: Defaultmäßig soll dem Benutzer die App in der Sprache abgezeigt werden, die der Benutzer
Aufgabe: Sorgen Sie dafür, dass <code>autofocus</code> korrekt funktioniert, insbesondere beim Seitenwechsel via Navigation.
im Browser voreingestellt hat. Sollte die voreingestellte Sprache nicht unterstütz werden, wird die App in der Defaultsprache angezeigt.
 
Es ist sinnvoll, die Funktionalität in ein eigenes Store-Modul auszulagern, damit
das Session-Modul in Anwendungen, die keine Autofocus-Funktionalität benötigen, keinen
überflüssigen Code enthalten (Bertrand Meyer: Jedes Modul erfüllt genau eine Aufgabe).
Das Session-Modul wird entsprechend abgespeckt.
Der Autofocus-Store soll zwei Funktionen bieten:
* <code>setAutofocusId</code>: Speichert die ID des Autofokus-Elements der aktuellen Seite (Default: <code>null</code>).
* <code>setFocus</code>: Setzt den Focus auf die aktuelle Autofokus-Komponente (sofern die ID ungleich <code>null</code> ist). Insbesondere für das Window-Ereignis <code>'visibilitychange'</code> soll diese Funktion
aufgerufen werden.
 
<syntaxhighlight lang="ecmascript">
// /src/store/StoreAutofocus.js
 
import { defineStore } from 'pinia'
import { ref }        from 'vue'
 
const storeAutofocus =
defineStore
( 'autofocus', // must be unique
 
  () =>
  { const
      c_autofocus_id = ref(),
      setFocus      = () => document.getElementById(c_autofocus_id.value)?.focus(),
      setAutofocusId = p_autofocus_id => { c_autofocus_id.value = p_autofocus_id;
                                          setFocus();
                                        }
 
    window.addEventListener('visibilitychange', setFocus);
 
    return { setAutofocusId, setFocus }
  }
)
 
export default storeAutofocus
</syntaxhighlight>
 
<syntaxhighlight lang="ecmascript">
// /src/store/StoreSession.js
 
import { defineStore } from 'pinia'
import { ref }        from 'vue'
 
const storeSession =
defineStore
( 'session', // must be unique
 
  () =>
  { const
      initializers  = [],
      isInitialized  = ref(false),
      addInitializer = p_initializer =>
                      initializers.push(new Promise(resolve => p_initializer(resolve))),
      initialize    = () => Promise.all(initializers)
                                    .then(() => isInitialized.value = true)
 
    return { addInitializer, initialize, isInitialized }
  }
)
 
export default storeSession
</syntaxhighlight>
 
Um die aktuelle ID speichern zu können, muss das zugehörige Textfeld die Information, welche ID es hat,
per <code>emit</code> an die Elternkomponente ausgeben.
 
<syntaxhighlight lang="html">
// /src/component/form/FormTextfield.vue
 
...
const
  props = defineProps
          ({ id: { type: String,  default: '' },
            ...
          })
  ...
  emit  = defineEmits(['id', 'update:text', 'enter']),
...
emit('id', inputId);
...
</syntaxhighlight>
 
Nun müssen die Views, die ein Autofokus-Feld beinhalten, dieses an den Session-Store melden,
sobald sie per Routing aktiviert werden. Das ist in unserem Fall nur die <code>ViewHome</code>.
Ein Watcher sorgt dafür, dass die ID des Textfeldes im Session-Objekt gespeichert wird, sobald
das zugehörige Textfeld die ID per <code>emit</code> meldet.
 
<syntaxhighlight lang="html">
// /src/view/ViewHome.vue
 
<script setup>
  import { ref, watch } from 'vue'
  ...
  import storeAutofocus from '/store/StoreAutofocus'
  ...
 
  const
    id        = ref(null),
    autofocus  = storeAutofocus(),
    ...
    setId      = p_id => id.value = p_id,
    ...
 
  watch( id, () => autofocus.setAutofocusId(id.value) )
</script>
 
<template>
  ...
        <FormTextfield @id="setId"
          ...
        >
  ...
</template>
...
</syntaxhighlight>
 
Zum Schluss muss im Router noch die Autofocus-ID bei jedem Seitenwechsel gelöscht werden.
 
<syntaxhighlight lang="ecmascript">
// /src/router/index.js
 
...
import storeAutofocus from '/store/StoreAutofocus'
 
const
  ...
 
router.beforeEach(() => storeAutofocus().setAutofocusId())
 
export default router
</syntaxhighlight>
 
Wichtig ist hier, dass man keine Konstante <code>session</code> definieren kann, da die Stores erst nach dem Router-Objekt erzeugt werden:
 
<syntaxhighlight lang="ecmascript">
// /src/router/index.js
...
import storeAutofocus from '/store/StoreAutofocus'
 
const
  autofocus = storeAutofocus(), // FUNKTIONIERT NICHT
  ...
router.beforeEach(() => autofocus.setAutofocusId())
...
</syntaxhighlight>
 
Der Funktionsaufruf <code>storeSession()</code> darf daher erst in  der Callback-Funktion von
<code>router.beforeEach</code> aufgerufen werden. Zu diesem Zeitpunkt sind die Stores und der Router auf jeden Fall schon initialisiert worden.
 
Zum Abschluss sollten Sie noch versuchen, <code>/src/component/AppNav.vue</code> so abzuändern,
dass ein Klick auf die Fahne nicht nur die Sprache ändert, sondern auch noch den Focus auf das Startfeld setzt.
 
== Hello World Vue 06c ==
 
Aufgabe: Sorgen Sie dafür, dass die unterstützten Sprachen und der Sprach-Automat vom Backend
geladen wird.
 
<syntaxhighlight lang="bash">
git checkout v06b    # Wechsle in den Branch v06b
git checkout -b v06c  # Klone v06b in einen neuen Branch v06c
</syntaxhighlight>
 
Splitten Sie die Config-Datei zunächst in zwei Teile: "config.json" wird aus dem Public-Ordner geladen.
"config_backend.json" wird normalerweise vom Backend geladen, kann aber zum Entwicklungszeitpunkt auch vom Public-Ordner geladen werden, wenn das Backend noch nicht stabil läuft.
 
<syntaxhighlight lang="json">
// src/public/json/config.json
 
{ "configBackend":  "/json/config_backend.json",
  "apiRoot":        "/json/i18n_$1.json",
  "imgPath":        "/img/$1.svg",
 
  "XapiRoot":      "/api/$1"
}
</syntaxhighlight>
 
<syntaxhighlight lang="json">
// src/public/json/config_backend.json
 
{ "languages":    ["de", "en"],
  "nextLanguage": {"de": "en", "en": "de"}
}
</syntaxhighlight>
 
Das Backend teilt dem Frontend mit, welche Sprachen unterstützt werden.
 
=== <code>main.js</code> ===
 
Laden Sie, nachdem Sie "config" asynchron geladen haben, "configBackend" asynchron und kopieren Sie den Inhalt dieses Objekts in das Config-Objekt. Den Pfad von "configBackend" finden Sie unter "config.configBackend".
 
<syntaxhighlight lang="ecmascript">
...
const
  ...
  init = async () =>
        { const
            config        = await getJson('/json/config.json'),
            configBackend = await getJson(config.configBackend)
 
          Object.assign(config, configBackend)
 
          app
            .provide('config', config)
            .use(pinia)
            .use(router)
            .mount('#app') // app is shown to the user
        }
</syntaxhighlight>
 
Nun gibt es im Konfigurationsobjekt keine Defaultsprache mehr.
Im I18N-Store wird daher ab sofort die erste Sprache in der Liste der
unterstützten Sprachen als Defaultsprache genutzt.
 
<syntaxhighlight lang="ecmascript">
// /src/store/storeI18N.js
 
initialize =
  async resolve =>
        { await changeLang(config.languages[0]);
          resolve();
        }
</syntaxhighlight>
 
== Hello World Vue 06d ==
 
Aufgabe: Defaultmäßig soll dem Benutzer die App in der Sprache angezeigt werden, die der Benutzer
im Browser voreingestellt hat. Sollte die voreingestellte Sprache nicht unterstützt werden, wird die App in der Defaultsprache angezeigt.


Legen Sie zunächst wieder einen neuen Branch an.
Legen Sie zunächst wieder einen neuen Branch an.


<source lang="bash">
<syntaxhighlight lang="bash">
git checkout v06a     # Wechsle in den Branch v06a
git checkout v06c     # Wechsle in den Branch v06c
git checkout -b v06b # Klone v06a in einen neuen Branch v06b
git checkout -b v06d # Klone v06c in einen neuen Branch v06d
</source>
</syntaxhighlight>


Das Vorgehen ist im Prinzip ganz einfach.
Das Vorgehen ist im Prinzip ganz einfach.


Mittels des NPM-Packets "navigator-languages" ermittelt man die vom Browser unterstützten
Mittels des NPM-Packets "navigator-languages" ermittelt man die vom Browser unterstützten
Sprachen.  In der Konfigurationsdatei listet man die Sprachen auf, die von der App unterstütz werden.
Sprachen.  In der Konfigurationsdatei listet man die Sprachen auf, die von der App unterstützt werden.
Besser wäre es natürlich, diese Liste vom Backend zu laden, damit die App mit bekommt, wenn neue
Besser wäre es natürlich, diese Liste vom Backend zu laden, damit die App mit bekommt, wenn neue
Sprachen zur Verfügung stehen. Am aufwändigsten ist es, diese beiden Array zu matchen.
Sprachen zur Verfügung stehen. Am aufwändigsten ist es, diese beiden Array zu matchen.
Zeile 472: Zeile 718:
Mögliche Erweiterung: Auch die HTML-Pfade sollten internationalisiert werden.
Mögliche Erweiterung: Auch die HTML-Pfade sollten internationalisiert werden.


<source lang="bash">
<syntaxhighlight lang="bash">
npm i navigator-languages // unterstützt mehrere Browservarianten
npm i navigator-languages // unterstützt mehrere Browservarianten
</source>
</syntaxhighlight>


=== Language Matching ===
=== Language Matching ===
Zeile 481: Zeile 727:
Zunächst wird überprüft, ob es exakte Übereinstimmungen gibt. Sollte das nicht der Fall sein, wird überprüft, ob es Matches zwischen den ersten beiden Buchstaben gibt ('de', 'de-AT', 'de-DE' matchen in diesem Schritt alle miteinander).
Zunächst wird überprüft, ob es exakte Übereinstimmungen gibt. Sollte das nicht der Fall sein, wird überprüft, ob es Matches zwischen den ersten beiden Buchstaben gibt ('de', 'de-AT', 'de-DE' matchen in diesem Schritt alle miteinander).


<source lang="ecmascript">
<syntaxhighlight lang="ecmascript">
// src/util/chooseLanguage.js
// src/util/chooseLanguage.js


Zeile 538: Zeile 784:


export default chooseLanguage
export default chooseLanguage
</source>
</syntaxhighlight>


=== <code>config.js</code> ===
=== <code>src/store/StoreI18n</code> ===
 
Das Attribut <code>defaultLanguage</code> wird durch das Attribut <code>languages</code> ersetzt.


<source lang="ecmascript">
Im I18N-Store wird nun beim Initialisieren nicht nicht mehr eine Defaultspache geladen, sondern
{ "apiRoot":      "/json/i18n_$1.json",
die Sprache, die möglichst nah an den Wünschen des im Browser des Benutzers gespeicherten Sprachwünschen liegt.
  "imgPath":      "/img/$1.svg",
  "XapiRoot":    "/api/$1",
  "XimgPath":    "/api/$1/img",
  "languages":    ["de", "en"],
  "nextLanguage": {"de": "en", "en": "de"}
}
</source>


=== <code>src/store/StoreI18n</code> ===
Beachten Sie, dass Sie in die Datei <code>vite.config.common.js</code> einen geeigneten Alias für den
<code>util</code>-Pfad definieren müssen, um <code>'/util/chooseLanguage'</code> an Stelle von
<code>'@/util/chooseLanguage'</code> schreiben zu können.


<source lang="ecmascript">
<syntaxhighlight lang="ecmascript">
...
...
import chooseLanguage from '@/util/chooseLanguage'
import chooseLanguage from '/util/chooseLanguage'
 
...
...
       () =>
       () =>
Zeile 571: Zeile 809:
         ...
         ...
...
...
</source>
</syntaxhighlight>
 
== Hello World Vue 06c ==
 
Aufgabe: Sorgen Sie dafür, dass die unterstützten Sprachen und der Sprach-Automat vom Backend
geladen wird.
 
<source lang="bash">
git checkout v06b    # Wechsle in den Branch v06b
git checkout -b v06c  # Klone v06b in einen neuen Branch v06c
</source>
 
Splitten Sie die Config-Datei zunächst in zwei Teile: "config.json" wird aus dem Public-Ordner geladen.
"config_backend.json" wird normalerweise vom Backend geladen, kann aber zum Entwicklungszeitpunkt auch vom Public-Ordner geladen werden, wenn das Backend noch nicht stabil läuft.
 
<source lang="json">
// src/public/json/config.json
 
{ "configBackend":  "/json/config_backend.json",
  "apiRoot":        "/json/i18n_$1.json",
  "imgPath":        "/img/$1.svg",
 
  "XconfigBackend": "/api/config",
  "XapiRoot":      "/api/$1",
  "XimgPath":      "/api/$1/img"
}
</source>
 
<source lang="json">
// src/public/json/config_backend.json
 
{ "languages":    ["de", "en"],
  "nextLanguage": {"de": "en", "en": "de"}
}
</source>
 
Das Backend teilt dem Frontend mit, welche Sprachen unterstützt werden.
 
=== <code>main.js</code> ===
 
Laden Sie, nachdem Sie "config" asynchron geladen haben, "configBackend" asynchron und kopieren Sie den Inhalt dieses Objekts in das Config-Objekt. Den Pfad von "configBackend" finden Sie unter "config.configBackend".
 
<source lang="ecmascript">
...
const
  ...
  init    = async () =>
            { const
                config = await getJson('/json/config.json'),
                configBackend = await getJson(config.configBackend)
 
              Object.assign(config, configBackend)
 
              app
                .provide('config', config)
                .use(pinia)
                .use(router)
                .mount('#app') // app is shown to the user
            }
</source>
 
=== <code>storeI18n.js</code> ===
 
Die Datei "index.html" sollte bei jeder Änderung der Sprache angepasst werden.


<source lang="json">
...
changeLang =
  async (p_lang = null) =>
        { lang.value = p_lang ?? nextLanguage[lang.value];
          await getI18n(lang.value);
          document.getElementsByTagName('html')[0].lang = lang.value
        }
...
</source>


<!--
<!--
== Fortsetzung des Tutoriums ==
== Fortsetzung des Tutoriums ==


Sie sollten nun [[HTML5-Tutorium: JavaScript: Hello World Vue 06|Teil 6 des Vue-Tutoriums]] bearbeiten.
Sie sollten nun [[HTML5-Tutorium: JavaScript: Hello World Vue 07|Teil 7 des Vue-Tutoriums]] bearbeiten.
In diesem Tutorium setzen Sie Routing ein, auch wenn das für die Hello-World-Anwendung nicht sonderlich sinnvoll ist.
In diesem Tutorium setzen Sie Routing ein, auch wenn das für die Hello-World-Anwendung nicht sonderlich sinnvoll ist.
-->
-->

Aktuelle Version vom 23. Mai 2025, 09:54 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
Git-Repository, git checkout v06, ... v06a, ... v06b

Anwendungsfälle (Use Cases)

Die Version des fünften Teils des Vue-Tutoriums wird so angepasst, dass die Sprache zur Laufzeit geändert werden kann.

Zunächst wird jedoch eine Navigationskomponente eingeführt. Dies hat den Zweck, das Routing an einem Beispiel zu erklären.

Aufgabe

Aufgabe: Konfigurieren und internationalisieren Sie die Anwendung mit Hilfe von dynamischen JSON-Dateien.

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 v05d    # Wechsle in den Branch v05d mit Express-Server
git checkout -b v06  # Klone v05d in einen neuen Branch v06
rm -rf express_Hello_world
cd frontend
npm i
npm i vue-router

In der Datei public/json/config.jso aktivieren wir wieder den Zugriff auf die lokalen JSON-Dateien. Den Zugriff auf den Backend-Server, um die Bilder von dort zu holen, behandeln wir später.

{ "startSection":    "form",
  "apiRoot":         "/json/i18n_$1.json",
  "XapiRoot":        "/api/$1",
  "defaultLanguage": "de"
}

Fügen Sie außerdem in die Datei vite.config.common.js zwei geeignete Aliase für view und router ein.

Views

Anstelle von Sections werden im Frontend Views verwendet. Dazu werden die Section-Komponenten in den Ordner View verschoben und umbenannt:

src/view/ViewHello.vue
src/view/ViewHome.vue     // an Stelle von ViewForm.vue

src/view/ViewError404.vue // neu

Die CSS-Dateien müssen natürlich auch entsprechend verschoben und abgepasst werden.

ViewForm.vue wird in ViewHome.vue unbenannt, da diese Seite als Startseite verwendet wird.

Zusätzlich wird eine Seite ViewError404.vue angelegt, die immer dann ausgeliefert wird, wenn der Benutzer auf eine URL zugreift, der keine View-Datei zugeordnet ist.

Die CSS-Dateien sollten analog umbenannt und unter den neuen Namen importiert werden. Für ViewError404.vue muss natürlich auch eine CSS-Datei angelegt werden. Diese verweist allerdings nur auf View.css.

<!-- src/view/ViewError404.vue -->

<script setup>
  import router    from '/router'
  import storeI18n from '/store/StoreI18n'

  const phrases = storeI18n().phrases
</script>

<template>
  <h1>404</h1>
  <p>{{phrases.pageNotFound.replace('$1', router.currentRoute.value.path)}}</p>
</template>

<style scoped>
  @import '/css/view/ViewError404.css';
</style>

Router-Plugin

Um mit Vue Multipage-Anwendungen realisieren zu können, benötigt man einRouter-Plugin. Dieses muss zunächst händisch auf Basis von vue-router implementiert werden.

// src/router/index.js

import { createRouter, createWebHistory } from 'vue-router'
import ViewHome                           from '/view/ViewHome.vue'

const
  routes =
    [ { path: '/',
        name: 'home',
        component: ViewHome,
      },
      { path: '/hello',
        name: 'hello',
        component: () => import('/view/ViewHello.vue'),
      },
      { path:      '/:pathMatch(.*)',
        component: () => import('/view/ViewError404.vue'),
      }
    ],

  router =
    createRouter({ history: createWebHistory(), routes })

export default router

Nun muss man das neu erstellte Plugin in die App integrieren.

// src/main.js

import { createPinia } from 'pinia'
import { createApp }   from 'vue'
import getJson         from '/service/getJson'
import router          from './router'  // './router/index.js'
import App             from './App.vue'
const
  pinia   = createPinia(),
  app     = createApp(App),
  init    = async () =>
            { app
                .provide('config', await getJson('/json/config.json'))
                .use(pinia)
                .use(router)
                .mount('#app') // app is shown to the user
            }

window.addEventListener('load', init)

index.html

Da das main-Tag nun innerhalb von App.vue verwendet werden soll, wird es aus der Datei index.html entfernt.

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

App.vue

Nun kann in die App eine Navigation eingebaut werden.

<!-- /src/App.vue -->

<script setup>
  import AppNav       from '/component/AppNav.vue'
  import storeSession from '/store/StoreSession'
  import storeI18n    from '/store/StoreI18n'

  const session = storeSession()

  storeI18n();

  session.initialize();
</script>

<template>
  <p      v-if="!session.isInitialized">Loading ...</p>
  <AppNav v-if="session.isInitialized"/>
  <main   v-if="session.isInitialized">
    <router-view/>
  </main>
</template>

<style>
  @import '/css/body.css';
</style>

Die zugehörige Navigationskomponente muss auch noch implementiert werden.

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

<script setup>
</script>

<template>
  <nav>
    <ul>
      <li><router-link to="/">Home</router-link></li>
      <li><router-link to="/hello">Hello</router-link></li>
    </ul>
  </nav>
</template>

<style scoped>
  @import '/css/component/AppNav.css';
</style>
/* src/css/_config.css */
...
--background-color:  #C5EFFC;
--link-color:        #0027d4;
...
--nav-dist:          0.5em;


/* src/css/AppNav.css */

@import 'config.css';

nav
{ top:      0;
  width:    100%;
  position: fixed;

  ul
  { list-style-type: none;
    margin: 0 var(--nav-dist);
    padding: 0;

    li
    { display: inline;
      a { padding: 0 var(--nav-dist); }
    }

    a
    { text-decoration: none;
      color:           var(--link-color);
      font-weight:     bold;
    }

    a:hover, .router-link-exact-active
    { text-decoration: underline;
      color:           var(--link-color);
      font-weight:     bold;
    }
  }

  label, input
  { margin: 0;
  }
}

Navigation

Die Navigation muss in allen Komponenten, in den Seiten gewechselt werden, angepasst werden.

Das ist in der App.vue der Fall (siehe zuvor) sowie in der ViewHome.vue.

<!-- /src/vie/ViewHome.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'
  import router        from '/router'

  const
    session    = storeSession(),
    greeting   = storeGreeting(),
    dictionary = greeting.dictionary,
    sayHello   = () => router.push('/hello')
</script>

Die Section-Verwaltung der ehemaligen Single-Page-Anwendung kann und sollte gelöscht werden:

  • config.js
  • storeSession.js
  • ViewHome.vue
  • ViewHello.vue

Die Datei HelloWorld.vue kann (samt CSS-Datei) ganz gelöscht werden.

Hello World Vue 06a

Aufgabe: Die Ausgabesprache soll vom Benutzer zur Laufzeit geändert werden können. Die Sprachen sollen durch Bilder (Flaggen) symbolisiert werden.

git checkout v06      # Wechsle in den Branch v06
git checkout -b v06a  # Klone v06 in einen neuen Branch v06a

config.json

Um die vorhandenen Sprachen durchblättern zu können, wird ein einfacher Automat verwendet.

// src/public/json/config.json

{ "apiRoot":         "/json/i18n_$1.json",
  "XapiRoot":        "/api/$1",
  "defaultLanguage": "en",
  "nextLanguage":    {"de": "en", "en": "de"}
}

StoreI18n.js: Integration von nextLanguage

Der Automat nextLanguage wird in den StoreI18n integriert. Wenn die Methode changeLang ohne Argument aufgerufen wird, soll der Automat die aktuelle Sprache lang durch die Nachfolgersprache ersetzen. Dazu sollte zunächst die zunächst Konstante nextLanguage definiert werden.

...
lang =
  ref(null),

nextLanguage =
  reactive(config.nextLanguage),

...

Diese neue Konstante nextLanguage wird verwendet, um die Sprache mittels changeLang zu definieren, sofern kein Argument übergeben wurde.

Die Funktion changeLang soll bei einer Änderung der Sprache sowohl die notwendige JSON-Sprach-Datei laden als auch die in der HTML-Datei index.html ausgewiesene Sprache anpassen:

// /src/store/storeI18N.js

changeLang =
  async (p_lang = null) =>
        { lang.value = p_lang ?? nextLanguage[lang.value];
          await getI18n(lang.value);
          document.getElementsByTagName('html')[0].lang = lang.value;
        }

initialize = 
  async resolve => 
        { await changeLang(config.defaultLanguage); 
          resolve(); 
        }

Bilder von Flaggen

Zur Visualisierung der gewählten Sprache werden Bilder von Flaggen eingesetzt. Wir kopieren die Bilder im SVG-Format aus dem NPM-Paket "https://github.com/lipis/flag-icons".

npm i -D flag-icons

Im Ordner "node_modules/flag-icons/flags/4x3" finden Sie Flaggen zahlreicher Länder im 4-zu-3-Format. Kopieren Sie die Flaggen der Länder Deutschland und Großbritanien in den Ordner public/img.

In die Datei config.js wird der Pfad, unter dem die Bilder zu finden sind, eingefügt.

//   public/config.js
...
  "apiRoot":  "/json/i18n_$1.json",
  "imgPath":  "/img/$1.svg",
  "XapiRoot": "/api/$1"
...

FormButtonIcon

Eine neue Komponente wird benötigt: FormButtonIcon Dabei handelt es sich um einen Button, der ein Icon anzeigt. Das Bild kann zur Laufzeit geändert werden.

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

  const
    props       = defineProps
                  ({ image: { type: String },
                     width: { type: String, default: '3em'},
                  }),
    config      = inject('config'),
    emit        = defineEmits(['click']),
    click       = () => emit('click'),
    require     = v_url => import.meta.url == null
                           ? v_url
                           : new URL(v_url, import.meta.url).href,
    imgPath     = config.imgPath,
    buttonImage = computed(() => require(imgPath.replace('$1', props.image)))
</script>

<template>
  <input type="image" :src="buttonImage" :style="{width}" @click="click" />
</template>

<style scoped>
  @import '/css/component/form/FormButtonIcon.css';
</style>
/* src/css/component/FormButtonIcon.css */

@import 'FormButton.css';

input
{ width:       $button_icon_width !important;
  margin-left: $nav-dist          !important;
}

Fügen Sie in src/css/_config.css noch

--button-icon-width: 1em;

ein.

Erweiterung von AppNav.vue

Der Sprachwahl-Button muss noch in die Navigation eingebaut werden. In diesem Rahmen wird auch gleich die Navigation etwas internationalisiert.

Ergänzen Sie in den Dictionaries der Dateien public/json/i18n_de.json und public/json/i18n_en.json folgende Attribute:

    ...         ...,
    "navHome":  "Start",
    "navHello": "Hallo"

bzw.

    ...         ...,
    "navHome":  "Home",
    "navHello": "Hello"

Damit können Sie das Navigationsmenü internationalisieren (einschließlich Auswahl der Sprache per Button).

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

<script setup>
  import FormButtonIcon from '/component/form/FormButtonIcon.vue'
  import storeI18n      from '/store/StoreI18n'

  const i18n = storeI18n()
</script>

<template>
  <nav>
    <ul>
      <li><router-link to="/">{{i18n.dictionary.navHome}}</router-link></li>
      <li><router-link to="/hello">{{i18n.dictionary.navHello}}</router-link></li>
      <li><FormButtonIcon :image="i18n.lang" @click="i18n.changeLang()"/></li>
    </ul>
  </nav>
</template>

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

Hello World Vue 06b

Aufgabe: Sorgen Sie dafür, dass autofocus korrekt funktioniert, insbesondere beim Seitenwechsel via Navigation.

Es ist sinnvoll, die Funktionalität in ein eigenes Store-Modul auszulagern, damit das Session-Modul in Anwendungen, die keine Autofocus-Funktionalität benötigen, keinen überflüssigen Code enthalten (Bertrand Meyer: Jedes Modul erfüllt genau eine Aufgabe). Das Session-Modul wird entsprechend abgespeckt.

Der Autofocus-Store soll zwei Funktionen bieten:

  • setAutofocusId: Speichert die ID des Autofokus-Elements der aktuellen Seite (Default: null).
  • setFocus: Setzt den Focus auf die aktuelle Autofokus-Komponente (sofern die ID ungleich null ist). Insbesondere für das Window-Ereignis 'visibilitychange' soll diese Funktion

aufgerufen werden.

// /src/store/StoreAutofocus.js

import { defineStore } from 'pinia'
import { ref }         from 'vue'

const storeAutofocus =
defineStore
( 'autofocus', // must be unique

  () =>
  { const
      c_autofocus_id = ref(),
      setFocus       = () => document.getElementById(c_autofocus_id.value)?.focus(),
      setAutofocusId = p_autofocus_id => { c_autofocus_id.value = p_autofocus_id; 
                                           setFocus();
                                         }

    window.addEventListener('visibilitychange', setFocus);

    return { setAutofocusId, setFocus }
  }
)

export default storeAutofocus
// /src/store/StoreSession.js

import { defineStore } from 'pinia'
import { ref }         from 'vue'

const storeSession =
defineStore
( 'session', // must be unique

  () =>
  { const
      initializers   = [],
      isInitialized  = ref(false),
      addInitializer = p_initializer =>
                       initializers.push(new Promise(resolve => p_initializer(resolve))),
      initialize     = () => Promise.all(initializers)
                                    .then(() => isInitialized.value = true)

    return { addInitializer, initialize, isInitialized }
  }
)

export default storeSession

Um die aktuelle ID speichern zu können, muss das zugehörige Textfeld die Information, welche ID es hat, per emit an die Elternkomponente ausgeben.

// /src/component/form/FormTextfield.vue

...
const
  props = defineProps
          ({ id: { type: String,  default: '' },
             ...
          })
  ... 
  emit  = defineEmits(['id', 'update:text', 'enter']),
...
emit('id', inputId);
...

Nun müssen die Views, die ein Autofokus-Feld beinhalten, dieses an den Session-Store melden, sobald sie per Routing aktiviert werden. Das ist in unserem Fall nur die ViewHome. Ein Watcher sorgt dafür, dass die ID des Textfeldes im Session-Objekt gespeichert wird, sobald das zugehörige Textfeld die ID per emit meldet.

// /src/view/ViewHome.vue

<script setup>
  import { ref, watch } from 'vue'
  ...
  import storeAutofocus from '/store/StoreAutofocus'
  ...

  const
    id         = ref(null),
    autofocus  = storeAutofocus(),
    ...
    setId      = p_id => id.value = p_id,
    ...

  watch( id, () => autofocus.setAutofocusId(id.value) )
</script>

<template>
  ...
        <FormTextfield @id="setId"
          ...
        >
  ...
</template>
...

Zum Schluss muss im Router noch die Autofocus-ID bei jedem Seitenwechsel gelöscht werden.

// /src/router/index.js

...
import storeAutofocus from '/store/StoreAutofocus'

const
  ...

router.beforeEach(() => storeAutofocus().setAutofocusId())

export default router

Wichtig ist hier, dass man keine Konstante session definieren kann, da die Stores erst nach dem Router-Objekt erzeugt werden:

// /src/router/index.js
...
import storeAutofocus from '/store/StoreAutofocus'

const
  autofocus = storeAutofocus(), // FUNKTIONIERT NICHT
  ...
router.beforeEach(() => autofocus.setAutofocusId())
...

Der Funktionsaufruf storeSession() darf daher erst in der Callback-Funktion von router.beforeEach aufgerufen werden. Zu diesem Zeitpunkt sind die Stores und der Router auf jeden Fall schon initialisiert worden.

Zum Abschluss sollten Sie noch versuchen, /src/component/AppNav.vue so abzuändern, dass ein Klick auf die Fahne nicht nur die Sprache ändert, sondern auch noch den Focus auf das Startfeld setzt.

Hello World Vue 06c

Aufgabe: Sorgen Sie dafür, dass die unterstützten Sprachen und der Sprach-Automat vom Backend geladen wird.

git checkout v06b     # Wechsle in den Branch v06b
git checkout -b v06c  # Klone v06b in einen neuen Branch v06c

Splitten Sie die Config-Datei zunächst in zwei Teile: "config.json" wird aus dem Public-Ordner geladen. "config_backend.json" wird normalerweise vom Backend geladen, kann aber zum Entwicklungszeitpunkt auch vom Public-Ordner geladen werden, wenn das Backend noch nicht stabil läuft.

// src/public/json/config.json

{ "configBackend":  "/json/config_backend.json",
  "apiRoot":        "/json/i18n_$1.json",
  "imgPath":        "/img/$1.svg",

  "XapiRoot":       "/api/$1"
}
// src/public/json/config_backend.json

{ "languages":    ["de", "en"],
  "nextLanguage": {"de": "en", "en": "de"}
}

Das Backend teilt dem Frontend mit, welche Sprachen unterstützt werden.

main.js

Laden Sie, nachdem Sie "config" asynchron geladen haben, "configBackend" asynchron und kopieren Sie den Inhalt dieses Objekts in das Config-Objekt. Den Pfad von "configBackend" finden Sie unter "config.configBackend".

...
const
  ...
  init = async () =>
         { const
             config        = await getJson('/json/config.json'),
             configBackend = await getJson(config.configBackend)

           Object.assign(config, configBackend)

           app
             .provide('config', config)
             .use(pinia)
             .use(router)
             .mount('#app') // app is shown to the user
         }

Nun gibt es im Konfigurationsobjekt keine Defaultsprache mehr. Im I18N-Store wird daher ab sofort die erste Sprache in der Liste der unterstützten Sprachen als Defaultsprache genutzt.

// /src/store/storeI18N.js

initialize = 
  async resolve => 
        { await changeLang(config.languages[0]); 
          resolve(); 
        }

Hello World Vue 06d

Aufgabe: Defaultmäßig soll dem Benutzer die App in der Sprache angezeigt werden, die der Benutzer im Browser voreingestellt hat. Sollte die voreingestellte Sprache nicht unterstützt werden, wird die App in der Defaultsprache angezeigt.

Legen Sie zunächst wieder einen neuen Branch an.

git checkout v06c     # Wechsle in den Branch v06c
git checkout -b v06d  # Klone v06c in einen neuen Branch v06d

Das Vorgehen ist im Prinzip ganz einfach.

Mittels des NPM-Packets "navigator-languages" ermittelt man die vom Browser unterstützten Sprachen. In der Konfigurationsdatei listet man die Sprachen auf, die von der App unterstützt werden. Besser wäre es natürlich, diese Liste vom Backend zu laden, damit die App mit bekommt, wenn neue Sprachen zur Verfügung stehen. Am aufwändigsten ist es, diese beiden Array zu matchen.

Mögliche Erweiterung: Auch die HTML-Pfade sollten internationalisiert werden.

npm i navigator-languages // unterstützt mehrere Browservarianten

Language Matching

Das Language Matching erfolgt zweimal mit zwei verschachtelten Schleifen. Zunächst wird überprüft, ob es exakte Übereinstimmungen gibt. Sollte das nicht der Fall sein, wird überprüft, ob es Matches zwischen den ersten beiden Buchstaben gibt ('de', 'de-AT', 'de-DE' matchen in diesem Schritt alle miteinander).

// src/util/chooseLanguage.js

import navigatorLanguages from 'navigator-languages'

const
  /**
   * @function
   * @param  { Array<string> } p_langs_available 
   *         An array of language strings containing the languages
   *         supported by the app 
   * @param  { Array<string> } [p_langs_required  = navigatorLanguages()]
   *         An array of language strings containing the languages
   *         requested by the client 
   * @return { string } 
   *         The language to be displayed
   */
  chooseLanguage =
  (p_langs_available, p_langs_required = navigatorLanguages()) =>
  { let
      lang = p_langs_available[0],
      i=0, j=0, n=p_langs_required.length, m=p_langs_available.length
  
    outer:
    { while (i<n)
      { const c_lang_required = p_langs_required[i]
        while (j<m)
        { const c_lang_availabe = p_langs_available[j]
          if (c_lang_availabe === c_lang_required)
          // both languages match perfectly
          { lang = c_lang_availabe;
            break outer;
          }
          j++
        }
        i++
      }
      i=0, j=0
      while (i<n)
      { const c_lang_required = p_langs_required[i]
        while (j<m)
        { const c_lang_availabe = p_langs_available[j]
          if (c_lang_availabe.substring(0,2) === c_lang_required.substring(0,2))
          // main part of the languages match
          { lang = c_lang_availabe;
            break outer;
          }
          j++
        }
        i++
      }
    }
  
     return lang
   }

export default chooseLanguage

src/store/StoreI18n

Im I18N-Store wird nun beim Initialisieren nicht nicht mehr eine Defaultspache geladen, sondern die Sprache, die möglichst nah an den Wünschen des im Browser des Benutzers gespeicherten Sprachwünschen liegt.

Beachten Sie, dass Sie in die Datei vite.config.common.js einen geeigneten Alias für den util-Pfad definieren müssen, um '/util/chooseLanguage' an Stelle von '@/util/chooseLanguage' schreiben zu können.

...
import chooseLanguage from '/util/chooseLanguage'
...
      () =>
      { const
          ...
          initialize = 
             async resolve => 
                   { await changeLang(chooseLanguage(config.languages)); 
                     resolve(); 
                   }
        ...
...


Quellen

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