@siliconst/metaui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -0
- package/package.json +35 -0
- package/src/nz/co/siliconst/ui5/metaui/.library +7 -0
- package/src/nz/co/siliconst/ui5/metaui/controls/GeneratorHost.ts +162 -0
- package/src/nz/co/siliconst/ui5/metaui/core/ConditionEngine.ts +40 -0
- package/src/nz/co/siliconst/ui5/metaui/core/Engine.ts +56 -0
- package/src/nz/co/siliconst/ui5/metaui/core/EventBus.ts +45 -0
- package/src/nz/co/siliconst/ui5/metaui/core/PluginRegistry.ts +56 -0
- package/src/nz/co/siliconst/ui5/metaui/core/SchemaNormalizer.ts +31 -0
- package/src/nz/co/siliconst/ui5/metaui/core/StateManager.ts +56 -0
- package/src/nz/co/siliconst/ui5/metaui/interfaces/IEventBus.ts +29 -0
- package/src/nz/co/siliconst/ui5/metaui/interfaces/ILayoutManager.ts +26 -0
- package/src/nz/co/siliconst/ui5/metaui/interfaces/IPlugin.ts +33 -0
- package/src/nz/co/siliconst/ui5/metaui/interfaces/ISchema.ts +70 -0
- package/src/nz/co/siliconst/ui5/metaui/layouts/BaseLayout.ts +34 -0
- package/src/nz/co/siliconst/ui5/metaui/layouts/FormLayout.ts +60 -0
- package/src/nz/co/siliconst/ui5/metaui/layouts/TableLayout.ts +102 -0
- package/src/nz/co/siliconst/ui5/metaui/library.ts +43 -0
- package/src/nz/co/siliconst/ui5/metaui/plugins/BasePlugin.ts +58 -0
- package/src/nz/co/siliconst/ui5/metaui/plugins/BooleanPlugin.ts +42 -0
- package/src/nz/co/siliconst/ui5/metaui/plugins/DatePlugin.ts +63 -0
- package/src/nz/co/siliconst/ui5/metaui/plugins/DropdownPlugin.ts +65 -0
- package/src/nz/co/siliconst/ui5/metaui/plugins/NumberPlugin.ts +55 -0
- package/src/nz/co/siliconst/ui5/metaui/plugins/StringPlugin.ts +55 -0
- package/ui5.yaml +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# MetaUI - Dynamic SAP UI5 Rendering Engine
|
|
2
|
+
|
|
3
|
+
MetaUI is an UI5 library that dynamically generates fully accessible, Fiori-compliant forms and tables purely from JSON schemas. It utilizes an **Isolated State Pattern** to entirely decouple from host OData bindings, yielding a clean JSON payload upon submission.
|
|
4
|
+
|
|
5
|
+
## 🏗️ Architecture
|
|
6
|
+
1. **Standard UI5**: Relies strictly on native UI5 layout grids (`sap.ui.layout.form.SimpleForm`, `sap.m.Table`).
|
|
7
|
+
2. **Schema Normalization**: Ingests ABAP RTTI JSON payloads and normalizes them into a strict `ISchema` layout matrix.
|
|
8
|
+
3. **Plugin Registry**: Maps primitive schema types (`string`, `number`, `boolean`, `dropdown`, `date`) directly to discrete UI5 control plugins (`sap.m.Input`, `sap.m.StepInput`, etc.).
|
|
9
|
+
|
|
10
|
+
## 📚 Documentation Wiki
|
|
11
|
+
Full payload references and API details are tracked in Git under the `docs/` folder:
|
|
12
|
+
* [Payload Formats & Schema API](docs/payload_formats.md)
|
|
13
|
+
|
|
14
|
+
## 🚀 How to Test the Sandbox
|
|
15
|
+
|
|
16
|
+
To understand how the engine works, a standalone test sandbox has been provided. It asynchronously loads two physical files:
|
|
17
|
+
* `test/mockSchema.json` - The schema definition detailing field types, required states, and dropdown arrays.
|
|
18
|
+
* `test/mockData.json` - The initial state payload to populate the form.
|
|
19
|
+
|
|
20
|
+
### Running the Test
|
|
21
|
+
You **cannot** run this test by double-clicking the `index.html` file due to browser CORS restrictions blocking `fetch()`. You must run it via a local web server.
|
|
22
|
+
|
|
23
|
+
1. Open a terminal in the root `MetaUI` project folder.
|
|
24
|
+
2. Run a static file server using Node.js:
|
|
25
|
+
```bash
|
|
26
|
+
npx serve -p 8080 .
|
|
27
|
+
```
|
|
28
|
+
3. Open your web browser and navigate to:
|
|
29
|
+
`http://localhost:8080/test/index.html`
|
|
30
|
+
4. Click **"Open MetaUI Dialog"**.
|
|
31
|
+
5. Modify the values in the form. Notice the built-in validation rules (e.g., required fields, dropdown selections).
|
|
32
|
+
6. Click **Save**. The clean, extracted JSON payload will be printed directly to the text area.
|
|
33
|
+
|
|
34
|
+
## 📦 Installation (NPM)
|
|
35
|
+
|
|
36
|
+
If you are developing a modern UI5 application utilizing `ui5-tooling`, you can pull this engine in directly via NPM (once published to your internal registry):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install @siliconst/ui5-metaui
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 1. ui5.yaml Configuration
|
|
43
|
+
Ensure your application consumes the library by adding it to your `ui5.yaml` dependencies:
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
specVersion: "3.0"
|
|
47
|
+
metadata:
|
|
48
|
+
name: my.custom.app
|
|
49
|
+
type: application
|
|
50
|
+
builder:
|
|
51
|
+
customTasks:
|
|
52
|
+
- name: ui5-tooling-transpile-task
|
|
53
|
+
afterTask: replaceVersion
|
|
54
|
+
---
|
|
55
|
+
specVersion: "3.0"
|
|
56
|
+
kind: extension
|
|
57
|
+
type: project-shim
|
|
58
|
+
metadata:
|
|
59
|
+
name: my.custom.app.shim
|
|
60
|
+
shims:
|
|
61
|
+
configurations:
|
|
62
|
+
"@siliconst/ui5-metaui":
|
|
63
|
+
type: library
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2. manifest.json Registration
|
|
67
|
+
You must explicitly declare the library dependency in your application's `manifest.json` so the UI5 core automatically downloads the runtime files and triggers the internal `PluginRegistry` bootstrapper.
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
"sap.ui5": {
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"minUI5Version": "1.120.0",
|
|
73
|
+
"libs": {
|
|
74
|
+
"sap.m": {},
|
|
75
|
+
"sap.ui.core": {},
|
|
76
|
+
"nz.co.siliconst.ui5.metaui": {}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 💻 Instantiating the Engine in your Controller
|
|
83
|
+
|
|
84
|
+
Once installed and registered, simply require the `GeneratorHost` control in your view controllers:
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
sap.ui.define([
|
|
88
|
+
"sap/ui/core/mvc/Controller",
|
|
89
|
+
"nz/co/siliconst/ui5/metaui/controls/GeneratorHost"
|
|
90
|
+
], function(Controller, GeneratorHost) {
|
|
91
|
+
return Controller.extend("my.app.controller.Main", {
|
|
92
|
+
|
|
93
|
+
openDynamicForm: function(schemaPayload, dataPayload) {
|
|
94
|
+
const host = new GeneratorHost({
|
|
95
|
+
schemaDefinition: schemaPayload, // The RTTI schema from ABAP
|
|
96
|
+
initialData: dataPayload, // Your isolated JSON payload
|
|
97
|
+
submit: (oEvent) => {
|
|
98
|
+
const finalJsonPayload = oEvent.getParameter("payload");
|
|
99
|
+
// Send finalJsonPayload back to SAP ABAP via your REST/OData API!
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
host.openInDialog("Edit Customer");
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 🔗 Declarative XML Model Binding
|
|
110
|
+
Because `GeneratorHost` extends `sap.ui.core.Control`, you can also embed it directly into your XML Views and bind standard UI5 Models (JSON, OData) natively to its properties.
|
|
111
|
+
|
|
112
|
+
First, add the namespace to your XML View:
|
|
113
|
+
```xml
|
|
114
|
+
xmlns:meta="nz.co.siliconst.ui5.metaui.controls"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Then bind your host application's models directly to the control:
|
|
118
|
+
```xml
|
|
119
|
+
<meta:GeneratorHost
|
|
120
|
+
schemaDefinition="{schemaModel>/CustomerSchema}"
|
|
121
|
+
initialData="{odataModel>/Customers('1001')}" />
|
|
122
|
+
```
|
|
123
|
+
*Note: The engine will safely clone the incoming `initialData` into its own isolated state, preventing any unwanted two-way binding bleed into your host OData models until you explicitly catch the `submit` event!*
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@siliconst/metaui",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Dynamic Metadata UI5 Engine",
|
|
5
|
+
"author": "SiliconStreetDev1",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/SiliconStreetDev1/MetaUI.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"ui5",
|
|
13
|
+
"metadata",
|
|
14
|
+
"engine"
|
|
15
|
+
],
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"ui5.yaml",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "ui5 build --clean-dest"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/openui5": "^1.120.0",
|
|
26
|
+
"@ui5/cli": "^3.0.0",
|
|
27
|
+
"typescript": "^5.3.0",
|
|
28
|
+
"ui5-tooling-transpile": "^3.2.0"
|
|
29
|
+
},
|
|
30
|
+
"ui5": {
|
|
31
|
+
"dependencies": [
|
|
32
|
+
"ui5-tooling-transpile"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file GeneratorHost.ts
|
|
3
|
+
* @description The core UI5 custom control that exposes the MetaUI engine to host applications.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Control from "sap/ui/core/Control";
|
|
7
|
+
import RenderManager from "sap/ui/core/RenderManager";
|
|
8
|
+
import Dialog from "sap/m/Dialog";
|
|
9
|
+
import Button from "sap/m/Button";
|
|
10
|
+
import { SchemaNormalizer } from "../core/SchemaNormalizer";
|
|
11
|
+
import { Engine } from "../core/Engine";
|
|
12
|
+
import { StateManager } from "../core/StateManager";
|
|
13
|
+
import { BaseLayout } from "../layouts/BaseLayout";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Enterprise wrapper element for embedding or popping the dynamic form.
|
|
17
|
+
*
|
|
18
|
+
* @namespace nz.co.siliconst.ui5.metaui.controls
|
|
19
|
+
*/
|
|
20
|
+
export default class GeneratorHost extends Control {
|
|
21
|
+
|
|
22
|
+
private stateManager: StateManager | null = null;
|
|
23
|
+
private generatedContent: Control | null = null;
|
|
24
|
+
|
|
25
|
+
static readonly metadata = {
|
|
26
|
+
properties: {
|
|
27
|
+
/** The raw JSON metadata schema provided by the ABAP backend. */
|
|
28
|
+
schemaDefinition: { type: "object", defaultValue: null },
|
|
29
|
+
/** The initial data payload to populate the fields. */
|
|
30
|
+
initialData: { type: "object", defaultValue: {} }
|
|
31
|
+
},
|
|
32
|
+
aggregations: {
|
|
33
|
+
/** The internal hidden container holding the generated layouts. */
|
|
34
|
+
_content: { type: "sap.ui.core.Control", multiple: false, visibility: "hidden" }
|
|
35
|
+
},
|
|
36
|
+
events: {
|
|
37
|
+
/** Triggered when the user submits the form via the Dialog modality. */
|
|
38
|
+
submit: {
|
|
39
|
+
parameters: {
|
|
40
|
+
/** The clean extracted JSON payload representing the current state. */
|
|
41
|
+
payload: { type: "object" }
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
/** Triggered if the dialog is cancelled. */
|
|
45
|
+
cancel: {}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Standard UI5 control renderer. Renders the generated internal content.
|
|
51
|
+
*/
|
|
52
|
+
static renderer = {
|
|
53
|
+
apiVersion: 2,
|
|
54
|
+
render(rm: RenderManager, control: GeneratorHost) {
|
|
55
|
+
rm.openStart("div", control);
|
|
56
|
+
rm.style("width", "100%");
|
|
57
|
+
rm.style("height", "100%");
|
|
58
|
+
rm.openEnd();
|
|
59
|
+
|
|
60
|
+
const content = control.getAggregation("_content") as Control;
|
|
61
|
+
if (content) {
|
|
62
|
+
rm.renderControl(content);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
rm.close("div");
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Initializes the UI5 control.
|
|
71
|
+
*/
|
|
72
|
+
public init(): void {
|
|
73
|
+
super.init();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Lifecycle hook to automatically generate the layout if bound declaratively in XML.
|
|
78
|
+
*/
|
|
79
|
+
public onBeforeRendering(): void {
|
|
80
|
+
if (!this.generatedContent && this.getProperty("schemaDefinition")) {
|
|
81
|
+
this.generate();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extracts the payload and fires the submit event manually. Useful for inline XML embedding.
|
|
87
|
+
*/
|
|
88
|
+
public triggerSubmit(): void {
|
|
89
|
+
if (this.stateManager) {
|
|
90
|
+
const payload = this.stateManager.extractPayload();
|
|
91
|
+
this.fireEvent("submit", { payload });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Triggers the engine to parse the schema and build the layout.
|
|
97
|
+
* Must be called manually or hooked into onBeforeRendering if properties are bound.
|
|
98
|
+
*/
|
|
99
|
+
public generate(): void {
|
|
100
|
+
const rawSchema = this.getProperty("schemaDefinition");
|
|
101
|
+
const initialData = this.getProperty("initialData");
|
|
102
|
+
|
|
103
|
+
if (!rawSchema) {
|
|
104
|
+
throw new Error("[MetaUI] Cannot generate layout without a schemaDefinition.");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 1. Normalize schema
|
|
108
|
+
const normalizedSchema = SchemaNormalizer.normalize(rawSchema);
|
|
109
|
+
|
|
110
|
+
// 2. Bootstrap isolated State Manager
|
|
111
|
+
this.stateManager = new StateManager(initialData);
|
|
112
|
+
this.setModel(this.stateManager.getModel(), "meta");
|
|
113
|
+
|
|
114
|
+
// 3. Delegate to Layout Engine
|
|
115
|
+
const engine = new Engine();
|
|
116
|
+
const layoutFactory = new BaseLayout();
|
|
117
|
+
|
|
118
|
+
this.generatedContent = engine.build(normalizedSchema, layoutFactory);
|
|
119
|
+
|
|
120
|
+
// 4. Mount into hidden aggregation
|
|
121
|
+
this.setAggregation("_content", this.generatedContent);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Dialog Modality: Opens the generated layout inside a sap.m.Dialog.
|
|
126
|
+
* Automatically wires up 'Save' and 'Cancel' buttons.
|
|
127
|
+
*/
|
|
128
|
+
public openInDialog(title: string = "Dynamic Form", submitButtonText: string = "Save"): void {
|
|
129
|
+
if (!this.generatedContent) {
|
|
130
|
+
this.generate();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const dialog = new Dialog({
|
|
134
|
+
title: title,
|
|
135
|
+
contentWidth: "800px",
|
|
136
|
+
content: [this], // Mount self inside the dialog
|
|
137
|
+
beginButton: new Button({
|
|
138
|
+
text: submitButtonText,
|
|
139
|
+
type: "Emphasized",
|
|
140
|
+
press: () => {
|
|
141
|
+
if (this.stateManager) {
|
|
142
|
+
const payload = this.stateManager.extractPayload();
|
|
143
|
+
this.fireEvent("submit", { payload });
|
|
144
|
+
}
|
|
145
|
+
dialog.close();
|
|
146
|
+
}
|
|
147
|
+
}),
|
|
148
|
+
endButton: new Button({
|
|
149
|
+
text: "Cancel",
|
|
150
|
+
press: () => {
|
|
151
|
+
this.fireEvent("cancel");
|
|
152
|
+
dialog.close();
|
|
153
|
+
}
|
|
154
|
+
}),
|
|
155
|
+
afterClose: () => {
|
|
156
|
+
dialog.destroy();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
dialog.open();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file ConditionEngine.ts
|
|
3
|
+
* @description Evaluates dynamic rules to alter the schema in real-time.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EventBus } from "./EventBus";
|
|
7
|
+
import { ISchema } from "../interfaces/ISchema";
|
|
8
|
+
import { IFieldChangeEvent } from "../interfaces/IEventBus";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Engine that listens to the EventBus and evaluates cross-field dependencies.
|
|
12
|
+
*/
|
|
13
|
+
export class ConditionEngine {
|
|
14
|
+
private schema: ISchema;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initializes the ConditionEngine with the active schema.
|
|
18
|
+
* @param schema The current UI layout schema.
|
|
19
|
+
*/
|
|
20
|
+
constructor(schema: ISchema) {
|
|
21
|
+
this.schema = schema;
|
|
22
|
+
this.initializeListeners();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Subscribes to the EventBus for real-time field change interception.
|
|
27
|
+
*/
|
|
28
|
+
private initializeListeners(): void {
|
|
29
|
+
EventBus.getInstance().subscribe(this.handleEvent.bind(this));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Internal handler to evaluate business rules when a field is updated.
|
|
34
|
+
* @param event The structured change event.
|
|
35
|
+
*/
|
|
36
|
+
private handleEvent(event: IFieldChangeEvent): void {
|
|
37
|
+
// Architecture hook: Logic to evaluate conditions against the schema
|
|
38
|
+
// and update plugin properties will be injected here in later phases.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Engine.ts
|
|
3
|
+
* @description The central orchestrator that routes data between the Layout Factory and Plugin Registry.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ISchema } from "../interfaces/ISchema";
|
|
7
|
+
import { ILayoutManager } from "../interfaces/ILayoutManager";
|
|
8
|
+
import { ConditionEngine } from "./ConditionEngine";
|
|
9
|
+
import Control from "sap/ui/core/Control";
|
|
10
|
+
import VBox from "sap/m/VBox";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The MetaUI Engine. Responsible for bootstrapping the layout generation.
|
|
14
|
+
* Follows strict Orchestrator patterns; delegates all actual rendering to LayoutManagers.
|
|
15
|
+
*/
|
|
16
|
+
export class Engine {
|
|
17
|
+
private conditionEngine: ConditionEngine | null = null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Evaluates the schema and delegates generation to the layout factory.
|
|
21
|
+
* @param schema The fully normalized schema definition.
|
|
22
|
+
* @param layoutManager The injected layout factory class.
|
|
23
|
+
* @returns The populated UI5 layout container.
|
|
24
|
+
*/
|
|
25
|
+
public build(schema: ISchema, layoutManager: ILayoutManager): Control {
|
|
26
|
+
// Bootstrap the condition engine to start listening for field events in this schema
|
|
27
|
+
this.conditionEngine = new ConditionEngine(schema);
|
|
28
|
+
|
|
29
|
+
if (schema.mode === "form") {
|
|
30
|
+
return layoutManager.renderForm(schema);
|
|
31
|
+
} else if (schema.mode === "table") {
|
|
32
|
+
if (!schema.tables || schema.tables.length === 0) {
|
|
33
|
+
throw new Error("[MetaUI] Table mode requires at least one table definition.");
|
|
34
|
+
}
|
|
35
|
+
return layoutManager.renderTable(schema.tables[0]);
|
|
36
|
+
} else if (schema.mode === "mixed") {
|
|
37
|
+
const container = new VBox({ renderType: "Bare" });
|
|
38
|
+
|
|
39
|
+
// 1. Render the top-level form if there are root fields
|
|
40
|
+
if (schema.rootFields && schema.rootFields.length > 0) {
|
|
41
|
+
container.addItem(layoutManager.renderForm(schema));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 2. Render all tables sequentially below the form
|
|
45
|
+
if (schema.tables && schema.tables.length > 0) {
|
|
46
|
+
schema.tables.forEach(tableMeta => {
|
|
47
|
+
container.addItem(layoutManager.renderTable(tableMeta));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return container;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw new Error(`[MetaUI] Mode '${schema.mode}' is not yet implemented or invalid.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file EventBus.ts
|
|
3
|
+
* @description Internal pub/sub channel for managing cross-field dependencies.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IEventBus, IFieldChangeEvent } from "../interfaces/IEventBus";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Singleton orchestrator for localized state changes across the generated UI layout.
|
|
10
|
+
*/
|
|
11
|
+
export class EventBus implements IEventBus {
|
|
12
|
+
private static instance: EventBus;
|
|
13
|
+
private listeners: Array<(event: IFieldChangeEvent) => void> = [];
|
|
14
|
+
|
|
15
|
+
private constructor() {
|
|
16
|
+
// Enforce singleton
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Retrieves the singular instance of the EventBus.
|
|
21
|
+
* @returns The EventBus singleton.
|
|
22
|
+
*/
|
|
23
|
+
public static getInstance(): EventBus {
|
|
24
|
+
if (!EventBus.instance) {
|
|
25
|
+
EventBus.instance = new EventBus();
|
|
26
|
+
}
|
|
27
|
+
return EventBus.instance;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Publishes a value change event to all internal subscribers.
|
|
32
|
+
* @param event The structured payload of the field change.
|
|
33
|
+
*/
|
|
34
|
+
public publishFieldChange(event: IFieldChangeEvent): void {
|
|
35
|
+
this.listeners.forEach(listener => listener(event));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Registers a callback listener to react to field changes.
|
|
40
|
+
* @param listener The callback function.
|
|
41
|
+
*/
|
|
42
|
+
public subscribe(listener: (event: IFieldChangeEvent) => void): void {
|
|
43
|
+
this.listeners.push(listener);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file PluginRegistry.ts
|
|
3
|
+
* @description Centralized hub for mapping UI5 controls to SAP schema data types.
|
|
4
|
+
* Enforces the Registry Pattern to decouple the core engine from specific UI5 control implementations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { IPlugin } from "../interfaces/IPlugin";
|
|
8
|
+
import { FieldType } from "../interfaces/ISchema";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Singleton registry that manages the lifecycle and lookup of UI5 plugin constructors.
|
|
12
|
+
*/
|
|
13
|
+
export class PluginRegistry {
|
|
14
|
+
private static instance: PluginRegistry;
|
|
15
|
+
|
|
16
|
+
/** Internal map linking string types to their respective constructor functions. */
|
|
17
|
+
private plugins: Map<FieldType, new () => IPlugin> = new Map();
|
|
18
|
+
|
|
19
|
+
private constructor() {
|
|
20
|
+
// Enforce singleton
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Retrieves the singular instance of the PluginRegistry.
|
|
25
|
+
* @returns The PluginRegistry singleton.
|
|
26
|
+
*/
|
|
27
|
+
public static getInstance(): PluginRegistry {
|
|
28
|
+
if (!PluginRegistry.instance) {
|
|
29
|
+
PluginRegistry.instance = new PluginRegistry();
|
|
30
|
+
}
|
|
31
|
+
return PluginRegistry.instance;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Registers a new UI control plugin against a specific schema primitive type.
|
|
36
|
+
* @param type The primitive FieldType (e.g., 'string', 'date').
|
|
37
|
+
* @param pluginConstructor The class constructor for the plugin.
|
|
38
|
+
*/
|
|
39
|
+
public register(type: FieldType, pluginConstructor: new () => IPlugin): void {
|
|
40
|
+
this.plugins.set(type, pluginConstructor);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Instantiates and returns a new plugin based on the requested primitive type.
|
|
45
|
+
* @param type The primitive FieldType to look up.
|
|
46
|
+
* @throws Error if no plugin has been registered for the provided type.
|
|
47
|
+
* @returns A newly instantiated plugin ready for rendering.
|
|
48
|
+
*/
|
|
49
|
+
public getPlugin(type: FieldType): IPlugin {
|
|
50
|
+
const PluginClass = this.plugins.get(type);
|
|
51
|
+
if (!PluginClass) {
|
|
52
|
+
throw new Error(`[MetaUI] No plugin registered for FieldType: ${type}`);
|
|
53
|
+
}
|
|
54
|
+
return new PluginClass();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file SchemaNormalizer.ts
|
|
3
|
+
* @description Transforms and validates incoming JSON payloads into the strict ISchema matrix.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ISchema } from "../interfaces/ISchema";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Utility class ensuring data fidelity before layout construction begins.
|
|
10
|
+
*/
|
|
11
|
+
export class SchemaNormalizer {
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validates that the provided raw payload conforms to the required ISchema structures.
|
|
15
|
+
* @param rawSchema The heterogeneous JSON object provided to the Engine.
|
|
16
|
+
* @returns The strictly cast ISchema matrix.
|
|
17
|
+
* @throws Error if the mandatory schema properties are missing.
|
|
18
|
+
*/
|
|
19
|
+
public static normalize(rawSchema: any): ISchema {
|
|
20
|
+
if (!rawSchema || typeof rawSchema !== 'object') {
|
|
21
|
+
throw new Error("[MetaUI] Invalid Schema Payload: Payload is not an object.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!rawSchema.mode || !rawSchema.rootFields || !rawSchema.tables) {
|
|
25
|
+
throw new Error("[MetaUI] Invalid Schema Payload: Missing required structures (mode, rootFields, tables).");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Return the strongly typed and validated schema block.
|
|
29
|
+
return rawSchema as ISchema;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file StateManager.ts
|
|
3
|
+
* @description Isolated internal state management. Decouples the MetaUI engine from external host models.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import JSONModel from "sap/ui/model/json/JSONModel";
|
|
7
|
+
import ManagedObject from "sap/ui/base/ManagedObject";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Manages the isolated internal data payload for the MetaUI engine.
|
|
11
|
+
*/
|
|
12
|
+
export class StateManager {
|
|
13
|
+
private model: JSONModel;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Initializes the state manager with an optional initial payload.
|
|
17
|
+
* @param initialData The initial JSON payload to populate the fields.
|
|
18
|
+
*/
|
|
19
|
+
constructor(initialData: Record<string, any> = {}) {
|
|
20
|
+
// Deep copy to ensure no reference leakage from the host application
|
|
21
|
+
const safeData = JSON.parse(JSON.stringify(initialData));
|
|
22
|
+
this.model = new JSONModel(safeData);
|
|
23
|
+
this.model.setDefaultBindingMode("TwoWay");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Retrieves the isolated JSONModel instance to attach to the root GeneratorHost control.
|
|
28
|
+
* @returns The internal JSONModel.
|
|
29
|
+
*/
|
|
30
|
+
public getModel(): JSONModel {
|
|
31
|
+
return this.model;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Binds a generated UI control property to a path in the internal model.
|
|
36
|
+
* @param control The target UI5 Control.
|
|
37
|
+
* @param property The UI5 property name (e.g., 'value').
|
|
38
|
+
* @param bindingPath The exact path in the JSON payload.
|
|
39
|
+
*/
|
|
40
|
+
public bindControl(control: ManagedObject, property: string, bindingPath: string): void {
|
|
41
|
+
control.bindProperty(property, {
|
|
42
|
+
path: bindingPath,
|
|
43
|
+
model: "meta" // Assumes the model is bound to the root container with the name 'meta'
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extracts the flat JavaScript object representing the current state of the UI.
|
|
49
|
+
* @returns A serialized record of the data payload, free of UI5 wrappers.
|
|
50
|
+
*/
|
|
51
|
+
public extractPayload(): Record<string, any> {
|
|
52
|
+
// Create a deep copy to prevent accidental mutations by the host
|
|
53
|
+
const data = this.model.getData();
|
|
54
|
+
return JSON.parse(JSON.stringify(data));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file IEventBus.ts
|
|
3
|
+
* @description Pub/Sub interface for localized cross-field communication and state logic.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Describes a state change payload fired by a plugin.
|
|
8
|
+
*/
|
|
9
|
+
export interface IFieldChangeEvent {
|
|
10
|
+
fieldName: string;
|
|
11
|
+
newValue: any;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Contract for the internal event orchestrator to manage conditional UI logic.
|
|
16
|
+
*/
|
|
17
|
+
export interface IEventBus {
|
|
18
|
+
/**
|
|
19
|
+
* Publishes a value change event to the internal channel.
|
|
20
|
+
* @param event The structured event detailing which field changed and its new value.
|
|
21
|
+
*/
|
|
22
|
+
publishFieldChange(event: IFieldChangeEvent): void;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Subscribes a listener to react to field value changes.
|
|
26
|
+
* @param listener The callback function to execute when a change occurs.
|
|
27
|
+
*/
|
|
28
|
+
subscribe(listener: (event: IFieldChangeEvent) => void): void;
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file ILayoutManager.ts
|
|
3
|
+
* @description Factory contract for generating UI5 layout containers (Forms/Tables).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Control from "sap/ui/core/Control";
|
|
7
|
+
import { ISchema, ITableMetadata } from "./ISchema";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The factory interface responsible for structuring the visual grid.
|
|
11
|
+
*/
|
|
12
|
+
export interface ILayoutManager {
|
|
13
|
+
/**
|
|
14
|
+
* Constructs a responsive form layout container utilizing native UI5 grids.
|
|
15
|
+
* @param schema The master schema containing rootFields to be mounted.
|
|
16
|
+
* @returns A populated UI5 container (e.g., sap.ui.layout.form.SimpleForm).
|
|
17
|
+
*/
|
|
18
|
+
renderForm(schema: ISchema): Control;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Constructs an analytical or responsive table layout.
|
|
22
|
+
* @param tableMeta The metadata defining the table columns.
|
|
23
|
+
* @returns A UI5 Table control (e.g., sap.m.Table or sap.ui.table.Table).
|
|
24
|
+
*/
|
|
25
|
+
renderTable(tableMeta: ITableMetadata): Control;
|
|
26
|
+
}
|