This tutorial covers creating static or server side rendered pages with Live Elements. It goes through configuring the server, rendering the content, adding events and behaviors, and creating reusable components within this context.
It uses the setup made in the first project article. Basically, we create an empty folder, and then use lvweb
inside the folder to generate a starting webpack project:
mkdir staticpages
cd staticpages
npm i live-elements-web-cli
npx lvweb init
npm i
lvweb serve
Now, if we go to localhost:8080, we should see the following text:
Welcome to Live Elements
In the generated sample, the rendered component in app/Home.lv
is configured in the bundle file (bundle/bundle.lv
):
import live-elements-web-server.bundle
import live-elements-web-server.router
import live-elements-web-server.style
import .app
instance bundle Bundle{
ViewRoute{ url: '/' c: Home }
Stylesheet{ src: './styles/style.css' output: 'style.css' }
}
The ViewRoute
tells the server to send this component for rendering at the root url: '/'
. By default the route is rendered on the client side, which means the page's content is rendered and updated dynamically. If we were to inspect the page's source code, it will initially have an empty body with a script that will render the content:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Index</title>
</head>
<body data-content-type="main">
<script src="/scripts/home.bundle.js"></script>
</body>
</html>
This is controlled by the render
property. The render
property, is set by default to ViewRoute.CSR
:
ViewRoute{ url: '/' c: Home render: ViewRoute.CSR }
We can set the render property to ViewRoute.SSR
, and then restart the server:
// ...
instance bundle Bundle{
ViewRoute{ url: '/' c: Home render: ViewRoute.SSR }
Stylesheet{ src: './styles/style.css' output: 'style.css' }
}
lvweb serve
If we check out the page source code again, unfortunately we won't see any changes. And that's because the page is still being rendered on the client side. This is actually due to lvweb being run in development mode. In development mode, lvweb prefers client side rendering, because it can reload pages live as they are being saved, making them easier and faster to develop. So, even server side rendered pages will only be rendered on the server when they are released in production, which is by compiling an optimized build, and then running the new build:
lvweb compile
lvweb run
Testing server side rendered pages this way is a bit cumberstone, as with any small change, we have to recompile the entire website before testing it again. It's why lvweb serve
has a renderMode
argument, through which we can tell lvweb to render all views as if they were in production:
lvweb serve --renderMode production
Looking at our page source code again, we will now see the entire source:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Live Elements</title>
<link rel="stylesheet" href="/styles/style.css">
</head>
<body data-content-type="main">
<h1>Welcome to Live Elements!</h1>
<script src="/scripts/home.bundle.js"></script>
</body>
</html>
We've made the page render server side. When compiling for production, this page will be rendered, cached and served with minimal resources from our server. It's useful for writing documentation pages, presentation pages, or other static content.
Let's try to add some interactions to our page.
In the previous quick start tutorial, we've added a button to our page to alert a message. Let's add the same button in app/Home.lv
:
import live-web.dom
import live-elements-web-server.view
component Home < PageView{
head: PageProperties{
title: "Live Elements"
StyleLink{ href: '/styles/style.css' }
}
Body{
H1`Welcome to Live Elements!`
Button{
on click: () => { alert('Button clicked.') }
T`Clickable`
}
}
}
We should also reset our style to have a better view of the button. We can set styles/style.css
to:
body{
font-family: Arial, Helvetica, sans-serif;
font-size: 20px;
color: black;
margin: 0px;
padding: 0px;
}
If we restart the server now lvweb server --renderMode production
, we will notice the button is there, however, clicking it won't have any effect:
This is because the button is rendered to the dom on the server side, without any events. When the server renders all the elements to the dom, it won't render any events or bindings, as most of them don't have an equivalent in html or javascript DOM functions. Take the following example:
import live-web.dom
import live-web.model
import live-elements-web-server.view
component Counter < State{
int counter : 0
}
component CounterView < PageView{
id: counterView // id to reference inside the hierarchy
head: PageProperties{ StyleLink`/styles/demo.css` }
// create the counter state
Counter state: Counter{}
Div{ classes: ['center', 'full']
Div{ classes: ['counter']
P{
T{ text: 'Counter value: ' + counterView.state.counter }
}
Button{ on click: () => {counterView.state.counter++} T`+` }
Button{ on click: () => {counterView.state.counter--} T`-` }
}
}
}
The example uses states, property bindings, and events to create a dynamic page. When rendering the page on the client side, all this functionality will work because the live elements code will be sent to the client in order to render and update the page accordingly. On the server side, the code will only be used to render once and save the page in its html form.
One way to add events to the page is through hydration. It's when the page is rendered on the server side, but the rendering code is also sent along with the rendered page to the client. The events from the rendered page are attached back to the DOM structure the page was rendered from. This would attach and reactivate all the functionality as if the page was rendered directly on the client.
This is useful in some cases, but it comes with a drawback. Hydrating a server-rendered page on the client side will increase load times and resource usage, as event listeners need to be reattached, and dynamic functionalities re-enabled, but also the entire javascript DOM structure needs to be downloaded by the browser additionaly to the rendered DOM.
Live Elements currently doesn't support hydration, but it's not far down the roadmap, and should be available soon.
In some cases hydration is worth it, in others not so much. For a page with lots of content and few interactions, like one or two buttons, hydration would be overkill. In this siutation, there are 2 options:
DOMBehavior
. An new approach specific to Live Elements.DOMBehavior
is a component that attaches events to the rendered DOM tree. It works on both client side and server side rendered pages. This is how a DOMBehavior
would attach to a Button
:
import live-web.dom
import live-elements-web-server.view
component Home < PageView{
head: PageProperties{
title: "Live Elements"
StyleLink{ href: '/styles/style.css' }
}
Body{
H1`Welcome to Live Elements!`
Button{
T`Clickable`
DOMBehavior{
domEvents = {
click: (e) => {
alert('Button Clicked')
}
}
}
}
}
}
DOMBehavior
has a property called domEvents
, which accepts an object where each key is a DOM event name, and values are handler functions for those events. By default, DOMBehavior
will attach these events to its parent in the hiararchy. In the eample above this is the Button
. We can also specify a different element to attach these events to using the target
property:
Button{ id: customButton T`Custom button`}
DOMBehavior{
target: customButton
domEvents = {
click: (e) => {
alert('Button Clicked')
}
}
}
DOMBehavior
works both when rendered on the client and on the server side. Behind the scenes, DOMBehavior
does the following:
Button
's functionality and attach it to the rendered page. If multiple DOMBehavior
components are used throghout the page, each one will get stored with it's functionality inside the script. When the page gets rendered, the script will attach all the behaviors to the marked DOM elements.Note that DOMBehavior
event handlers should only be used with html DOM elements. When implementing these functions, do not use or reference any live elements objects.
Button{
id: customButton
T`Clickable`
string message: ''
DOMBehavior{
domEvents = {
click: (e) => {
alert(customButton.message)
}
}
}
}
In the example above, if using server side rendering, the reference to button.message
will be lost on the client side, and this will error. You can set an attribute to the DOM instead:
Button{
T`Clickable`
props = { data: { message: 'click message' } }
DOMBehavior{
domEvents = {
click: (e) => {
alert(e.target.dataset.message)
}
}
}
}
So, why use DOMBehavior
? The main reason is modularity. Using DOMBehavior
inside a component makes it reusable without needing to attach any extra scripts to a page. A component, whether client side or server side rendered, can encapsulate it's entire logic inside DOMBehavior
s.
Take a tree-view for example. This website uses it for it's sidebar navigation. The tree-view has minimal functionality, as it only needs to collapse or expand its items when clicked. We can delegate that functionality with DOMBehavior
, and create a reusable TreeView
component quite easily:
import live-web.dom
import live-web.behavior
import live-web.model
component TreeNode < State{
default children: []
string label: ''
boolean expanded: false
}
component TreeNodeView < Li{
id: treeNodeView
TreeNode node: null
Button{
Span{
T{
text: (treeNodeView.node && treeNodeView.node.children.length > 0)
? (treeNodeView.node.expanded ? '-' : '+')
: ''
}
}
T{ text: treeNodeView.node ? treeNodeView.node.label : '' }
DOMBehavior{
domEvents = { click: (e) => {
const ul = e.target.parentNode.querySelector('ul')
if ( !ul.hasChildNodes() )
return
if ( ul.classList.contains('hidden') ){
e.target.parentNode.querySelector('span').innerHTML = '-'
ul.classList.remove('hidden')
} else {
e.target.parentNode.querySelector('span').innerHTML = '+'
ul.classList.add('hidden')
}
}}
}
}
TreeView{
classes: [treeNodeView.node && treeNodeView.node.expanded ? '' : 'hidden']
state: treeNodeView.node
}
}
component TreeView < Ul{
TreeNode state: null
children: this.state ? this.state.children.map(child => (TreeNodeView{ node: child })) : []
}
We're using a TreeNode
as a state. Each TreeNode
has a TreeNodeView
as it's view. The TreeNodeView
acts like a button that expands or collapses it's child nodes, which are recursively stored under a TreeView
component. To expand/collapse, we only use DOM object classes to check if the tree is expanded or not:
const ul = e.target.parentNode.querySelector('ul')
if ( ul.classList.contains('hidden') ){
e.target.parentNode.querySelector('span').innerHTML = '-'
ul.classList.remove('hidden')
} else {
e.target.parentNode.querySelector('span').innerHTML = '+'
ul.classList.add('hidden')
}
We can save the TreeView and associated components above in app/TreeView.lv
and then use them in the Home.lv
page:
import live-web.dom
import live-web.behavior
import live-elements-web-server.view
component Home < PageView{
head: PageProperties{
title: "Live Elements"
StyleLink{ href: '/styles/style.css' }
}
Body{
H1`Welcome to Live Elements!`
TreeView{
state: TreeNode{
TreeNode{ label: 'Chapter 1' }
TreeNode{ label: 'Chapter 2'
TreeNode{ label: 'Subchapter 1' }
}
TreeNode{ label: 'Chapter 3'
TreeNode{ label: 'Subchapter 1' }
TreeNode{ label: 'Subchapter 2' }
}
}
}
}
}
We can also apply a minimal style to the view to make it a bit more visible:
body{
color: black;
}
li{
list-style: none;
}
li button{
background: none;
border: none;
}
.hidden{
display: none;
}
This tutorial covered the following:
ViewRoute
to render server sidelvweb
to render in productionDOMBehavior
to attach events to both client and server rendered pagesDOMBehavior