< PreviousChapter 8: Example2208.3.8 Front End: Screen View Mask (fe/src/screen.html)The following is the Front-End Screen View Mask in HTML/VueJS format. It provides the structure of the outmost screen or window of the application. The most nota- } } export default class Controller extends mvc.Controller { create () { /* create the component tree */ this.establish( "screen-model/screen-view/{units,unit,persons,person}", [ Model, View, Units, Unit, Persons, Person ] ) } async prepare () { /* hook into service error reporting */ this.sv().on("error", (err) => { /* eslint no-console: off */ this.value("dataScreenHint", err) console.log(`[SV] ERROR: ${err}`) }) /* hook into service connection reporting */ ➍ this.sv().on("open", () => this.value("dataOnlineStatus", "online")) this.sv().on("close", () => this.value("dataOnlineStatus", "offline")) /* connect to the backend... */ await this.sv().connect() this.spool(() => this.sv().disconnect()) /* subscribe to the server status */ let subscription = this.sv().query(`{ _Server { clients } }`).subscribe((result) => { if (result && result.data && result.data._Server) this.value("dataOnlineUsers", result.data._Server.clients) }) this.spool(() => subscription.unsubscribe()) /* master/detail communication */ ➎ this.subscribe("unit-add", (id) => { this.my("unit").publish("unit-added", id) }) this.subscribe("unit-delete", (id) => { this.my("unit").publish("unit-deleted", id) }) this.subscribe("unit-select", (id) => { this.my("unit").publish("unit-selected", id) }) this.subscribe("person-add", (id) => { this.my("person").publish("person-added", id) }) this.subscribe("unit-delete", (id) => { this.my("person").publish("person-deleted", id) }) this.subscribe("person-select", (id) => { this.my("person").publish("person-selected", id) }) } }User Interface Implementation221ble aspects are: First, ➊ it uses a unidirectional data-binding ({{...}}) for expand-ing fixed texts in the View Mask, while still allowing them to be translated by the Internationalization (I18N) facility. Second, ➋ it uses a bidirectional data-binding (v-model) for linking the View Mask input widgets with Presentation Model fields. Third, ➌ it uses Sockets for providing the View Mask slots for the Users, User, Per-sons and Person Dialogs. Fourth, ➍ it uses in-line expressions to let the View Mask dynamically change based on Presentation Model fields.<div class="screen" scope="screen"> <div class="header"> <div class="left"> ➊ <span class="title">{{ $t("title") }}</span> <span class="subtitle">— {{ $t("subtitle") }}</span> </div> <div class="right"> ➋ <select class="lang" name="lang" v-model="gsLang" title="Change the language of the application" v-tippy="{ delay: [ 1000, 200 ], arrow: true }"> <option value="en">EN</option> <option value="de">DE</option> </select> </div> </div> <div class="body"> ➌ <div class="units" data-socket="@units"></div> <div class="unit" data-socket="@unit"></div> <div class="persons" data-socket="@persons"></div> <div class="person" data-socket="@person"></div> </div> <div class="footer"> <div class="left"> ➍ <i class="fa" v-bind:class="[ dataOnlineStatus == 'online' ? 'fa-chain' : 'fa-chain-broken' ]"> </i> <span v-show="dataOnlineStatus === 'online'"> <i class="fa fa-user"></i> <span class="users">{{ dataOnlineUsers }}</span> <i v-show="dataOnlineProgress" class="fa fa-refresh fa-spin"></i> </span> </div> <div class="middle"> <div v-if="dataScreenHint !== ''"> <i class="fa fa-exclamation-triangle"></i> <span v-html="dataScreenHint"></span> </div> </div> <div class="right"> <i class="resizeHandle fa fa-th-large" title="Change the size of the application window" v-tippy="{ delay: [ 1000, 200 ], arrow: true }" ></i> </div> </div> </div>Chapter 8: Example222# English text translations en: title: Units & Persons subtitle: The HUICA Example Application error: Error # German text translations de: title: Einheiten & Personen subtitle: Die HUICA Beispiel-Anwendung error: Fehler@import "./common.css"; body { user-select: none; padding-left: 20px; padding-top: 20px; background-color: #cccccc; cursor: default; font-family: var(--font-family); font-size: var(--font-size); } @scope screen { .screen { width: 800px; height: 600px; user-select: none; border-radius: 4px; box-shadow: 0 2px 4px 0 #999999; display: flex; ➊ flex-direction: column; justify-content: space-between; > .header { height: 32px; border-radius: 4px 4px 0 0; background-color: var(--color-brown); color: var(--color-white); padding: 2px 10px 2px 10px; display: flex; flex-direction: row; flex-shrink: 0; justify-content: space-between; 8.3.9 Front End: Screen View I18N (fe/src/screen.yaml)The following is the Front-End Screen View Internationalization (I18N) mapping in YAML format. It translates the keywords in the View Mask to English and German texts.8.3.10 Front End: Screen View Style (fe/src/screen.css)The following is the Front-End Screen View Style in CSS4/PostCSS format. It gives the View Mask its distinct layout and visual styling. The most notable aspects are: First, ➊ it uses a CSS Flex layout for the header, body and footer areas. Second, ➋ it uses a CSS Grid layout for the four sub-Dialogs.User Interface Implementation2238.3.11 Front End: Units Components: (fe/src/units.js)The following is the Front-End Units Dialog Controller Component in ECMAScript 2018 format. The most notable aspects are: First, ➊ it consists of just a Control-ler Component and reuses the Roster Model and View Components. Second, ➋ it subscribes to the GraphQL-IO Back-End queries to continuously update the Roster Model in case of changes to the Unit entities by other instances of the User Inter-face. align-items: center; .title { font-weight: bold; font-size: 120%; } .subtitle { color: var(--color-brown-glare); } select.lang { background-color: var(--color-brown); color: var(--color-brown-glare); border-color: var(--color-brown-light); font-size: 80%; } } > .body { flex: 1; padding: 10px 10px 10px 10px; width: calc(100% - 20px); height: calc(100% - 20px); background-color: var(--color-white); display: grid; ➋ grid-template-columns: calc(50% - 7px) calc(50% - 7px); grid-template-rows: calc(50% - 7px) calc(50% - 7px); grid-column-gap: 10px; grid-row-gap: 10px; } > .footer { display: flex; flex-direction: row; flex-shrink: 0; justify-content: space-between; align-items: center; border-radius: 0 0 4px 4px; background-color: var(--color-grey); color: var(--color-white); padding: 4px 10px 4px 10px; .left, .right { color: var(--color-grey-light); .resizeHandle { cursor: nwse-resize; } } } } }Chapter 8: Example2248.3.12 Front End: Units I18N (fe/src/units.yaml)The following is the Front-End Units Internationalization (I18N) mapping in YAML format. It translates the keywords of the Roster View Mask to English and German texts./* external imports */ import { mvc, vue, i18next } from "gemstone" /* internal imports */ import Roster from "./roster.js" import i18n from "./units.yaml" /* MVC/CT Controller */ export default class Controller extends mvc.Controller { create () { this.establish("units-roster", [ Roster ]) ➊ } prepare () { /* provide translated roster title */ i18next.addResourceBundles("units", i18n) this.observe("gsLang", () => { this.value("paramTitle", vue.t("units:units")) }, { boot: true }) /* subscribe to all items via service */ let subscription = this.sv().query(`{ Units { id name } }`).subscribe((result) => { ➋ if (result && result.data && result.data.Units) this.value("dataItems", result.data.Units) }) this.spool(() => subscription.unsubscribe()) /* attach to the roster events */ this.observe("eventItemAdd", async () => { [...] }) this.observe("eventItemDelete", async (item) => { [...] }) this.observe("eventItemSelect", (item) => { /* notify sibling dialogs */ this.publish("unit-select", item.id) }) } }# English text translations en: units: Units # German text translations de: units: EinheitenUser Interface Implementation2258.3.13 Front End: Unit Components (fe/src/unit.js)The following is the Front-End Unit Dialog Components in ECMAScript 2018 format. It displays the form for viewing and editing a single Unit entity. The most notable aspects are: First, ➊ it uses a Presentation Model which directly reflects the fields of the Unit entity. Second, ➋ it uses a Presentation Logic to derive the enabled/disabled state of the View Mask from the selection of a Unit entity. Third, ➌ it sub-scribes to the GraphQL-IO Back-End queries to continuously update the Presenta-tion Model in case of changes to the selected Unit entity by other instances of the User Interface.import { mvc } from "gemstone" import { Bridge } from "./common.js" import mask from "./unit.html" import i18n from "./unit.yaml" import "./unit.css" class View extends mvc.View { render () { /* render the UI view mask fragment */ this.ui = this.mask("unit", { render: mask, i18n: i18n }) this.plug(this.ui) } } class Model extends mvc.Model { create () { /* define the presentation model */ ➊ this.model({ "stateDisabled": { value: true, valid: "boolean" }, "dataUnit": { value: null, valid: "object" }, "dataUnits": { value: [], valid: "object" }, "dataPersons": { value: [], valid: "object" }, "dataName": { value: null, valid: "(string|null)" }, "dataAbbreviation": { value: null, valid: "(string|null)" }, "dataParentUnit": { value: null, valid: "(string|null)" }, "dataDirector": { value: null, valid: "(string|null)" }, "dataMembers": { value: [], valid: "[string*]" } }) /* derive the enabled/disabled state */ ➋ this.observe("dataUnit", (unit) => { this.value("stateDisabled", unit === null) }) } } export default class Controller extends mvc.Controller { create () { this.establish("unit-model/unit-view", [ Model, View ]) } prepare () { /* define a data bridge between presentation and business model */ let bridge = new Bridge([ { pm: "dataName", bm: "name", type: "attr" }, { pm: "dataAbbreviation", bm: "abbreviation", type: "attr" }, { pm: "dataParentUnit", bm: "parentUnit", type: "rel?" }, { pm: "dataDirector", bm: "director", type: "rel?" }, Chapter 8: Example226 { pm: "dataMembers", bm: "members", type: "rel*" } ]) /* react on a new selected unit */ let subscriptionUnit = null this.spool(() => { if (subscriptionUnit !== null) subscriptionUnit.unsubscribe() }) this.subscribe("unit-selected", (id) => { /* short-circuit on re-selection */ let unit = this.value("dataUnit") if (unit !== null && unit.id === id) return /* unsubscribe previous subscription */ if (subscriptionUnit !== null) { subscriptionUnit.unsubscribe() subscriptionUnit = null } if (id === "") { /* destroy previous selection */ this.value("dataUnit", null) this.value("dataName", "") this.value("dataAbbreviation", "") this.value("dataParentUnit", null) this.value("dataDirector", null) this.value("dataMembers", []) } else { /* provide new selection */ subscriptionUnit = this.sv().query(`($id: UUID!) { Unit (id: $id) { id name abbreviation parentUnit { id } director { id } members { id } } }`, { id: id }).subscribe((result) => { ➌ if (result && result.data && result.data.Unit) { let unit = result.data.Unit this.value("dataUnit", unit) bridge.bm2pm(unit, this) } }) } }) /* react on view mask edits */ let timer = null this.observe(bridge.fields.map((x) => x.pm), () => { if (timer !== null) clearTimeout(timer) timer = setTimeout(async () => { let unit = this.value("dataUnit") let changeset = bridge.pm2bm(this, unit) if (Object.keys(changeset).length === 0) return await this.sv().mutation(`($id: UUID!, $with: JSON!) { Unit (id: $id) { update (with: $with) { id } User Interface Implementation2278.3.14 Front End: Unit View Mask (fe/src/unit.html)The following is the Unit Dialog View Mask in HTML/VueJS format. It specifies the structure of the Dialog. The most notable aspects are: First, ➊ it uses a unidirec-tional data-binding ({{...}}) for expanding fixed texts in the View Mask, while still allowing them to be translated by the Internationalization (I18N) facility. Second, ➋ it uses a bidirectional data-binding (v-model) for linking the View Mask input widgets with Presentation Model fields. } }`, { id: unit.id, with: changeset }) }, 1000) }, { op: "changed" }) /* subscribe to list of Units and Persons */ let subscription = this.sv().query(`{ Units { id name } Persons { id name } }`).subscribe((result) => { if (result && result.data) { this.value("dataUnits", result.data.Units) this.value("dataPersons", result.data.Persons) } }) this.spool(() => subscription.unsubscribe()) } }<div class="unit" scope="unit"> <div class="header"> <div class="title">{{ $t("title") }}</div> </div> <div class="body perfect-scrollbar"> <table class="form"> <!-- Unit.name (attribute) --> <tr> <td class="label"> <div class="label">{{ $t("name") }}:</div> ➊ </td> <td class="element" title="Change the name of this unit" v-tippy="{ delay: [ 1000, 200 ], arrow: true }"> <input class="textfield" v-model="dataName" v-bind:placeholder="$t('enter-name')"> ➋ </td> </tr> <!-- Unit.abbreviation (attribute) --> <tr> <td class="label"> <div class="label">{{ $t("abbreviation") }}:</div> </td> <td class="element" title="Change the abbrevation of this unit" v-tippy="{ delay: [ 1000, 200 ], arrow: true }"> <input class="textfield" Chapter 8: Example228 v-model="dataAbbreviation" v-bind:placeholder="$t('enter-abbreviation')"> </td> </tr> [...] </table> </div> <div class="mask-overlay" v-show="stateDisabled"> </div> </div>8.3.15 Front End: Unit View I18N (fe/src/unit.yaml)The following is the Front-End Unit Internationalization (I18N) mapping in YAML for-mat. It translates the keywords of the Unit View Mask to English and German texts.8.3.16 Front End: Unit View Style (fe/src/unit.css)The following is the Front-End Unit View Style in CSS4/PostCSS format. It gives the View Mask its distinct layout and visual styling.# English text translations en: title: Unit name: Name abbreviation: Abbreviation parent-unit: Parent Unit director: Director members: Members enter-name: Enter name of unit... enter-abbreviation: Enter abbreviation of unit... select-parent-unit: Select parent unit... select-director: Select director... select-members: Select members... # German text translations de: title: Einheit name: Name abbreviation: Abkürzung parent-unit: Mutter-Einheit director: Leiter members: Mitglieder enter-name: Gib Namen der Einheit ein... enter-abbreviation: Gib Abkürzung der Einheit ein... select-parent-unit: Wähle Mutter-Einheit... select-director: Wähle Leiter... select-members: Wähle Mitglieder...@import "./common.css"; @scope unit { div.unit { position: relative; width: 100%; height: 100%; border-radius: 4px 4px 4px 4px; User Interface Implementation2298.3.17 Front End: Persons Components: (fe/src/persons.js)The following is the Front-End Persons Dialog Controller Component in ECMAScript 2018 format. The most notable aspects are: First, ➊ it consists of just a Control-ler Component and reuses the Roster Model and View Components. Second, ➋ it subscribes to the GraphQL-IO Back-End queries to continuously update the Roster Model in case of changes to the Person entities by other instances of the User In-terface. border: 1px solid var(--color-brown-light); display: flex; flex-direction: column; justify-content: flex-start; > .header { height: 26px; flex-shrink: 0; border-radius: 4px 4px 0 0; background-color: var(--color-brown-light); color: var(--color-white); padding: 2px 10px 2px 10px; display: flex; flex-direction: row; justify-content: space-between; align-items: center; > .title { font-weight: bold; } } > .body { padding: 10px 10px 10px 10px; position: relative; overflow-x: hidden; overflow-y: hidden; flex-grow: 1; table { width: 100%; border-spacing: 4px; border-collapse: separate; td.label { vertical-align: top; div.label { width: 90px; } } td.element { width: 100%; } } } } }/* external imports */ import { mvc, vue, i18next } from "gemstone" /* internal imports */ import Roster from "./roster.js" import i18n from "./persons.yaml" /* MVC/CT Controller */ Next >