feat(image-cropper-web): allow custom aspect ratio width/height as expressions - #2333
feat(image-cropper-web): allow custom aspect ratio width/height as expressions#2333hunter-koppen wants to merge 6 commits into
Conversation
…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>
|
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:
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.
|
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`. |
There was a problem hiding this comment.
It is already 11.12. If it is not set correctly it should be corrected. This is not part of the story.
| #### 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 |
There was a problem hiding this comment.
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.
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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) | ||
| ); |
There was a problem hiding this comment.
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.
- (Loading, undefined) // first render
- (Available, 5) // loading done
- (Loading, 5) // something changed under the hood, but old value retained
- (Available, 10) // new value arrived
| ## [1.0.0] - 2026-07-16 | ||
|
|
||
| ### Added | ||
|
|
There was a problem hiding this comment.
Version header should not be added
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.