[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