Lucid.js / Components
Introduction 🔗
Components are at the heart of working with Lucid. All your structure, styling and logic will be declared in component files.
Components are declarative entities that can be used and reused. You'll have a master component, inside which all other components live. Components will typically pass data between each other - both at the time a parent component loads a child component (passing along data in this generational way is known as "props") and after the props have rendered, via JavaScript.
For example, a child component may wish to notify a parent component about an event - or vice versa.
Component files 🔗
Each Lucid component normally lives in its own HTML file (the component name, in lower-case, with a .html file extension). A component's CSS, HTML and JS live fully in that file (see Component structure).
When instantiating Lucid, you tell Lucid where these files live via the compsPath
param. If omitted, Lucid will look for the files in the current directory.
For smaller apps it may be preferable to have all components stored in a single components file, rather than each in its own separate file. This can be achieved by passing the compsFile
instantiation param.
Where components all live in the same file, each one most be preceded by a structured comment denoting its name, in the following format:
Component structure 🔗
style
tag - the component's CSS- HTML template
script
tag - the component's JavaScript
The style and script tags are optional, whereas the HTML template is mandatory.
A simple component, therefore, is a HTML file with the following content.
<style>
#myComponent { border: solid 2px red; padding: 1rem; }
</style>
<div id='MyComponent'>
<h1>Hello, world!</h1>
</div>
<script>
this.DOM.onclick = () => alert('Hello from the component!');
</script>
It doesn't matter which order you specify the three tags in.
As mentioned in Component files, if all components live in a single file, rather than each in a separate file, each component should be preceded by a comment identifying it.
It's possible to tell if a given element is a Lucid component by checking for a Lucid.component
(symbol) property, i.e. someElement[Lucid.component]
.
Component rules 🔗
There's a few rules to keep in mind when creating components. (Don't worry; if you break any of them, Lucid will tell you via the console.)
- The HTML template must be a single container element with all other tags inside it.
- A maximum of one
style
,script
and HTML container tag is allowed per template. - Child component tags cannot have content in them; they must exist as opening tags only, or self-close, i.e.
or
. - Child component tags must be capitalised; normal HTML tags must be lowercase.
- Child component templates can obviously not cotain references to themselves (this would lead to recursion hell).
The components API 🔗
The JavaScript attached to a component runs in the context of the component - in other words, this
=== the component instance.
The instance holds a small API of methods and properties via which you can interact with the component.
Properties: 🔗
data
(object) - the component's data, including the result of any props that were passed in from the parent component when the child component's template was parsed.DOM
- a reference to the component's DOM, i.e. its container element (the outer tag of its HTML template).children
(array) - a live collection of the component's child components. This is updated automatically whenever child component's are added or removed.siblings
(array) - as withchildren
, but for sibling components.activeState
(integer; string) - the component's current state ID. This will be integer unless a persistent state has been saved (seenewState()
) and is the current state, in which case this will be the name of that persistent state. See States.name
(string) - the component's name.instance
(integer) - the component's zero-indexed instance ID. This is generated and managed automatically by Lucid. For example, if two instantes of the same component are rendered, one will be instance ID 0, the other 1, and so on.renderTime
(date) - a timestamp of when the component instance was last rendered.reRendered
(boolean) - whether the component has ever been re-rendered since its initial render.parent
(object) - a reference to the parent component (i.e. the component that loaded the current component). This will be undefined for the topmost (i.e. master) component.app
(obj) - a reference to the Lucid instance.master
(obj) - a reference to the app's master component.methods
(obj) - a namespace for declaring component-level filter methods to be used in variable parsing. See Variables > Filter methods.events
(obj) - a namespace for declaring callbacks for DOM events on the component. See Events > DOM events.
Methods: 🔗
render()
- re-renders the component (and any child components). The current state of the component'sdata
is retained; that is, any data written to the component since its last render will persist after the re-render. However, its prop values will be populated afresh, and merged intodata
.on(eventType, callback)
- register a lifecycle event on the component. See Events > Lifecycle events.reprocessReps([selector])
- reprocess the component's repeaters, optionally filtered by a selector pointing to a specific repeater. See Repeaters.reprocessConds([selector])
- reprocess the component's conditionals, optionally filtered by a selector pointing to a specific conditional. See Conditionals.go(route)
- activate a route, by passing along the route segments (segment routes) or JSON string (JSON routes). See Routes.message(xpath, data)
- send a message from the component to another component in the components hierarchy. This can be upwards, downwards or sideways. The component to message is targeted relatively by an XPath statement. See Component messaging.newState([persist])
- register a new state (version) of the component. This is used to record a snapshot of the component's current state, to return to it later if need be. The state can optionally be saved as a persistent state, meaning it can't be overridden by "undo-redo"-style traversal. See States.changeState(which)
- return the component to an earlier state that was saved vianewState()
. See States.rc()
- alias forreprocessConds()
.ra()
- alias forreprocessAttrs()
.rr()
- alias forreprocessReps()
.
Next, let's look at components in more detail by looking at Repeaters - the way Lucid handles iterated elements.
Data & props 🔗
Every component has a data
object. This is initially populated by any "props" (short for properties) passed to it by its parent component when it was rendered.
The master component doesn't have props as such, as it has no parent component, however you can pass data to it via the data
instantiation param.
So for example, imagine a parent component like so:
When that component is parsed, the reference to the child component is gathered up and the child component gets loaded - with those prop values passed in to its data
object.
As shown, props can take the form of literal values (prop1
) or references to data (prop2
- this will pass the value of the bar
property on the foo
object's from the component's data.)
Any variable templates in the component's HTML template will be parsed when the component is rendered (or, later, re-rendered). These will be swapped for values read from the component's data.
Continuing the example above, if the Child component had the following HTML template and JS:
...then given the props passed to it as shown above, it would initially render as:
Component events 🔗
There are two kinds of component event:
- DOM events - events that fire when a user interacts with the component
- Lifecylc events - Lucid-defined events that fire when something happens to the component or the app, such as a route change, or data mutation
See the Events page for more.
Re-rendering components 🔗
Re-rendering components is a major part of Lucid. When a component is first rendered, it's likely that some of the data it wants to output does not exist yet.
Consider the following simple component.
<h1>{{text}}</h1>
<script>
if (!this.data.text) {
this.data.text = 'Hello!';
this.render();
}
</script>
A similar technique is used with components that re-render but also maintain States - see States > Re-rendering.
When the component is first loaded, the template parsing engine looks for a text
property in its data
object, and doesn't find one.
We set it, then re-render the component. When re-rendering a component any changes to its data
since it last rendered persist, so after re-rendering the engine will this time find the property and display it.
Components can also be re-rendered as the result of their parent component reprocessing repeaters or conditionals, if those repeaters/conditionals deal with child components.
Did I help you? Feel free to be amazing and buy me a coffee on Ko-fi!