State History
We have been contrasting the HTML + javascript based vs. Qt based GUI development frameworks.
As discussed in the appendum, the web browser has - because of it’s original purpose of being just a “file browser” - the extremely nice feature of being able to move forward and backward in the browsing history.
In the context of modern webapps, these buttons allow you to go forth and back in the state history. This is a feature that is not offered by default by Qt or by any modern GUI framework (to my knowledge), for obvious reasons.
Modern webapps with lots of javascript in the frontend, either offer this possibility correctly, or make a total mess out of it (“I clicked the back-button, and now it jumped to god-knows where!”), while classical, “bronze-age” webapps (see appendum) that actually do jump from one html file to another, offer this possibility OK by default.
As you might have noticed, the state of a webapp is encoded in the URL address bar like this:
http://www.example.com?par1=kikkelis&par2=kokkelis
, where in this case, the page carries two state parameters, par1
with value kikkelis
and so forth.
You can also copy-paste such URL addresses and send them to someone else, so then that person can go to the page and reach the desired state directly.
CuteFront offers a consistent way of saving the state and handling the state history. Please check out a working demo in here (and remember to try browser’s forward/backward buttons after throwing the ball a few times).
State de/serialization
First of all, a widget that wants to use the state history feature, needs to implement the serialization
and deserialization of it’s internal state. Let’s extend the widget BallPlayer
from the original example.
class BallPlayer2 extends BallPlayer {
...
// define state serialization
stateToPar() {
let par = +this.has_ball // boolean to int
this.log(-1, "stateToPar", par)
return par.toString()
}
// validate a serialized state
validatePar(par) {
// return false if par can't parsed as an integer value
let i = parseInt(par)
if (isNaN(i)) { // not an integer
this.err("validatePar failed with par",par,":",i,"not a number")
return false
}
if (i!=0 && i!=1) {
this.err("validatePar failed with :",i,"not 0 or 1")
return false
}
return true;
}
// define state deserialization
parToState(par) {
this.has_ball = (parseInt(par) == 1) // int to bool
this.log(-1, "parToState", this.has_ball)
this.setBall()
}
...
}
stateToPar
defines how the internal state of the widget is encoded into a string, so
that it can appear in the browser’s URL field in the form of par=value
.
parToState
defines the inverse operation: how we can set the internal
state of the widget using the text par=value
from the browser’s URL field.
validatePar
returns true
or false
, depending if the provided parameter
can be deserialized into a state or not.
All widgets that want the state management feature, must also define
a signal named state_change
:
createSignals() {
super.createSignals()
this.signals.state_change = new Signal() // required for state management
}
They must also define meticulously when the state should be serialized and saved,
by calling stateSave
:
catch_ball_slot() { // receive a ball
super.catch_ball_slot()
this.stateSave()
}
createState() {
super.createState()
this.stateSave()
// initialize to not having a ball
}
throwBall() {
if (!this.has_ball) {
// we don't have the ball..
return
}
this.has_ball = false
this.setBall()
this.stateSave()
this.signals.throw_ball.emit()
}
Using state de/serialization
Let’s modify the original ballgame example to include state history handling:
import { BallPlayer2, BillBoard2 } from './ballplayer2.js';
import { StateManager } from './statemanager.js'
class MyStateManager extends StateManager {
validateInitialState() { // how to form the initial state
if ((this.cache.alex == null) ||
(this.cache.bob == null) ||
(this.cache.billboard == null)) {
this.cache.alex = '0'
this.cache.bob = '1'
this.cache.billboard = '0'
}
}
}
var alex = new BallPlayer2("alex");
var bob = new BallPlayer2("bob");
var billboard = new BillBoard2("billboard")
var state = new MyStateManager("state")
alex.setLogLevel(-1)
bob.setLogLevel(-1)
billboard.setLogLevel(-1)
state.setLogLevel(-1)
// connect alex, bob and billboard's state_change signal
// to state's state_change_slot
state.register(alex, bob, billboard);
// ball from alex to bob
alex.signals.throw_ball.connect(
bob.catch_ball_slot.bind(bob)
)
// ball from bob to alex
bob.signals.throw_ball.connect(
alex.catch_ball_slot.bind(alex)
)
// inform billboard about the game
// alex throws
alex.signals.throw_ball.connect(
billboard.ball_throw_slot.bind(billboard)
)
// bob throws
bob.signals.throw_ball.connect(
billboard.ball_throw_slot.bind(billboard)
)
// NOTE: order of the following calls (1, 2) is important
// as you want to update the state information _before_ it is saved
// into the url address bar
state.connectStateChanges() // (1)
// when the URL address serialize state is updated:
billboard.signals.state_change.connect( // (2)
state.state_save_slot.bind(state)
)
Let’s dissect what is exactly happening here.
Serialize
(a) Caching the state
Each time a ballplayer changes it’s state (throws or catches a ball), it sends the signal state_change
, carrying a key-value pair
to the MyStateManager
widget . MyStateManager
then caches these key value pairs.
A default behaviour is that always when a widget changes it’s state, the information is sent to MyStateManager
. This is enabled by:
state.connectStateChanges() // (1)
That line of code connects all widgets’ state_change
signals to MyStateManager
‘s state_change_slot
(you could connect them separately by yourself as well).
Say, if the ball has been thrown 5 times and is currently with alex, then the cached key-values within MyStateManager
would look like this:
alex: 1
bob: 0
billboard: 5
(b) Updating the URL address bar
When the state.save_state_slot
is fired, MyStateManager
proceeds to update the URL address bar and insert the new address into the
browser’s history, with this trailing address:
?alex=1&bob=0&billboard=5
Note that the (a) caching of the deserialized state into MyStateManager
is separated from the (b) URL and history update. When and if
the latter happens is defined by connecting signals to MyStateManager
’s save_state_slot
.
When the new deserialized state appears in the browser’s URL address and history, is another matter completely and is defined by these lines (2):
// when the URL address serialize state is updated:
billboard.signals.state_change.connect( // (2)
state.state_save_slot.bind(state)
)
Had we connected signals from all widgets (ballplayers and the billboard) to state_save_slot
, we would create three states into browser’s history each time a ball is thrown - a thing
we obviously do not want.
One final note: the order of calling (1) and (2) is important: we want the slots to be called in such an order that the states are cached first and only after the caching is complete, the state is updated into the browser URL address bar and history.
Deserialize
Deserialization, i.e. the widgets setting their state, based on what appers in the URL address field ?alex=1&bob=0&billboard=5
ending of the URL, is triggered always when you press the forward/backward buttons of the browser.
MyStateManager
will pick up the parameters from the URL address field and calls each widget’s parToState
method.
Initialization
We also have to define what happens at the initial page load. In the current case, if a single state parameter is missing, we will just reset all of them:
class MyStateManager extends StateManager {
validateInitialState() { // how to form the initial state
if ((this.cache.alex == null) ||
(this.cache.bob == null) ||
(this.cache.billboard == null)) {
this.cache.alex = '0'
this.cache.bob = '1'
this.cache.billboard = '0'
}
}
}
A more default behaviour would be:
class MyStateManager extends StateManager {
validateInitialState() { // how to form the initial state
if ((this.cache.alex == null) ||
(this.cache.bob == null) ||
(this.cache.billboard == null)) {
this.setDefaultValues()
}
}
}
Where MyStateManager
simply asks from each widget for a suitable initial parameter by calling setDefaultValues()
.
Pitfalls
parToState
is fired always when a serialized state is pulled from the browser history, so you probably don’t want
signals and widget interactions fired when parToState
is called: this would make one widget to alter the state of another
widget (via the signal-slot connections), while we actually want them just to create a state from the deserialized data.
So be carefull not to fire any signals originating in/directly from parToState
.