Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions .claude/skills/mendix/alter-page/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,21 +470,27 @@ Property names resolve against the keys the installed widget declares, so both
the schema key and mxcli's MDL alias work (`DynamicCellClass` and `ColumnClass`
both reach `columnClass`). An unknown name lists what *is* settable on that grid.

**Expression-valued properties take a Mendix expression, not a literal.**
`DynamicCellClass` and `Visible` are expressions, so a literal CSS class has to be
a quoted string *inside* the expression — doubled quotes in MDL:
**`DynamicCellClass` (and a widget's `DynamicClasses`) take a Mendix expression,
written as-is.** A quoted value is a Mendix string, so a literal CSS class is just
the quoted class name, and a computed one is the expression itself:

```mdl
-- WRONG: the expression becomes a bare identifier, mxbuild reports CE0117
-- a literal class: the string 'highlight'
alter page Mod.P { SET DynamicCellClass = 'highlight' ON dg1.Label }

-- correct: the expression is the string literal 'highlight'
alter page Mod.P { SET DynamicCellClass = '''highlight''' ON dg1.Label }
-- a computed class
alter page Mod.P { SET DynamicCellClass = if $currentObject/Price > 100 then 'highlight' else '' ON dg1.Label }

-- WRONG: a bare name is an identifier, not a string — mxbuild reports CE0117
alter page Mod.P { SET DynamicCellClass = highlight ON dg1.Label }
```

This applies equally to `create page`; the two paths behave identically. A bare
identifier is not a valid Mendix expression, and mxbuild reports CE0117 against
the column.
The old spelling — the expression's text in quotes, `'if … then ''a'' else '''''`
— is refused as MDL-WIDGET33, because it would now store that text as a class
name. This applies equally to `create page`; the two paths behave identically.

A column's pluggable `Visible` expression is not converted yet: there a quoted
value is still the expression's text, so a literal needs the doubled quotes.

Properties holding a **structured** value — `attribute`, `filter`, `content`,
actions — cannot be set by ALTER at all. It refuses them and points at
Expand Down
17 changes: 10 additions & 7 deletions .claude/skills/mendix/create-page/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,26 +134,29 @@ actionbutton btn (caption: 'Save', designproperties: ['Size': 'Large', 'Full wid
```

**Dynamic Classes** — a Mendix expression evaluated at runtime that returns a
class list (applied on top of the static `class`). Root attributes in
`$currentObject` and escape single quotes by doubling them (`''`):
class list (applied on top of the static `class`). Write the expression as-is —
no outer quotes, no doubled ones — and root attributes in `$currentObject`. A
quoted value is a Mendix string: `dynamicclasses: 'is-featured'` is the class
`is-featured`.
```sql
dynamictext ovChip (
content: 'chip',
class: 'ss-chip',
dynamicclasses: 'if $currentObject/VesselClass = Mod.BoatClass.Astute then ''ss-chip--astute'' else '''''
dynamicclasses: if $currentObject/VesselClass = Mod.BoatClass.Astute then 'ss-chip--astute' else ''
)
```

Write it quoted, not in brackets: `dynamicclasses: [ … ]` (and a column's
`DynamicCellClass: [ … ]`) parses as a list, which no writer reads — `check`
reports it as MDL-WIDGET32 rather than letting the value be dropped.
Not in brackets: `dynamicclasses: [ … ]` (and a column's `DynamicCellClass: [ … ]`)
parses as a list, which no writer reads — `check` reports it as MDL-WIDGET32. And
not the old quoted spelling `'if … then ''a'' else '''''`, which would now store
the expression's text as a class name — `check` reports it as MDL-WIDGET33.

**All can be combined on a single widget:**
```sql
container ctnHero (
class: 'card',
style: 'border-left: 4px solid #264AE5;',
dynamicclasses: 'if $currentObject/Featured then ''is-featured'' else ''''',
dynamicclasses: if $currentObject/Featured then 'is-featured' else '',
designproperties: ['Spacing top': 'Large', 'Full width': on]
) {
dynamictext txtTitle (content: 'Styled Container', rendermode: H3)
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/mendix/create-page/reference/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ column colPrice (
Sortable: false, Resizable: false,
Hidable: hidden,
ColumnWidth: manual, Size: 150,
DynamicCellClass: 'if($currentObject/Price > 100) then ''highlight'' else '''' ',
DynamicCellClass: if($currentObject/Price > 100) then 'highlight' else '' ,
tooltip: 'Price in USD'
)
```
Expand Down
10 changes: 5 additions & 5 deletions .claude/skills/mendix/migrate-design-prototype/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -501,13 +501,13 @@ top of** `Class:`.
```sql
container heatCell (
Class: 'ss-heat-cell',
DynamicClasses: 'if $currentObject/M01 >= 100 then ''ss-heat--over''
else if $currentObject/M01 >= 80 then ''ss-heat--warn''
else ''ss-heat--ok'''
DynamicClasses: if $currentObject/M01 >= 100 then 'ss-heat--over'
else if $currentObject/M01 >= 80 then 'ss-heat--warn'
else 'ss-heat--ok'
)
```

(Note the doubled single-quotes for string literals inside an MDL expression.)
(Written as-is: plain single quotes inside, no outer quotes around the expression.)

### Computed dimensions — the bucket-class idiom

Expand All @@ -526,7 +526,7 @@ bucket and generate one class per bucket**:
```

3. Select the class from the bucket:
`DynamicClasses: '''ss-pb-'' + toString($currentObject/PctBucket)'`.
`DynamicClasses: 'ss-pb-' + toString($currentObject/PctBucket)`.

Trade-off worth noting: this adds one bucket attribute per animated dimension to the
domain model. Pick a bucket count that matches the visual precision you need (20 → 5%
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Changed

- **`DynamicClasses` and a column's `DynamicCellClass` are written as Mendix expressions** (mendixlabs/mxcli#750) — the expression is written as-is, so the doubled-quote spelling is gone: `dynamicclasses: if $currentObject/Featured then 'is-featured' else ''`, and `dynamicclasses: 'is-featured'` is the string — the class `is-featured`. The same rule as the OData client's credentials. `create page`, `alter page … set` and `describe` all use it, and a describe → exec round trip stores identical values (measured on a Mendix 11.14.0 project). **Migrating a script:** the old spelling, the expression's text in quotes (`'if … then ''a'' else '''''`), still parses but would now store that text as a class name, so `check` and `exec` refuse it as **MDL-WIDGET33** and give the unquoted expression. An expression in any other widget property is an error rather than an empty value; a pluggable property whose schema kind is Expression (a column's `Visible`, for one) keeps the quoted form until a following change.
- **An OData client's credentials and header values are written as Mendix expressions** (mendixlabs/mxcli#750) — `HttpUsername`, `HttpPassword`, `ClientCertificate` and every `headers (…)` value hold an expression, and MDL now writes it as-is: `HttpUsername: 'admin'` is the string `'admin'`, `@Module.Const` reads a constant, and `'Bearer ' + @Module.Token` concatenates. Before, a quoted value was the expression's *text*, so `'admin'` stored the identifier `admin` and a string needed `'''admin'''`. `describe` prints the stored expression as-is, so Studio Pro's `'abc'` now reads `HttpUsername: 'abc'`; measured against a Studio Pro-authored client, and a describe → exec round trip stores identical values. **Migrating a script:** `'''admin'''` becomes `'admin'`, and a quoted constant `'@Module.Const'` becomes `@Module.Const` — both old forms still parse but would now store something else, so `check` and `exec` refuse them as **MDL-ODATA07**. A compound expression in any other OData property (`Path: 'a' + 'b'`) is an error rather than an empty value. `ServiceUrl` is a constant reference, not an expression — see the next entry.
- **An OData client's `ServiceUrl` names a constant, like `ProxyHost`** (mendixlabs/mxcli#750) — Studio Pro picks the service URL as a constant and stores it as `@Module.Name`. `ServiceUrl: Module.Location` is now accepted alongside `@Module.Location` and `'@Module.Location'` (the bare name used to be refused as "not a constant reference"); all three store the same value, and `describe` prints the bare name, as it does for the proxy references. A literal URL is still refused (CE6825).

Expand Down
2 changes: 1 addition & 1 deletion cmd/mxcli/syntax/features_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) {
"Reach for ALTER STYLING rather than CREATE OR REPLACE PAGE whenever only\n" +
"the look changes: replacing the page rewrites every widget in it, so the\n" +
"diff is the whole document and anything MDL cannot yet spell is lost.",
Example: "CONTAINER ctn (\n Class: 'my-card',\n DynamicClasses: 'if $currentObject/Priority = ''High'' then ''card-danger'' else ''card-normal'''\n) {\n DYNAMICTEXT txt (Content: 'Styled text')\n}\n\n" +
Example: "CONTAINER ctn (\n Class: 'my-card',\n DynamicClasses: if $currentObject/Priority = 'High' then 'card-danger' else 'card-normal'\n) {\n DYNAMICTEXT txt (Content: 'Styled text')\n}\n\n" +
"-- Restyle one widget on a page that already exists\n" +
"alter styling on page Sales.OrderOverview widget btnSave\n" +
" set Class = 'btn-primary', 'Spacing top' = 'Large';\n\n" +
Expand Down
4 changes: 2 additions & 2 deletions docs-site/src/appendixes/quick-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ MDL uses explicit property declarations for pages:
| Selection binding | `DataSource: SELECTION widget` | `DATAVIEW dv (DataSource: SELECTION galleryList)` |
| CSS class | `Class: 'classes'` | `CONTAINER c (Class: 'card mx-spacing-top-large')` |
| Inline style | `Style: 'css'` | `CONTAINER c (Style: 'padding: 16px;')` |
| Dynamic classes | `DynamicClasses: 'expr'` | `CONTAINER c (DynamicClasses: 'if $currentObject/IsActive then ''is-active'' else ''''')` — runtime-computed; stacks on `Class` |
| Dynamic classes | `DynamicClasses: 'expr'` | `CONTAINER c (DynamicClasses: if $currentObject/IsActive then 'is-active' else '')` — runtime-computed; stacks on `Class` |
| Design properties | `DesignProperties: [...]` | `CONTAINER c (DesignProperties: ['Spacing top': 'Large', 'Full width': ON])` |
| Width (pixels) | `Width: integer` | `IMAGE img (Width: 200)` |
| Height (pixels) | `Height: integer` | `IMAGE img (Height: 150)` |
Expand All @@ -383,7 +383,7 @@ MDL uses explicit property declarations for pages:
| `ColumnWidth` | `autoFill`, `autoFit`, `manual` | `autoFill` | `ColumnWidth: manual` |
| `Size` | integer (px) | `1` | `Size: 200` |
| `Visible` | expression string | `true` | `Visible: '$showColumn'` (page variable, not $currentObject) |
| `DynamicCellClass` | expression string | (empty) | `DynamicCellClass: 'if(...) then ... else ...'` |
| `DynamicCellClass` | expression string | (empty) | `DynamicCellClass: if(...) then ... else ...` |
| `Tooltip` | text string | (empty) | `Tooltip: 'Price in USD'` |

**Page Example:**
Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/language/alter-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ ALTER PAGE Module.EditPage {
| `ButtonStyle` | Button visual style | `SET ButtonStyle = Danger ON btnDelete` |
| `Class` | CSS class names | `SET Class = 'card p-3' ON cMain` |
| `Style` | Inline CSS | `SET Style = 'margin: 8px;' ON cBox` |
| `DynamicClasses` | Runtime-computed CSS classes | `SET DynamicClasses = 'if $currentObject/IsActive then ''is-active'' else ''''' ON cMain` |
| `DynamicClasses` | Runtime-computed CSS classes | `SET DynamicClasses = if $currentObject/IsActive then 'is-active' else '' ON cMain` |
| `Editable` | Editability mode | `SET Editable = ReadOnly ON txtEmail` |
| `Visible` | Visibility expression | `SET Visible = '$showField' ON txtPhone` |
| `Name` | Widget name | `SET Name = 'txtFullName' ON txtName` |
Expand Down
4 changes: 2 additions & 2 deletions docs-site/src/language/widget-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ CONTAINER cCard (Class: 'card mx-spacing-top-large') {
|----------|-------------|---------|
| `Class` | CSS class names | `Class: 'card p-3'` |
| `Style` | Inline CSS styles | `Style: 'padding: 16px;'` |
| `DynamicClasses` | Runtime-computed CSS classes (expression; stacks on `Class`) | `DynamicClasses: 'if $currentObject/IsActive then ''is-active'' else '''''` |
| `DynamicClasses` | Runtime-computed CSS classes (expression; stacks on `Class`) | `DynamicClasses: if $currentObject/IsActive then 'is-active' else ''` |
| `DesignProperties` | Design property values | `DesignProperties: ['Spacing top': 'Large']` |

### CUSTOMCONTAINER
Expand Down Expand Up @@ -566,7 +566,7 @@ These properties are shared across many widget types:
|----------|-------------|---------|
| `Class` | CSS class names | `Class: 'card p-3'` |
| `Style` | Inline CSS styles | `Style: 'margin-top: 8px;'` |
| `DynamicClasses` | Runtime-computed CSS classes (expression; stacks on `Class`) | `DynamicClasses: 'if $currentObject/IsActive then ''is-active'' else '''''` |
| `DynamicClasses` | Runtime-computed CSS classes (expression; stacks on `Class`) | `DynamicClasses: if $currentObject/IsActive then 'is-active' else ''` |
| `DesignProperties` | Atlas design properties | `DesignProperties: ['Spacing top': 'Large', 'Full width': ON]` |
| `Visible` | Visibility expression | `Visible: '$showSection'` |
| `Editable` | Editability mode | `Editable: ReadOnly` |
Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/reference/page/create-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ These properties are available on most widget types:
|----------|-------------|---------|
| `Class` | CSS class names | `Class: 'card mx-spacing-top-large'` |
| `Style` | Inline CSS | `Style: 'padding: 16px;'` |
| `DynamicClasses` | Runtime-computed CSS classes (expression; stacks on `Class`) | `DynamicClasses: 'if $currentObject/IsActive then ''is-active'' else '''''` |
| `DynamicClasses` | Runtime-computed CSS classes (expression; stacks on `Class`) | `DynamicClasses: if $currentObject/IsActive then 'is-active' else ''` |
| `Editable` | Edit control | `Editable: NEVER` or `Editable: ALWAYS` |
| `Visible` | Visibility expression | `Visible: '$showField'` |
| `DesignProperties` | Atlas design properties | `DesignProperties: ['Spacing top': 'Large']` |
Expand Down
4 changes: 2 additions & 2 deletions docs/01-project/MDL_QUICK_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1455,7 +1455,7 @@ MDL uses explicit property declarations for pages:
| Association source ("data from context") | `datasource: $currentObject/Module.Assoc` | nested `dataview dvCust (datasource: $currentObject/Order_Customer)` shows the to-one referenced object; a list widget shows the to-many collection |
| CSS class | `class: 'classes'` | `container c (class: 'card mx-spacing-top-large')` |
| Inline style | `style: 'css'` | `container c (style: 'padding: 16px;')` |
| Dynamic classes | `dynamicclasses: 'expr'` | `container c (dynamicclasses: 'if $currentObject/IsActive then ''is-active'' else ''''')` — runtime-computed classes; stacks on `class` |
| Dynamic classes | `dynamicclasses: 'expr'` | `container c (dynamicclasses: if $currentObject/IsActive then 'is-active' else '')` — runtime-computed classes; stacks on `class` |
| Design properties | `designproperties: [...]` | `container c (designproperties: ['Spacing top': 'Large', 'full width': on])` |
| Width (pixels) | `width: integer` | `image img (width: 200)` |
| Height (pixels) | `height: integer` | `image img (height: 150)` |
Expand Down Expand Up @@ -1521,7 +1521,7 @@ MDL uses explicit property declarations for pages:
| `ColumnWidth` | `autofill`, `autoFit`, `manual` | `autofill` | `ColumnWidth: manual` |
| `Size` | integer (px) | `1` | `Size: 200` |
| `visible` | expression string | `true` | `visible: '$showColumn'` (page variable, not $currentObject) |
| `DynamicCellClass` | expression string | (empty) | `DynamicCellClass: 'if(...) then ... else ...'` |
| `DynamicCellClass` | expression string | (empty) | `DynamicCellClass: if(...) then ... else ...` |
| `tooltip` | text string | (empty) | `tooltip: 'Price in USD'` |

**Page Example:**
Expand Down
10 changes: 10 additions & 0 deletions docs/11-proposals/PROPOSAL_first_class_expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,12 @@ proposals compose rather than compete.
otherwise quote. Confirm this output variance is acceptable.

4. **Pluggable expression properties are resolved by schema, not by name.**
**Decided 2026-09-25: (a)**, shipped in two steps. First the named
properties, `DynamicClasses` and a column's `DynamicCellClass`: written
as-is, a quoted value is a string (the Q5 rule), and the old quoted-text
spelling is refused as MDL-WIDGET33. Then the schema-driven extension to every
pluggable property whose kind is Expression.

Slice 2 either (a) lets any generic property take a bare expression and
rejects it at check time when the widget schema says the slot is not
`expression`-typed, or (b) adds the bare form only for the named properties
Expand Down Expand Up @@ -376,6 +382,10 @@ quoted output with the bare form.

**Slice 2 — expression family, widget slots.**

*Named-property step done (2026-09-25):* `DynamicClasses` and `DynamicCellClass`
take the expression as written through `create page`, `alter page … set` and
`describe`. The table below is the schema-driven remainder.

| File | Change |
|---|---|
| `mdl/grammar/domains/MDLPage.g4` | `widgetPropertyV3`: add `(IDENTIFIER \| keyword) COLON expression` **after** every existing generic branch, so `'text'`, numbers, booleans, qualified names and `[ … ]` keep their current parse and only what those reject (`if …`, `$v/Attr + …`, calls) reaches it. `make grammar`; watch for new ambiguity reports. |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- mendixlabs/mxcli#750, PROPOSAL_first_class_expressions.md slice 2 (named
-- properties): DynamicClasses and a datagrid column's DynamicCellClass hold a
-- Mendix expression written as-is.
--
-- dynamicclasses: if $currentObject/Featured then 'is-featured' else ''
-- dynamicclasses: 'is-featured' -- the string: the class is-featured
--
-- The old spelling — the expression's text in quotes, with its own quotes
-- doubled — still parses, and would now store that text as a class name. check
-- and exec refuse it as MDL-WIDGET33 and name the unquoted expression.
--
-- This file must FAIL `mxcli check`.

create page Legacy.P (title: 'P', layout: Atlas_Core.Atlas_Default) {
container c1 (dynamicclasses: 'if $currentObject/Featured then ''is-featured'' else ''''') { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ create or replace page MyFirstModule.P_LowerSet
-- Every one of these used to hard-error "widget has no pluggable Object".
alter page MyFirstModule.P_LowerSet {
set class = 'fl-topbar' on topBar;
set dynamicclasses = 'if $currentObject/Name != '''' then ''is-named'' else ''''' on topBar;
set dynamicclasses = if $currentObject/Name != '' then 'is-named' else '' on topBar;
set class = 'fl-badge' on rowBadge;
set caption = 'Shortcuts' on btnKeys;
}
Expand Down
4 changes: 2 additions & 2 deletions mdl-examples/bug-tests/bug10-dynamic-css-classes.mdl
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ create page BugTests.Bug10 (
dynamictext t1 (
content: 'hi',
class: 'base',
dynamicclasses: 'if 1 = 1 then ''mod-a'' else ''mod-b'''
dynamicclasses: if 1 = 1 then 'mod-a' else 'mod-b'
)
-- 10a: DataGrid2 column DynamicCellClass must persist as an expression
datagrid dg1 (
Expand All @@ -49,7 +49,7 @@ create page BugTests.Bug10 (
column colStatus (
Attribute: FullName,
Caption: 'Name',
DynamicCellClass: 'if $currentObject/FullName = ''x'' then ''ea-ok'' else ''ea-draft'''
DynamicCellClass: if $currentObject/FullName = 'x' then 'ea-ok' else 'ea-draft'
)
}
}
Expand Down
6 changes: 3 additions & 3 deletions mdl-examples/bug-tests/widget-dynamicclasses.mdl
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ create or replace page MyFirstModule.P_DynClass
dynamictext ovClass (
Content: 'chip',
Class: 'ss-chip',
DynamicClasses: 'if $currentObject/Name = ''Astute'' then ''ss-chip--astute'' else '''''
DynamicClasses: if $currentObject/Name = 'Astute' then 'ss-chip--astute' else ''
)
-- DynamicClasses on a container
container ovBox (
Class: 'ss-box',
DynamicClasses: 'if $currentObject/Name = '''' then ''ss-box--empty'' else '''''
DynamicClasses: if $currentObject/Name = '' then 'ss-box--empty' else ''
) {
dynamictext boxLabel (content: 'inner')
}
Expand All @@ -58,7 +58,7 @@ create or replace page MyFirstModule.P_DynClass
-- hard error "widget has no pluggable Object" on core widgets).
alter page MyFirstModule.P_DynClass {
set Class = 'ss-later' on ovLater;
set DynamicClasses = 'if $currentObject/Name != '''' then ''ss-later--named'' else ''''' on ovLater;
set DynamicClasses = if $currentObject/Name != '' then 'ss-later--named' else '' on ovLater;
}

describe page MyFirstModule.P_DynClass;
Loading
Loading