< PreviousChapter 8: Example2308.3.18 Front End: Persons I18N (fe/src/persons.yaml)The following is the Front-End Persons Internationalization (I18N) mapping in YAML format. It translates the keywords of the Roster View Mask to English and German texts.8.3.19 Front End: Person Components (fe/src/person.js)The following is the Front-End Person Dialog Components in ECMAScript 2018 for-mat. It displays the form for viewing and editing a single Person 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 ena-bled/disabled state of the View Mask from the selection of a Person entity. Third, ➌ it subscribes to the GraphQL-IO Back-End queries to continuously update the Pres-entation Model in case of changes to the selected Person entity by other instances of the User Interface.export default class Controller extends mvc.Controller { create () { this.establish("persons-roster", [ Roster ]) ➊ } prepare () { /* provide translated roster title */ i18next.addResourceBundles("persons", i18n) this.observe("gsLang", () => { this.value("paramTitle", vue.t("persons:persons")) }, { boot: true }) /* subscribe to all items via service */ let subscription = this.sv().query(`{ Persons { id name } }`).subscribe((result) => { ➋ if (result && result.data && result.data.Persons) this.value("dataItems", result.data.Persons) }) 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("person-select", item.id) }) } }# English text translations en: persons: Persons # German text translations de: persons: PersonenUser Interface Implementation231import { mvc } from "gemstone" import { Bridge } from "./common.js" import mask from "./person.html" import i18n from "./person.yaml" import "./person.css" class View extends mvc.View { render () { /* render the UI view mask fragment */ this.ui = this.mask("person", { 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" }, "dataPerson": { value: null, valid: "object" }, "dataUnits": { value: [], valid: "object" }, "dataPersons": { value: [], valid: "object" }, "dataName": { value: null, valid: "(string|null)" }, "dataInitials": { value: null, valid: "(string|null)" }, "dataRole": { value: null, valid: "(string|null)" }, "dataSupervisor": { value: null, valid: "(string|null)" }, "dataBelongsTo": { value: null, valid: "(string|null)" } }) /* derive the enabled/disabled state */ ➋ this.observe("dataPerson", (unit) => { this.value("stateDisabled", unit === null) }) } } export default class Controller extends mvc.Controller { create () { this.establish("person-model/person-view", [ Model, View ]) } prepare () { /* define a data bridge between presentation and business model */ let bridge = new Bridge([ { pm: "dataName", bm: "name", type: "attr" }, { pm: "dataInitials", bm: "initials", type: "attr" }, { pm: "dataRole", bm: "role", type: "attr" }, { pm: "dataSupervisor", bm: "supervisor", type: "rel?" }, { pm: "dataBelongsTo", bm: "belongsTo", type: "rel?" } ]) /* react on a new selected person */ let subscriptionPerson = null this.spool(() => { if (subscriptionPerson !== null) subscriptionPerson.unsubscribe() }) this.subscribe("person-selected", (id) => { /* short-circuit on re-selection */ let person = this.value("dataPerson") if (person !== null && person.id === id) return /* unsubscribe previous subscription */ Chapter 8: Example232 if (subscriptionPerson !== null) { subscriptionPerson.unsubscribe() subscriptionPerson = null } if (id === "") { /* destroy previous selection */ this.value("dataPerson", null) this.value("dataName", "") this.value("dataInitials", "") this.value("dataRole", "") this.value("dataSupervisor", null) this.value("dataBelongsTo", null) } else { /* provide new selection */ subscriptionPerson = this.sv().query(`($id: UUID!) { Person (id: $id) { id name initials role supervisor { id } belongsTo { id } } }`, { id: id }).subscribe((result) => { ➌ if (result && result.data && result.data.Person) { let person = result.data.Person this.value("dataPerson", person) bridge.bm2pm(person, 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 person = this.value("dataPerson") let changeset = bridge.pm2bm(this, person) if (Object.keys(changeset).length === 0) return await this.sv().mutation(`($id: UUID!, $with: JSON!) { Person (id: $id) { update (with: $with) { id } } }`, { id: person.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) User Interface Implementation2338.3.20 Front End: Person View Mask (fe/src/person.html)The following is the Person 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. } }) this.spool(() => subscription.unsubscribe()) } }<div class="person" scope="person"> <div class="header"> <div class="title">{{ $t("title") }}</div> </div> <div class="body perfect-scrollbar"> <table class="form"> <!-- Person.name (attribute) --> <tr> <td class="label"> <div class="label">{{ $t("name") }}:</div> ➊ </td> <td class="element" title="Change the name of the person" v-tippy="{ delay: [ 1000, 200 ], arrow: true }"> <input class="textfield" v-model="dataName" v-bind:placeholder="$t('enter-name')"> ➋ </td> </tr> <!-- Person.initials (attribute) --> <tr> <td class="label"> <div class="label">{{ $t("initials") }}:</div> </td> <td class="element" title="Change the initials of the person" v-tippy="{ delay: [ 1000, 200 ], arrow: true }"> <input class="textfield" v-model="dataInitials" v-bind:placeholder="$t('enter-initials')"> </td> </tr> <!-- Person.role (attribute) --> <tr> <td class="label"> <div class="label">{{ $t("role") }}:</div> </td> <td class="element" title="Change the role of the person" v-tippy="{ delay: [ 1000, 200 ], arrow: true }"> <input class="textfield" v-model="dataRole" Chapter 8: Example234@import "./common.css"; @scope person { div.person { position: relative; width: 100%; height: 100%; 8.3.21 Front End: Person View I18N (fe/src/person.yaml)The following is the Front-End Person Internationalization (I18N) mapping in YAML format. It translates the keywords of the Unit View Mask to English and German texts.8.3.22 Front End: Person View Style (fe/src/person.css)The following is the Front-End Person View Style in CSS4/PostCSS format. It gives the View Mask its distinct layout and visual styling. v-bind:placeholder="$t('enter-role')"> </td> </tr> [...] </table> </div> <div class="mask-overlay" v-show="stateDisabled"> </div> </div># English text translations en: title: Person name: Name initials: Initials role: Role supervisor: Supervisor select-supervisor: Select supervisor... unit: Unit select-unit: Select unit... enter-name: Enter name of person... enter-initials: Enter initials of person... enter-role: Enter role of person... # German text translations de: title: Person name: Name initials: Initialien role: Rolle supervisor: Vorgesetzter select-supervisor: Wähle Vorgesetzten... unit: Einheit select-unit: Wähle Einheit... enter-name: Gib Namen der Person ein... enter-initials: Gib Initialien der Person ein.. enter-role: Gib Rolle der Person ein..User Interface Implementation2358.3.23 Front End: Roster Components (fe/src/roster.js)The following is the Front-End Roster Widget Model and View Components in EC-MAScript 2018 format. It is reused by both the Units and Persons Dialogs to display the list of Unit and Person entities. The most notable aspects are: First, ➊ it uses a Presentation Model Command field to scroll the list to the end of the items in case of a newly added item. Second, ➋ it uses a Presentation Model which distinguish-es between external model fields the Widget consumer uses, and internal model fields the Widget just uses for internal Model to View communication. Third, ➌ it uses a Presentation Logic to determine the current list of filtered items. border-radius: 4px 4px 4px 4px; 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%; } } } } }Chapter 8: Example236/* external imports */ import { $, mvc } from "gemstone" import minimatch from "minimatch" /* internal imports */ import mask from "./roster.html" import i18n from "./roster.yaml" import "./roster.css" /* MVC/CT View */ class View extends mvc.View { render () { /* render UI fragment */ this.ui = this.mask("roster", { render: mask, i18n: i18n }) this.plug(this.ui) /* react on a command to scroll and select the end of the items */ this.observe("cmdScrollToEnd", () => { ➊ setTimeout(() => { $(this.ui.$refs.body).scrollTo("max") let items = this.value("dataItems") this.value("eventItemSelect", items[items.length - 1]) }, 100) }) } } /* MVC/CT Model */ export default class Model extends mvc.Model { create () { /* create component tree */ this.establish("roster-view", [ View ]) /* define presentation model */ ➋ this.model({ /* external model fields */ "paramTitle": { value: "", valid: "string" }, "cmdScrollToEnd": { value: false, valid: "boolean", autoreset: true }, "dataItems": { value: [], valid: "object" }, "eventItemAdd": { value: false, valid: "boolean", autoreset: true }, "eventItemDelete": { value: null, valid: "object", autoreset: true }, "eventItemSelect": { value: null, valid: "object", autoreset: true }, /* internal model fields */ "dataItemFilter": { value: "", valid: "string" }, "dataItemsFiltered": { value: [], valid: "object" }, "stateItemSelected": { value: null, valid: "object" } }) /* determine items to show */ ➌ this.observe([ "dataItems", "dataItemFilter" ], () => { let items = this.value("dataItems") let filter = this.value("dataItemFilter") /* determine filtered items */ let itemsFiltered = [] if (filter === "") itemsFiltered = items.slice() else { items.forEach((item) => { User Interface Implementation2378.3.24 Front End: Roster View Mask (fe/src/roster.html)The following is the Roster Widget View Mask in HTML/VueJS format. It specifies the structure of the Widget. 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. Third, ➌ it uses in-line expressions to let the View Mask dynamically change based on Presentation Model fields. if (minimatch(item.name || "", `*${filter}*`, { nocase: true })) itemsFiltered.push(item) }) } this.value("dataItemsFiltered", itemsFiltered) /* determine selected item */ let itemSelected = this.value("stateItemSelected") if (itemSelected !== null && items.filter((item) => item.id === itemSelected.id).length === 0) this.value("stateItemSelected", null) }, { op: "changed" }) /* react on selected item */ this.observe("eventItemSelect", (item) => { this.value("stateItemSelected", item) }) } }<div class="roster" scope="roster"> <!-- the dialog header --> <div class="header"> <div class="title"> {{ paramTitle }} ➊ <span v-if="dataItemsFiltered.length < dataItems.length" class="count"> ({{ dataItemsFiltered.length }}/{{ dataItems.length }}) </span> <span v-else class="count"> ({{ dataItems.length }}) </span> </div> <div class="icons"> <!-- filter field --> <input v-model="dataItemFilter" class="filter" v-bind:placeholder="$t('filter-items-ph')" v-bind:title="$t('filter-items-tt')" v-tippy="{ delay: [ 1000, 200 ], arrow: true }"> ➋ <!-- add button --> <i class="fa fa-plus-circle" v-on:click="eventItemAdd" v-bind:title="$t('add-item-tt')" v-tippy="{ delay: [ 1000, 200 ], arrow: true }"> Chapter 8: Example2388.3.25 Front End: Roster View I18N (fe/src/roster.yaml)The following is the Front-End Roster Widget Internationalization (I18N) mapping in YAML format. It translates the keywords of the Roster Widget View Mask to English and German texts.8.3.26 Front End: Roster View Style (fe/src/roster.css)The following is the Front-End Roster Widget View Style in CSS4/PostCSS format. It gives the View Mask its distinct layout and optical styling. </i> </div> </div> <!-- the dialog body --> <div class="body perfect-scrollbar" ref="body"> [...] </div> </div> </div># English text translations en: no-items: No items available no-items-filter: No items left to show due to filter filter-items-ph: Filter items... filter-items-tt: Filter the list of items (with a Unix glob expression) add-item-tt: Add new item delete-item-tt: Delete this item unnamed-item: unnamed item # German text translations de: no-items: Keine Einträge verfügbar no-items-filter: Keine Einträge mehr übrig zum Anzeigen aufgrund Filter filter-items-ph: Filtere Einträge... filter-items-tt: Filtere die Liste der Einträge (mit einem Unix glob-Aus-druck) add-item-tt: Füge Eintrag hinzu delete-item-tt: Lösche diesen Eintrag unnamed-item: unbenannter Eintrag@import "./common.css"; @scope roster { div.roster { width: 100%; height: 100%; border-radius: 4px 4px 4px 4px; border: 1px solid var(--color-brown-light); display: flex; flex-direction: column; justify-content: flex-start; /* the dialog header */ > .header { User Interface Implementation2398.3.27 Back End: Application Manifest (be/package.json)The following is the Back-End application manifest for the Node Package Manager (NPM) in JavaScript Object Notation (JSON) format. It specifies meta-information, development- and run-time dependencies for third-party products and the com-mands to execute at various stages in the development life-cycle. [...] } /* the dialog body */ > .body { [...] } } }{ "name": "unp-be", "version": "0.0.0", "homepage": "http://architecturedissertation.com/", "description": "Units and Persons (UnP) -- Backend", "keywords": [ "units", "persons", "backend" ], "license": "Apache-2.0", "author": { "name": "Ralf S. Engelschall", "url": "mailto:rse@engelschall.com" }, "devDependencies": { "nodemon": "1.17.5", "shx": "0.3.1" }, "dependencies": { "babel-register": "6.26.0", "babel-polyfill": "6.26.0", "babel-preset-env": "1.7.0", "co": "4.6.0", "microkernel": "2.0.0", "microkernel-mod-ctx": "0.9.6", "microkernel-mod-options": "0.9.7", "microkernel-mod-logger": "0.9.12", "microkernel-mod-daemon": "0.9.11", "microkernel-mod-title": "0.9.8", "microkernel-mod-shutdown": "0.9.7", "microkernel-mod-sequelize": "1.2.7", "microkernel-mod-graphqlio": "1.4.7", "graphql": "0.13.2", "graphql-tools": "3.0.4", "graphql-tools-types": "1.1.24", "graphql-tools-sequelize": "2.0.4", "sequelize": "4.38.0", "sqlite3": "4.0.1", "pure-uuid": "1.5.3", "chance": "1.0.16" }, "engines": { "node": ">=8.0.0" }, "scripts": { "dev": "npm start", Next >