11# Pluggable Widget Engine: 声明式 Widget 构建系统
22
33** Date** : 2026-03-25
4- ** Status** : Design (research only)
4+ ** Status** : Implemented
55
66## Problem
77
5656 "templateFile" : " combobox.json" ,
5757 "defaultEditable" : " Always" ,
5858
59- "modes" : {
60- "default" : {
61- "description" : " Enumeration mode" ,
62- "propertyMappings" : [
63- {
64- "propertyKey" : " attributeEnumeration" ,
65- "source" : " Attribute" ,
66- "operation" : " attribute"
67- }
68- ]
69- },
70- "association" : {
59+ "modes" : [
60+ {
61+ "name" : " association" ,
7162 "condition" : " hasDataSource" ,
7263 "description" : " Association mode with DataSource" ,
7364 "propertyMappings" : [
7667 "value" : " association" ,
7768 "operation" : " primitive"
7869 },
79- {
80- "propertyKey" : " attributeAssociation" ,
81- "source" : " Attribute" ,
82- "operation" : " association"
83- },
8470 {
8571 "propertyKey" : " optionsSourceAssociationDataSource" ,
8672 "source" : " DataSource" ,
8773 "operation" : " datasource"
8874 },
75+ {
76+ "propertyKey" : " attributeAssociation" ,
77+ "source" : " Attribute" ,
78+ "operation" : " association"
79+ },
8980 {
9081 "propertyKey" : " optionsSourceAssociationCaptionAttribute" ,
9182 "source" : " CaptionAttribute" ,
9283 "operation" : " attribute"
9384 }
9485 ]
86+ },
87+ {
88+ "name" : " default" ,
89+ "description" : " Enumeration mode" ,
90+ "propertyMappings" : [
91+ {
92+ "propertyKey" : " attributeEnumeration" ,
93+ "source" : " Attribute" ,
94+ "operation" : " attribute"
95+ }
96+ ]
9597 }
96- }
98+ ]
9799}
98100```
99101
@@ -105,33 +107,26 @@ Gallery (with child slots):
105107 "mdlName" : " GALLERY" ,
106108 "templateFile" : " gallery.json" ,
107109 "defaultEditable" : " Always" ,
108- "defaultSelection" : " Single" ,
109110
110111 "propertyMappings" : [
111- {
112- "propertyKey" : " datasource" ,
113- "source" : " DataSource" ,
114- "operation" : " datasource"
115- },
116- {
117- "propertyKey" : " itemSelection" ,
118- "source" : " Selection" ,
119- "operation" : " primitive" ,
120- "default" : " Single"
121- }
112+ {"propertyKey" : " advanced" , "value" : " false" , "operation" : " primitive" },
113+ {"propertyKey" : " datasource" , "source" : " DataSource" , "operation" : " datasource" },
114+ {"propertyKey" : " itemSelection" , "source" : " Selection" , "operation" : " selection" },
115+ {"propertyKey" : " itemSelectionMode" , "value" : " clear" , "operation" : " primitive" },
116+ {"propertyKey" : " desktopItems" , "value" : " 1" , "operation" : " primitive" },
117+ {"propertyKey" : " tabletItems" , "value" : " 1" , "operation" : " primitive" },
118+ {"propertyKey" : " phoneItems" , "value" : " 1" , "operation" : " primitive" },
119+ {"propertyKey" : " pageSize" , "value" : " 20" , "operation" : " primitive" },
120+ {"propertyKey" : " pagination" , "value" : " buttons" , "operation" : " primitive" },
121+ {"propertyKey" : " pagingPosition" , "value" : " below" , "operation" : " primitive" },
122+ {"propertyKey" : " showEmptyPlaceholder" , "value" : " none" , "operation" : " primitive" },
123+ {"propertyKey" : " onClickTrigger" , "value" : " single" , "operation" : " primitive" }
122124 ],
123125
124126 "childSlots" : [
125- {
126- "propertyKey" : " content" ,
127- "mdlContainer" : " TEMPLATE" ,
128- "operation" : " widgets"
129- },
130- {
131- "propertyKey" : " filtersPlaceholder" ,
132- "mdlContainer" : " FILTER" ,
133- "operation" : " widgets"
134- }
127+ {"propertyKey" : " content" , "mdlContainer" : " TEMPLATE" , "operation" : " widgets" },
128+ {"propertyKey" : " emptyPlaceholder" , "mdlContainer" : " EMPTYPLACEHOLDER" , "operation" : " widgets" },
129+ {"propertyKey" : " filtersPlaceholder" , "mdlContainer" : " FILTERSPLACEHOLDER" , "operation" : " widgets" }
135130 ]
136131}
137132```
@@ -192,6 +187,19 @@ type ChildSlotMapping struct {
192187}
193188```
194189
190+ ### Critical: Property Mapping Order Dependency
191+
192+ ** The engine processes ` propertyMappings ` in array order.** Some operations depend on side effects of earlier ones:
193+
194+ - ` datasource ` sets ` pageBuilder.entityContext ` as a side effect
195+ - ` association ` reads ` pageBuilder.entityContext ` to resolve the target entity
196+
197+ Therefore, in any mode that uses both, ** ` datasource ` must come before ` association ` ** in the mappings array. Getting this wrong produces silently incorrect BSON (wrong entity reference).
198+
199+ ### Operation Validation
200+
201+ Operation names in ` .def.json ` files are validated at load time against the 6 known operations: ` attribute ` , ` association ` , ` primitive ` , ` selection ` , ` datasource ` , ` widgets ` . Invalid operation names produce an error when ` NewWidgetRegistry() ` or ` LoadUserDefinitions() ` runs, rather than failing silently at build time.
202+
195203### Mode Selection Conditions
196204
197205Built-in conditions (extensible):
@@ -201,7 +209,7 @@ Built-in conditions (extensible):
201209| ` hasDataSource ` | ` w.GetDataSource() != nil ` |
202210| ` hasAttribute ` | ` w.GetAttribute() != "" ` |
203211| ` hasProp:X ` | ` w.GetStringProp("X") != "" ` |
204- | (none) | ` "default" ` mode always selected |
212+ | (none) | Fallback — first no-condition mode wins if multiple exist |
205213
206214### Engine Flow
207215
@@ -317,3 +325,6 @@ mxcli widget extract --mpk path/to/widget.mpk
317325| Template version drift | Existing ` augment.go ` handles .mpk sync, works unchanged |
318326| Performance regression | Template loading is already cached; engine adds minimal overhead |
319327| User-provided templates may be invalid | Validate on load: check type+object sections exist, PropertyKey coverage |
328+ | MPK zip-bomb attack | ` ParseMPK ` enforces per-file (50MB) and total (200MB) extraction limits |
329+ | Invalid operation names in .def.json | Validated at load time, not build time — immediate feedback |
330+ | Engine init failure retried on every widget | Init error cached; subsequent widgets skip immediately |
0 commit comments