[Cuis-dev] Rerendering when touched: Experiments in simplifying UI development in Cuis inspired by Mithril.js
Paul D. Fernhout
pdfernhout at kurtz-fernhout.com
Sat May 13 12:05:40 PDT 2023
Hi again Juan and others,
Here is a rough implementation of what I mentioned below, transforming
the example you supplied into a buildMorph: method of a
KfFToCExperiment2Model class:
https://github.com/pdfernhout/Cuis-Smalltalk-RedrawWhenClicked/blob/main/KfExperiments-v4.st#L248-L312
That code also adds a button to show and hide a "advanced" section that
displays temperature in Kelvin.
You can launch the demo with: "KfRebuiltWindow new openWith:
KfFToCExperiment2Model new".
As a test, you can enter -460 for Fahrenheit and see what it is in Kelvin.
==== More details
Here is the core bit of code highlighted at the link above which defines
the UI:
buildMorph: builder
"KfRebuiltWindow new openWith: KfFToCExperiment2Model new"
| showAdvancedLabel |
Transcript show: 'buildMorph ' , temperatureF asString , ' ',
temperatureC asString; cr.
builder create: [Panel new model: self] update: [:morph | morph
layoutMorph removeAllMorphs.].
builder
namedSubmorph: #eF
create: [ | newMorph |
newMorph := SimpleNumberEntryMorph integerDefault: temperatureF
maxNumChars: 9.
newMorph crAction: [
Transcript show: 'crAction', newMorph contents.
temperatureF := newMorph contents asNumber].
newMorph]
update: [:submorph | Transcript show: 'set value F ' , temperatureF
asString; cr. submorph setValue: temperatureF].
builder namedSubmorph: #b create: [
PluggableButtonMorph
model: [self convertFToC]
stateGetter: nil
action: #value
label: 'Convert'
].
builder
namedSubmorph: #eC
create: [SimpleNumberEntryMorph integerDefault: temperatureC
maxNumChars: 9]
update: [:submorph | Transcript show: 'update'; cr. submorph setValue:
temperatureC].
showAdvancedLabel := (showAdvanced ifNil: [false]) ifTrue: ['Hide
advanced'] ifFalse: ['Show advanced'].
builder
namedSubmorph: #advancedToggle
create: [
PluggableButtonMorph
model: [showAdvanced := showAdvanced ifNil: [true] ifNotNil:
[showAdvanced not]]
stateGetter: nil
action: #value
label: showAdvancedLabel
]
update: [:submorph | submorph label: showAdvancedLabel].
builder morph layoutMorph
addMorph: (builder namedSubmorph: #eF);
addMorph: (builder namedSubmorph: #b);
addMorph: (builder namedSubmorph: #eC);
addMorph: (builder namedSubmorph: #advancedToggle).
showAdvanced ifTrue: [
| k |
k := (temperatureC + 273.15) asFloat rounded.
builder
namedSubmorph: #eK
create: [SimpleNumberEntryMorph integerDefault: k maxNumChars: 9]
update: [:submorph | submorph setValue: k].
builder morph layoutMorph addMorph: (builder namedSubmorph: #eK)
].
"builder morph morphExtent: 200 at 160.
(builder namedSubmorph: #b) morphExtent: 140 at 30."
^builder morph
This buildMorph: code is supposed to get called every time the user
clicks in the window (or eventually on other listened-for events). The
code will rebuild the morph and its submorphs, reusing the existing
morphs and updating them as needed when the method is called a
subsequent time.
That code uses a "builder" pattern (KfMorphBuilder) for a model to make
morphs that view the model (where for now the model has to be installed
in a KfRebuiltWindow to use the builder for rebuilding). That approach
also required separating out a "create" phase from an "update" phase for
submorphs made in buildMorph: rather than just having a single phase
that does either definition or updating subcomponents like Mithril has.
While overall this code is not the way I would have done this all from
scratch, for now I avoided any changes to Cuis base classes and
leveraged the Morphic infrastructure as much as possible.
In theory maybe a "builder" would not be needed for building and
rebuilding UIs on events -- but that also might require invasive changes
to Cuis and Morphic.
The demo currently has an updating issue where after clicking the
"Convert" button or "Show advanced" button you need to click elsewhere
in the window to see the updated Celsius value or the newly added input
field.
I'm not sure why the window is not updating when the Convert button is
clicked. It seems like it is entirely absorbing the button-up event --
even when I thought that event should still have been processed by the
enclosing KfRebuiltWindow's processMouseUp:localPosition: method.
Probably something I don't understand about morphic event handling in
Cuis. Any suggestions from anyone on how to fix that bug would be
appreciated.
KfMorphBuilder's morphToBuilderMap WeakValueDictionary is not currently
used, but was intended eventually for more general support of builders
used with any model with a buildMorph: method, not just a model
installed in a KfRebuiltWindow (without otherwise having to add a
"builder" ivar to Morph).
I could not get the initial window sizing set correctly yet using this
pattern because the sizing in the example you supplied has to happen
after the morph is open, not before when it is built (otherwise it is
ignored). I left some commented code about that in the example. Again,
probably something I don't understand about sizing and layout.
When the Kelvin section is added to the layout, it spills out of the
enclosing panel. So, another layout issue to be fixed.
In general, the layout part is also rough as a first cut. Ideally the
builder should be able to handle simple layout tasks, avoiding the need
to reference "builder morph layoutMorph" and manually call "addMorph:"
multiple times.
This filein is for an entire KfExperiments category and so also includes
the previous experiment (KfMithrilInspiredExperimentMorph) plus an
earlier unfinished experiment that uses the conventional
observer/dependency pattern ( KfFToCConverterModel and
KfFToCConverterWindow). No doubt more stuff I need to learn about
elegantly sharing Cuis code sharing.
Even with bugs and limitations, this code does show that in theory this
rebuild-the-UI-on-events approach is possible in Cuis. Whether any
Smalltalkers used to the conventional observer/dependency approach might
eventually prefer this alternative approach for some UI development is
still an open question.
--Paul Fernhout (pdfernhout.net)
"The biggest challenge of the 21st century is the irony of technologies
of abundance in the hands of those still thinking in terms of scarcity."
On 5/8/23 23:50, Paul D. Fernhout via Cuis-dev wrote:
> What I ultimately want is more like some hybrid of the example you
> supplied (including the use of multiple morphs in a layout) and the
> example I supplied (state fetched from ivars).
>
> That might be something close to your example, but defined in, say, a
> "buildView" method of SystemWindow subclass like, say,
> "TemperatureConverterWindow". The "integerDefault:" values would come
> from something like "temperatureF" and "temperatureC" ivars in an
> instance of that class. The first SimpleNumberEntryMorph (e1) would
> store its changes back to that window instance temperatureF ivar. And
> every time someone clicked on the button, that block would fetch the
> temperatureF ivar value from the window instance (not the
> SimpleNumberEntryMorph) and then set the temperatureC ivar. And then a
> rebuild would be triggered (due to the window being "dirty" because
> there was a mouse click in it) and as part of the rebuilding process,
> the e2 morph would get the updated value from the temperatureC ivar,
> notice that value had changed from when it was initialized, and update
> itself.
>
> There is, however, nothing much to motivate this seeming extra
> complexity of rebuilding on a mouse click in the above (especially in
> preference to the concise example you supplied). And that is one limit
> of starting with a simple Proof of Concept. The use of the button also
> makes things somewhat easier compared to triggering the conversion when
> the edited text changes (as in the Mithril examples I linked to for
> currency conversion).
>
> So, here is an extra twist to the POC. Imagine there was a "advanced"
> section of the UI for displaying the temperature value in Kelvin. So
> there might be a toggle whose state was stored in an "showAdvanced" ivar
> in the window instance. And when "showAdvanced" was true, then (and only
> then) the build process would include a third morph in the layout which
> was also initialized from the "temperatureC" ivar in the buildView
> method, but which transforming temperatureC into the corresponding
> Kelvin value by adding 273.15. And, as one further twist, we want to add
> that extra UI behavior while our POC is already running in a window. :-)
>
> I'll play around with the example code you have supplied a bit more in
> days to come and see if I can eventually make a better proof of concept
> in Cuis more like outlined above, with dynamic morph creation and
> removal in response to a mouse event
More information about the Cuis-dev
mailing list