Lifecycle
Every Modulo Component goes through a certain "lifecycle". Here's an
analogy: If CParts are the stage-hands and actors who put on a play, then the
"lifecycle" is the pre-described arrangement of each "act", intermission, and
so on, such that every CPart stage-hand "jumps in" at the right moment to do
it's job and set up for the next act.
#
Global lifecycle
Every Component that you define triggers a certain set of lifecycle
phases. The lifecycle phases that a Modulo Component progresses
through can be categorized into a few groups. One way to think of these phases
is that typically, the "output" of a previous lifecycle phase in a group is the
"input" of the next phase. As an example, prepare
figures out the
template variables, render
causes the template to actually perform
the render based on the variables in the previous phase, and
update
updates the HTML of the element based on the rendered
output of the previous phase.
#
Component lifecycle
Factory
The factory phase happen once per Component definition. It will
happen even if the component is never used or instantiated anywhere, but only
defined. This factory
thus happens during the define
global phase of Modulo, and is invoked to prepare the Component definition.
factory
- Happens while a component is getting defined.
This happens after the prebuild
and define
phases,
but before the component is used. Unlike the prebuild
and
define
phases, factory
happens after the page
loads, and thus has no restrictions on what it does or generate. (E.g. it's
output does not need to be serializeable, since it will be run
after built JS files as well).
Construction and initialization lifecycle
Two methods are triggered when a component is mounted on the
page:
- constructor() - While technically not a lifecycle method, the
constructor() method will be invoked when creating an instance of the CPart
class. This happens before the Component's HTML Element is
guaranteed to be fully loaded on the DOM.
initialized
- Happens once per instance, every time a
component is fully mounted (i.e. used) on a page, but before the first
render. Useful for fetching or setting up initial data on a component, as
all the component will be fully ready for use at this point. Note that this
will only be invoked after the initial DOM is fully loaded, but before the
prepare of the first rerender (described below).
Rendering lifecycle
All of 4 these lifecycle phases trigger in the following sequence every time
a component renders. This including the first time, every manual rerender (e.g.
invoking element.rerender()
), and ever automatic rerender (e.g.
after events or state manipulation). These will phases even trigger if you
don't have a Template CPart defined (although without a
Template there will be nothing to render, per se).
prepare
- Gather data needed before rendering (e.g. gather
variables for template). Return values from here to have "computed
variables" within the Template and Script code.
render
- The Template CPart will use this phase to to
render HTML code from the template specified
(by setting renderObj.component.innerHTML
reconcile
- The Component CPart will compare the new
rendered HTML to the DOM, and generates a "patch-set" or array of
operations necessary to reconcile the DOM to the new HTML (by setting
renderObj.component.patches
)
update
- Finally, the Component will apply the patch-set
generated in the previous step, actually modifying the DOM and invoking any
directive callbacks specified
Directives that invoke lifecycle methods
Directives can can cause lifecycle phases as well. With the built-in CParts,
there is only one directive that will trigger any lifecycle phases: The
event
directive (e.g. @click
). More information on
directives is covered in the Directives
section.
event
- Triggers when an event is about to happen.
eventCleanup
- Triggers after an event happened. A common
pattern is to use this and the previous to figure out what has "changed"
due to the event, and respond accordingly.
#
Lifecycle Callbacks
The purpose of lifecycle phases is the lifecycle methods
or callback functions that "hook into" each phase and run during them.
You can get the name for a lifecycle method by simply suffixing the
name of the phase with the word Callback
. For example,
prepare
thus becomes
function prepareCallback() {...
. Every CPart has the
ability to hook into lifecycle methods by defining a method with an expected
name. This is useful for CPart developers to implement the actual behavior and
functionality of a CPart. In fact, most of CPart code is just hooking into
these different methods!
The Script CPart also exposes this interface to component
developers. This is so that you can write custom code in each component to
execute during any one of the phases. Note that you cannot hook into the global
phases (e.g., prebuild, define, factory) this way, since those need to happen
before your script tag is even ready.
Directives also have callback functions, covered in a
the directive section.
Try it out below:
renderObj
renderObj is like "req" -
renderObj
is comparable to the "request" or "response" objects
in many backend MVC frameworks, such as in Express.js or Django. In these,
middleware works behind the scenes to modify or construct the "request" object.
They prepare it for the controller functions that accept it as an argument, by
attaching data and injecting dependencies. For a concrete example, enabling
Express's JSON parsing middleware adds the req.json
attribute to
request objects, for parsing of JSON data.
This is what inspired the renderObj
, which is a plain object
that gets passed along throughout the lifecycle phases. The Component Parts act
just like middleware in other web frameworks: Work together to construct the
renderObj
for users of the framework.
So far in this document, we covered how lifecycle methods are central to
CParts, and that CParts are central to Modulo components in general.
Now, we will learn how renderObj
is central to lifecycle methods.
It's how lifecycle methods pass data and communicate, as a component passes
through the different phases.
renderObj and lifecycle
The renderObj goes through 3 phases. The first is during set-up, the second
during component factory initialization, and the third happens every time a
component rerenders.
loadObj
- This is the version created during the
load
phase, and is typically just the text and attributes of
the component definition with any pre-processing done. Unlike the other
objects, loadObj
can only contain plain or
JSON-friendly data types.
baseRenderObj
- The loadObj
is copied to the
baseRenderObj
, which is the result of the factory
phase. This might have more dependencies injected, e.g. the template CPart
at this point will have compiled the template code a JS function.
renderObj
- This is the one that component developers are
more likely to encounter. During first render, and every time a
.rerender()
method is called, renderObj
is
duplicated from the baseRenderObj
, and then gets passed around
through the prepare
, render
, reconcile
,
and update
phases.
Typically, unless developing new CParts, you will have little need to
directly interact with the renderObj
. You already implicitly use
it in several cases. For example,
whenever you access variables in the Script CPart, the
state
variable is in fact shorthand for
element.renderObj.state
, or whenever you access variables in the
template CPart (e.g., props.text
is
shorthand for element.renderObj.props.text
).
So, if you are a component developer and not a CPart developer, and you find
yourself thinking that it doesn't make a lot of sense why you'd need to access
or modify the renderObj
explicitly... You probably don't!
Nevertheless, use the following example to examine the output of the
following example to see what information is available or modified at each
step: