The Wazimap-NG frontend uses a custom component architecture. Over time, this will likely evolve to use a more standard framework such as web components.
All components extend the Component
abstract class which itself extends the Observable
abstract class. Observable
implements the Observer design pattern. Extending it gives a class the ability to register listeners and trigger events [Note, we may move to use native CustomEvents if we adopt the web component architecture]. Events are arbitrary string identifiers although they are often namespaced to the component itself.
For instance, when a dropdown element is selected, a dropdown component might fire the dropdown.selected
event.
Here's an example:
The payload
is any arbitrary object and datatype that the class seeks to send to any listeners on that event.
Calling code might look something like this:
whenever the onElementSelected
method is called, every listener will receive the payload.
Component
extends Observable
and adds a parent-child relationship enabling components built from other components.
Instead of our Dropdown extending Observable
, it now extends Component
.
The calling code would look like this:
Doing this simply stores the parent and child components. In future this may be used to bubble events or in someway communicate across the entire component hierarchy. [Note, the Component class is still being developed].
Apart from extending Component
Wazimap-NG components use a Model View Controller design. The view comprises an HTML fragment, usually created by the web designer. [These are currently embeded in index.html but may in future be separated into individual component html files.]
The model stores the state and associated logic of the component and the controller acts as the glue between model and view, as well as handling events and interactions.
Here is a basic implementation of the DropDown component.
A few points worth noting:
All components receive their parent component as the first argument. In many cases the second argument is the DOM element that contains the view HTML.
By convention, the constructor calls prepareDomElements
which prepares various DOM elements used in the component. Note how each of the lines in prepareDomElements
ends in [0]
. This is due to the fact that JQuery's find
method returns a JQuery object. By convention, DOM objects are used in models, these can easily be converted into JQuery objects using the $ function, e.g. $(this._textArea)
.
A prepareEvents
method is often also used to wire up events from the constructor.
It's is important to note that only the web designer edits the HTML directly. If addition DOM elements are needed, the component will clone existing elements:
While it is tempting to create the Component using a single class, separating the model from the view often results in cleaner, more robust, and easier-to-test code. The model only stores the state of the component and does not interact with either the view or the controller directly.
It is important that the model is agnostic of the controller. In other words, the model should not hold a reference to the controller and should not call its methods. Communication takes place using either events or callbacks. While not mandatory, events are preferred as callbacks can result in slightly less-readable code.
In the case of a DropDown component, the state that needs to be stored includes:
Current options available in the dropdown
The currently selected item
Default text to display if no item is selected.
most of the model class looks like boilerplate, the most interesting method is the setter for currentIndex
Here we check if the index of the selected item has changed, if so, it is update and a DropDownModel.EVENTS.changeValue
is fired. Recall from the DropDownComponent class above, we add a listener to this event:
which then gets the appropriate value from the model:
The current index value on the model is updated when the li element is clicked:
The benefit of using a decoupled MVC framework is that the resulting code is uncomplicated and reusable. Using this structure, it would be possible to have two controllers working with a single model. When a user interacts with the first component, the second one will automatically update.
To ensure that the Model is decoupled from the Controller, it is usually best to separate events into two types, user interaction events and state change events. A user clicking on a box is an example of a user interaction event. They are typically primitive events. The controller notifies the model of the event by calling a method on the model object. This method will likely modify the internal state of the model. Once the state changes, a state change event is fired from the model. An example is the changeValue
event. These events usually operate at a higher semantic level. The controller receives the state change event and updates the view accordingly. In this case, the text shown.
Prefer using the _
prefix as a signifier of a private attribute, e.g.
Use getters and setters
Where possible provide the simplest possible arguments to methods, e.g.
Methods should receive only information that they need. Large application-wide state objects should be avoided.