Skip to content

feat(image-cropper-web): allow custom aspect ratio width/height as expressions - #2333

Open
hunter-koppen wants to merge 6 commits into
mendix:mainfrom
hunter-koppen:feat/image-cropper-aspect-expression
Open

feat(image-cropper-web): allow custom aspect ratio width/height as expressions#2333
hunter-koppen wants to merge 6 commits into
mendix:mainfrom
hunter-koppen:feat/image-cropper-aspect-expression

Conversation

@hunter-koppen

Copy link
Copy Markdown

Convert the customAspectWidth/customAspectHeight properties from static integers to Integer-returning expressions so mendix developers can bind an attribute (or any expression) to the ratio sides instead of a fixed value.

The store now reads each DynamicValue via ValueStatus.Available guard, resolveAspectRatio tolerates undefined, and the editor preview parses the expression text (numeric literals only) with a free-aspect fallback.

…pressions

Convert the customAspectWidth/customAspectHeight properties from static
integers to Integer-returning expressions so app developers can bind an
attribute (or any expression) to the ratio sides instead of a fixed value.

The store now reads each DynamicValue<Big> via ValueStatus.Available guard,
resolveAspectRatio tolerates undefined, and the editor preview parses the
expression text (numeric literals only) with a free-aspect fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hunter-koppen
hunter-koppen requested a review from a team as a code owner July 16, 2026 10:09
@leonardomendix

Copy link
Copy Markdown
Collaborator

Hello maker! ☺️

Thank you for opening a pull request to the `web-widgets` repository and helping us make our widgets better. 💙

A small note from the maintainers:

We know that you probably wish to see your cool new feature or fix ASAP, but bringing a new feature to the widget is a small journey. But don't be scared! We just need time to triage, test, and analyze this feature or fix. We may request XML, code, or other changes that may be needed to match our desired look and feel for the new feature. We, as maintainers, strive to make sure that every change made in the code doesn't break existing behavior.

For now, here is what will happen next:

  1. Our TotW* will quickly scan your code and, if all is well, trigger a PR check.
  2. We will discuss your new feature or fix within our team.
  3. If we decide to add it, we will do code review and testing. As mentioned, we may ask you to make some changes.
  4. If all goes well, we will publish this new feature or fix in the next minor or patch release.

IMPORTANT: This repository uses code checks (linters, tests, etc.). Someone from our team will trigger these checks for your PR. In short, we will not proceed with your PR until all checks are green. It's the PR owner's goal to make checks green; maintainers are not going to do it for you.

* TotW — Tech of the Week

The reaction's data function builds a fresh { uri, name } object on every
evaluation, and GateProvider annotates props with observable.ref rather than
observable.struct by design, to preserve datasource identity. Default Object.is
equality therefore treated every parent re-render as a new image, re-fetching
the original bytes and discarding the user's crop selection.
Custom aspect ratio sides are expressions, so they resolve asynchronously while
the image may already have loaded. The resolved aspect used undefined for both
"free aspect" and "not loaded yet", so seeding during that window picked free
aspect and then visibly snapped once the real ratio arrived.

Add an explicit readiness signal to separate those states, hold off seeding
until both sides are available, and re-seed in one step when the ratio settles.
Re-seeding stays disarmed, so a ratio change never writes a re-cropped image
back to the bound attribute, and a ratio that becomes unavailable keeps the
existing selection instead of flashing to free aspect.
Now that the ratio sides are expressions, either can arrive undefined or as
non-numeric text. Cover the undefined and negative cases in resolveAspectRatio,
and the editor preview's numeric-literal parsing with its free-aspect fallback
for attribute bindings that cannot be evaluated at design time.
Raise the minimum Mendix version to 11.12, since expression-bound properties
with an Integer return type are the supported baseline for the custom aspect
ratio. Place the openspec change under the widget package, matching where this
widget already keeps its spec, and record it as a delta against the existing
image-cropper capability rather than a new one.
@rahmanunver

Copy link
Copy Markdown
Contributor

Hi @hunter-koppen, thanks for this! I'm taking it over to get it moving: handling some edge cases around your change, plus a related fix and some chores.

Pushing directly to this branch rather than opening a new PR so the contribution stays attributed to you.


### Decision 4: Raise `minimumMXVersion` to 11.12

Expression-typed properties with `returnType Integer` bound to attributes are the supported baseline. Bump `marketplace.minimumMXVersion` from `10.21.0` to `11.12`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is already 11.12. If it is not set correctly it should be corrected. This is not part of the story.

Comment on lines +26 to +29
#### Scenario: Custom ratio expression not yet resolved

- **WHEN** `aspectRatio` is `custom` and either side is not yet Available
- **THEN** the resolved ratio SHALL be treated as not yet known, which is distinct from free-form

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I miss explicit scenario definition when properties never resolve to a real value, for example they are unavailable from the very first render and it stays this way.

Comment on lines +156 to 162
get aspectReady(): boolean {
if (this.props.aspectRatio !== "custom") {
return true;
}
const isAvailable = (p: DynamicValue<Big>): boolean => p.status === ValueStatus.Available;
return isAvailable(this.props.customAspectWidth) && isAvailable(this.props.customAspectHeight);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should make aspect return something more meaningful - undefined - not ready; -1 - ready, free; positive number - ready, aspect set. Then this check doesn't have to rely on the props, it has to rely on simple fact that: not undefined means ready.

Comment on lines +139 to +145
const toNumber = (p: DynamicValue<Big>): number | undefined =>
p.status === ValueStatus.Available && p.value ? p.value.toNumber() : undefined;
return resolveAspectRatio(
this.props.aspectRatio,
toNumber(this.props.customAspectWidth),
toNumber(this.props.customAspectHeight)
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is cleaner to keep reading the value even if it is not strictly availalbe, if value goes to loading it retain the old value for a while and then replaces with the new value.

  1. (Loading, undefined) // first render
  2. (Available, 5) // loading done
  3. (Loading, 5) // something changed under the hood, but old value retained
  4. (Available, 10) // new value arrived

Comment on lines +18 to +21
## [1.0.0] - 2026-07-16

### Added

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version header should not be added

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants