hydrogen-web/doc/architecture/updates.md

4.5 KiB

Updates

How updates flow from the model to the view model to the UI.

EventEmitter, single values

When interested in updates from a single object, chances are it inherits from EventEmitter and it supports a change event.

ViewModel by default follows this pattern, but it can be overwritten, see Collections below.

Parameters

Often a parameters or params argument is passed with the name of the field who's value has now changed. This parameter is currently only sometimes used, e.g. when it is too complicated or costly to check every possible field. An example of this is TilesListView.onUpdate to see if the shape property of a tile changed and hence the view needs to be recreated. Other than that, bindings in the web UI just reevaluate all bindings when receiving an update. This is a soft convention that could probably be more standardized, and it's not always clear what to pass (e.g. when multiple fields are being updated).

Another reason to keep this convention around is that if one day we decide to add support for a different platform with a different UI, it may not be feasible to reevaluate all data-bindings in the UI for a given view model when receiving an update.

Collections

As an optimization, Hydrogen uses a pattern to let updates flow over an observable collection where this makes sense. There is an update event for this in both ObservableMap and ObservableList. This prevents having to listen for updates on each individual item in large collections. The update event uses the same params argument as explained above.

Some values like BaseRoom emit both with a change event on the event emitter and also over the collection. This way consumers can use what fits best for their case: the left panel can listen for updates on the room over the collection to power the room list, and the room view model can listen to the event emitter to get updates from the current room only.

MappedMap and mapping models to ViewModels

This can get a little complicated when using MappedMap, e.g. when mapping a model from matrix/ to a view model in domain/. Often, view models will want to emit updates spontanously, e.g. without a prior update being sent from the lower-lying model. An example would be to change the value of a field after the view has called a method on the view model. To support this pattern while having updates still flow over the collection requires some extra work; ViewModel has a emitChange option which you can pass in to override what ViewModel.emitChange does (by default it emits the change event on the view model). MappedMap passes a callback to emit an update over the collection to the mapper function. You can pass this callback as the emitChange option and updates will now flow over the collection.

MappedMap also accepts an updater function, which you can use to make the view model respond to updates from the lower-lying model.

Here is an example:

const viewModels = someCollection.mapValues(
	    (model, emitChange) => new SomeViewModel(this.childOptions({
	        model,
	        // will make ViewModel.emitChange go over
	        // the collection rather than emit a "change" event
	        emitChange,
	    })),
	    // an update came in from the model, let the vm know
	    (vm: SomeViewModel) => vm.onUpdate(),
	);

ListView & the parentProvidesUpdates flag.

ObservableList is always rendered in the UI using ListView. When receiving an update over the collection, it will find the child view for the given index and call update(params) on it. Views will typically need to be told whether they should listen to the change event in their view model or rather wait for their update() method to be called by their parent view, ListView. That's why the mount(args) method on a view supports a parentProvidesUpdates flag. If true, the view should not subscribe to its view model, but rather updates the DOM when its update() method is called. Also see BaseUpdateView and TemplateView for how this is implemented in the child view.

ObservableValue

When some method wants to return an object that can be updated, often an ObservableValue is used rather than an EventEmitter. It's not 100% clear cut when to use the former or the latter, but ObservableValue is often used when the returned value in it's entirety will change rather than just a property on it. ObservableValue also has some nice facilities like lazy evaluation when subscribed to and the waitFor method to work with promises.