< PreviousChapter 8: Example210Figure 8.4: UnP Component Tree<<view>>roster<<model>>roster<<controller>>units<<controller>>persons<<view>>roster<<model>>roster<<controller>>person<<controller>>unit<<view>>unit<<model>>unit<<view>>person<<model>>person<<controller>>screen<<view>>screen<<model>>screenFigure 8.5: UnP Hierarchical DecompositionUser Interface Architecture211For the Back End of the UnP application, the GraphQL-IO, GraphQL-Tools-Se-quelize, and SQLite frameworks are primarily chosen. The communication between Front End and Back End happens via bi-directional GraphQL over WebSockets. See Figure 8.6 on page 211 for a Functional View of the architecture of UnP in total.<<composite>>Screen<<widget>>Roster<<composite>>Units<<composite>>Unit<<composite>>Persons<<composite>>Person<<procedure>>Main<<procedure>>Main<<framework>>GemstoneJS<<framework>>GraphQL-IO<<framework>>GraphQL-IO<<library>>GraphQL-Tools-Sequelize<<library>>SQLite<<framework>>BootstrapUnPBackendFrontend<<data>>DataFigure 8.6: UnP Functional ViewChapter 8: Example2128.3 User Interface ImplementationIf you don’t think carefully, you might believe that programming is just typing statements in a programming language. — Ward CunninghamIn this section, the Units and Persons (UnP) application finally has to be implement-ed. The following subsections show the partially reduced source-code of both the Front End and the Back End of the UnP application. For the full source-code see https://github.com/huica/unp/. The most notable HUICA-related aspects are: ■Reused Roster Widget: for the rosters of Units and Persons, the same un-derlying reusable Roster Widget Component is used. ■Master-Detail User Interface Pattern: For the Units to Unit and Persons to Person Dialog relationships, the usual Master-Detail User Interface Pattern is used. The Units and Persons Dialogs are the masters, the Unit and Person Dialogs are the details. ■Third-Party Solution Integration: For the Unit.parentUnit, Unit.direc-tor, Unit.members, Person.supervisor and Person.belongsTo rela-tionships, a special non-standard User Interface widget is used which is implemented by integrating the third-party Select2 library. ■Auto-Save: for convenience reasons, each change in the User Interface should be immediately persisted via GraphQL. ■Real-Time Updates: for convenience reasons, each change to any entity in the Back End should immediately be reflected in all connected Front Ends. ■Authentication & Authorization: as this example is about HUICA and HUI-CA is primarily about the Front End, the Back End intentionally is simplified by leaving out any authentication and authorization functions.8.3.1 Application Manifest (package.json) The following is the top-level application manifest for Node Package Manager (NPM) in JavaScript Object Notation (JSON) format. It specifies the top-level third-party package dependencies and the run-command scripts to be executed by the developer.{ "name": "unp", "version": "0.0.0", "homepage": "https://architecturedissertation.com/", "description": "Units and Persons (UnP)", "keywords": [ "huica", "example" ], "author": { "name": "Ralf S. Engelschall", "url": "mailto:rse@engelschall.com" }, User Interface Implementation2138.3.2 Front End: Application Manifest (fe/package.json)The following is the Front End application manifest for Node Package Manager (NPM) in JavaScript Object Notation (JSON) format. It specifies the Front End third-party package run-time and development-time dependencies and the run-com-mand scripts to be executed by the developer.{ "name": "unp-fe", "version": "0.0.0", "homepage": "http://architecturedissertation.com/", "description": "Units and Persons (UnP) -- Frontend", "keywords": [ "units", "persons", "example", "fron-tend" ], "license": "Apache-2.0", "author": { "name": "Ralf S. Engelschall", "url": "mailto:rse@engelschall.com" }, "devDependencies": { "gemstone": "0.9.34", "gemstone-tool": "0.9.39", "gemstone-tool-generator": "0.9.37", "gemstone-tool-frontend": "0.9.69", "gemstone-framework-frontend": "0.9.64", "shx": "0.3.1" }, "dependencies": { "deep-equal": "1.0.1", "vue-tippy": "2.0.17", "jquery.scrollto": "2.1.2", "minimatch": "3.0.4", "jquery": "3.3.1", "popper.js": "1.14.3", "typopro-web": "4.0.1", "font-awesome": "4.7.0", "devDependencies": { "npm-run-all": "4.1.3", "stmux": "1.5.5", "shx": "0.3.1" }, "scripts": { "install": "npm-run-all -s install:fe install:be", "install:fe": "cd fe && npm install", "install:be": "cd be && npm install", "build": "cd fe && npm run build", "start": "cd be && npm start", "clean": "npm-run-all -s clean:fe clean:be", "clean:fe": "cd fe && npm run clean", "clean:be": "cd be && npm run clean", "distclean": "npm-run-all -s distclean:fe distclean:be && shx rm -rf node_modules", "distclean:fe": "cd fe && npm run distclean", "distclean:be": "cd be && npm run distclean", "dev": "stmux -w always -e \"\\d+ error,!without code style errors\" -m beep,system -- [ \"npm run dev:fe\" : \"npm run dev:be\" ]", "dev:fe": "cd fe && npm run dev", "dev:be": "cd be && npm run dev" } }Chapter 8: Example2148.3.3 Front End: Application Configuration (fe/gemstone.yaml)The following is the GemstoneJS application configuration in YAML format. It speci-fies some meta-information for the GemstoneJS technology stack.8.3.4 Front End: Main Procedure (fe/src/main.js)The following is the Front-End main procedure in ECMAScript 2018 format. It con-tains the Front-End bootstrapping code. First, ➊ it imports third-party library code and style. Second, it integrates the third-party User Interface Widget Select2 ➋ and Perfect-Scrollbar ➌. Third, it setups the GraphQL-IO Back-End service communica-tion ➍. Forth, it finally bootstraps the GemstoneJS framework ➎.header: | UnP ~ Units and Persons (HUICA Example Application) Copyright 2018 (c) Ralf S. Engelschall <http://engelschall.com> Licensed under Apache-2.0 <https://spdx.org/licenses/Apache-2.0> meta: title: "UnP" description: "Units and Persons" author: "Ralf S. Engelschall" keywords: "units, persons, example" path: output: ./dst source: ./src main: ./src/main.js/* external imports */ ➊ import gs from "gemstone" import deepEqual from "deep-equal" import { Client } from "graphql-io-client" import "jquery-resizable-dom/dist/jquery-resizable.js" import "typopro-web/web/TypoPRO-SourceSansPro/TypoPRO-SourceSan-sPro.css" import "font-awesome/css/font-awesome.css" import Scrollbar from "perfect-scrollbar" import "perfect-scrollbar/css/perfect-scrollbar.css" import select2 from "select2/dist/js/select2.full.min.js" "select2": "4.0.5", "jquery-resizable-dom": "0.32.0", "graphql-io-client": "1.4.7", "perfect-scrollbar": "1.4.0" }, "upd": [ "!select2" ], "scripts": { "gemstone": "gemstone", "build": "gemstone frontend-build", "build:prod": "gemstone frontend-build env=production", "dev": "npm run build watch=true beep=true", "clean": "shx rm -rf dst", "distclean": "shx rm -rf dst node_modules" } }User Interface Implementation215import "select2/dist/css/select2.min.css" import "vue-tippy" import "jquery.scrollto" /* internal imports */ import Screen from "./screen" /* integrate Select2 and ComponentJS/ComponentJS-MVC */ ➋ select2(gs.$) gs.mvc.latch("mask:vue-result", ({ comp, id, mask }) => { /* for each Select2 usage in the masks... */ gs.$("select.select2", mask.$el).each((_, el) => { /* figure out the Select2 usage parameters */ let model = gs.$(el).data("model") let modelSource = gs.$(el).data("model-source") let multiple = !!(gs.$(el).attr("multiple")) /* observe the ComponentJS model */ comp.observe(model, (_, value) => { let allowed = comp.value(modelSource) if (multiple) { /* sanity check new ComponentJS model value */ value.forEach((id) => { if (allowed.findIndex((item) => item.id === id) < 0) throw new Error(`invalid id "${id}" in model field "${model}"`) }) /* update Select2 */ if (!deepEqual(gs.$(el).val(), value, { strict: true })) gs.$(el).val(value).trigger("change") } else { /* sanity check new ComponentJS model value */ if (value !== null && allowed.findIndex((item) => item.id === value) < 0) throw new Error(`invalid id "${value}" in model field "${model}"`) /* update Select2 */ if (value === null) value = "" if (gs.$(el).val() !== value) gs.$(el).val(value).trigger("change") } }) /* activate Select2 and update ComponentJS model */ gs.$(el) .select2({ width: "100%" }) .on("change", (ev) => { let model = gs.$(ev.target).data("model") let multiple = !!(gs.$(ev.target).attr("multiple")) let value = gs.$(ev.target).val() if (multiple && value.length === 1 && value[0] === "") value = [] else if (!multiple && value === "") value = null if (comp.value(model) !== value) comp.value(model, value) }) [...] }) Chapter 8: Example2168.3.5 Front End: Common Code (fe/src/common.js)The following is the common Front-End code in ECMAScript 2018 format. It mainly contains a utility class Bridge, for shuffling data between the Business Model (BM) in GraphQL-IO format and the Presentation Model (PM) in ComponentJS format ➊ and vice versa ➋.}) /* integrate PerfectScrollbar */ ➌ gs.mvc.latch("mask:vue-result", ({ comp, id, mask }) => { gs.$(".perfect-scrollbar", mask.$el).each((_, el) => { setTimeout(() => { void (new Scrollbar(el)) }, 1000) }) }) /* provide GraphQL-IO service client */ ➍ const sv = (url, cid) => { let client = new Client({ url: url.toString().replace(/\/$/, ""), encoding: "json", compress: true, throttle: 50, debug: 4 }) client.on("debug", ({ log }) => { /* eslint no-console: off */ console.log(`[SV]: ${log}`) }) return client } /* boot application via GemstoneJS framework */ ➎ gs.boot({ app: "unp", config: process.config, ui: () => [ "screen", Screen, "visible" ], sv: sv })import deepEqual from "deep-equal" export class Bridge { constructor (fields) { this.fields = fields } bm2pm (entity, comp) { ➊ if (entity === null || comp === null) return this.fields.forEach((field) => { if (field.type === "attr") { let value = entity[field.bm] if (comp.value(field.pm) !== value) comp.value(field.pm, value) } else if (field.type === "rel?") { let value = entity[field.bm] !== null ? entity[field.bm].id : null if (comp.value(field.pm) !== value) User Interface Implementation2178.3.6 Front End: Common Style (fe/src/common.css)The following is the common Front-End style in CSS4/PostCSS format. It mainly de-fines the color set ➊ and gives the input fields a distinct style ➋. comp.value(field.pm, value) } else if (field.type === "rel*") { let value = entity[field.bm].map((entity) => entity.id) if (!deepEqual(comp.value(field.pm), value)) comp.value(field.pm, value) } }) } pm2bm (comp, entity) { ➋ let changeset = {} if (entity === null || comp === null) return changeset this.fields.forEach((field) => { let value = comp.value(field.pm) if (field.type === "attr") { if (entity[field.bm] === value) return entity[field.bm] = value changeset[field.bm] = value } else if (field.type === "rel?") { if ( (entity[field.bm] === null && value === null) || (entity[field.bm] !== null && entity[field.bm].id === value)) return entity[field.bm] = { id: value } changeset[field.bm] = { set: value === null ? [] : [ value ] } } else if (field.type === "rel*") { let oldSet = entity[field.bm].map((x) => x.id) let newSet = value if (deepEqual(oldSet, newSet)) return let delSet = oldSet.filter((id) => newSet.indexOf(id) < 0) let addSet = newSet.filter((id) => oldSet.indexOf(id) < 0) entity[field.bm] = value.map((id) => ({ id: id })) changeset[field.bm] = {} if (delSet.length > 0) changeset[field.bm].del = delSet if (addSet.length > 0) changeset[field.bm].add = addSet } }) return changeset } }:root { ➊ --color-white: #ffffff; --color-grey-light: #e0e0e0; --color-brown-ray: #fcf4ed; --color-brown-glare: #f4dbc2; --color-brown-light: #e4a86b; --color-brown: #c67524; --color-brown-dark: #6f4214; --color-red: #81193a; --color-grey: #4d4d4d; --color-black: #222222; --font-family: "TypoPRO Source Sans Pro", sans-serif; Chapter 8: Example2188.3.7 Front End: Screen Components (fe/src/screen.js)The following are the Front-End Screen Components in ECMAScript 2018 format. They form the outer screen or window of the application. The most notable aspects are: First, ➊ it applies a third-party functionality onto the previously rendered User Interface View Mask. Second, ➋ it provides some “global” Presentation Model fields which all other Components can use. Third, ➌ it performs some automatic User Interface reset for the status bar. Fourth, ➍ it links the Back-End service connection status into the Presentation Model. Fifth, ➎ it performs the master/detail commu-nication between the Units and the Unit Dialog and between the Persons and the Person Dialog. --font-size: 11pt; } input.textfield { ➋ border: 1px solid var(--color-brown-light) !important; border-radius: 4px; height: 20px; font-size: 11pt; padding: 2px 8px 2px 8px; width: calc(100% - 18px); } input.textfield:focus { border: 1px solid var(--color-brown) !important; outline: none; } input.textfield::placeholder { color: var(--color-brown-light); font-family: var(--font-family); } div.label { height: 28px; padding-left: 10px; padding-right: 10px; border: 1px solid var(--color-brown-glare); background-color: var(--color-brown-glare); color: var(--color-black); border-top-left-radius: 4px; border-bottom-left-radius: 4px; display: flex; flex-direction: column; justify-content: center; white-space: nowrap; } .mask-overlay { z-index: 1000; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-color: var(--color-brown-glare); opacity: 0.5; } [...]User Interface Implementation219import { $, mvc, mousetrap } from "gemstone" import Units from "./units.js" import Unit from "./unit.js" import Persons from "./persons.js" import Person from "./person.js" import mask from "./screen.html" import i18n from "./screen.yaml" import "./screen.css" class View extends mvc.View { render () { /* render and attach the UI view mask fragment */ this.ui = this.mask("screen", { render: mask, i18n: i18n }) this.plug(this.ui) /* make screen resizable */ $(this.ui.$el).resizable({ ➊ handleSelector: ".resizeHandle", resizeWidth: true, resizeHeight: true, resizeWidthFrom: "right", resizeHeightFrom: "bottom", onDragStart: (e, $el, opt) => { $el.css("cursor", "nwse-resize") }, onDragEnd: (e, $el, opt) => { $el.css("cursor", "") }, onDrag: (e, $el, newWidth, newHeight, opt) => { if (newWidth < 600) newWidth = 600 if (newHeight < 400) newHeight = 400 $el.width(newWidth) $el.height(newHeight) return false } }) /* allow user to easily switch the UI language */ mousetrap.bind("ctrl+a l", () => { let lang = this.value("gsLang") this.value("gsLang", lang !== "en" ? "en" : "de") }) } } class Model extends mvc.Model { create () { /* define the presentation model */ ➋ this.model({ "dataSelectedUnit": { value: "", valid: "string" }, "dataSelectedPerson": { value: "", valid: "string" }, "dataScreenHint": { value: "", valid: "string" }, "dataOnlineStatus": { value: "offline", valid: "string" }, "dataOnlineProgress": { value: false, valid: "boolean" }, "dataOnlineUsers": { value: 0, valid: "number" } }) /* automatically clear the status model field after 5s */ ➌ this.timer = null this.observe("dataScreenHint", () => { if (this.timer !== null) clearTimeout(this.timer) this.timer = setTimeout(() => { this.value("dataScreenHint", "") }, 5 * 1000) }) Next >