When building front-end applications for the web, I always found it unintuitive to switch between HTML/XML-based syntax for the declarative parts of the application and JavaScript/TypeScript for the logic. This always seemed like an unnecessary complexity with most tools and frameworks - why not use a single language for both?
This is why I created Live Elements, a programming language extension to javascript (or typescript) that eliminates the need for XML-like languages entirely, and provides a seamless transition between logic and UI declarations.
Live Elements also adds:
- An easier way to deal with state management.
- A better solution to create components with scoped styles, and to use them in an application.
This article will give a quick overview of the main concepts behind Live Elements, but if you want to dive deeper, there's a step-by-step guide at liveelements.io/documentation taking you through all the concepts.
There's also a live playground where you can view and try different examples live.
Overview
Live Elements introduces the concept of components. A component works similarly to a javascript class, but comes with additional functionality. Here's how we can declare a basic component using the component
keyword:
component MyComponent{}
To inherit from another component, we use the <
symbol:
component MySecondComponent < MyComponent{}
Inheritance works similarly to as it does with javascript classes, meaning components can inherit functions and properties from their parent components.
Creating a component is a bit different from javascript, as we don't use the new
keyword, but use the component name followed by curly braces:
const mycomponent = MyComponent{} // similar to 'new MyComponent()' in javascript
Inside the component body, we can declare properties:
component MyComponent{
number a: 100
};
const m = MyComponent{}
console.log(m.a) // Output: 100
Notice how we combined javascript with Live Elements. const mc = ...
and console.log(mc.a)
are javascript expressions, while the component declaration and creation are part of Live Elements syntax.
When creating components, we can assign properties inside the component body:
component MyComponent{
number a: 100
}
const m = MyComponent{
a: 200 // reassign 'a' with 200
}
console.log(m.a) // Output: 200
Properties bind automatically to other properties. In the follwing example, when a
changes, b
changes as well:
component MyComponent{
number a: 100
number b: this.a // b is now bound to a
}
const m = MyComponent{}
console.log(m.b) // Output: 100
m.a = 200 // b is now 200 as well, since it's bound to a
console.log(m.b) // Output: 200
Constructors are declared in a similar way to javascript constructors, but they must explicitly call the super()
method and this{}
expression. this{}
expression is used to initialize the component, and to create and assign component properties:
component ComponentWithConstructor{
constructor(a:number, b:number){
super()
this{}
console.log("Initialized with:", a, b)
}
}
To create a component that uses a constructor, include a dot and parentheses after the component name, and before the curly braces:
const c = ComponentWithConstructor.(100, 200){} // Output: Initialized with 100 200
Live Elements provides a shortcut to create components with a constructor taking a single string argument. Instead of using parentheses, you can use a tagged string immediately after the component name. Here's an example:
component ComponentWithString{
constructor(s:string){
super()
this{}
console.log("Initialized with:", s)
}
}
This syntax allows you to write:
const c = ComponentWithString`Hello!` // Output: Initialized with: Hello
Components can have children declared inside the component body. The following component is created using an array of children:
component A{}
component ComponentWithChildren{
Array<BaseElement> children
}
const c = ComponentWithChildren{
children: [
A{},
A{}
]
}
console.log(c.children.length) // Output: 2
However, the same component can be declared with a default
property, which is a property that gets automatically assigned with the children declared inside the component body:
component A{}
component ComponentWithChildren{
// declares a default property, which is implicitly of type Array<BaseElement>
default children
}
const c = ComponentWithChildren{
A{} // Add children directly to the component body
A{}
}
console.log(c.children.length) // Output: 2
The default
property makes it more natural to add child components inline, keeping the syntax clean and intuitive.
DOM and Web pages
We can now have a look at how the above concepts can be applied to the web. Html tags like <div>
, <p>
, <h1>
and so on are treated as components in Live Elements. Additionaly, there's a T
component which represents a text node. A paragraph for example, can be written like this:
P{ T{ text: 'This is a paragraph' }} // equivalent to <p>This is a paragraph</p>
P
has a children
property that is treated as default
, the above is actually short for:
P{ children: [T{ text: 'This is a paragraph' }] }
To shorten the expression even more, T
can be created with a tagged string constructor mentioned above:
P{ T`This is a paragraph` }
P
can also be created with a tagged string constructor, so the above can be shortened even more:
P`This is a paragraph`
You can try these examples in the playground. Simply paste the following code to see it in action:
import live-web.dom
import live-elements-web-server.view
component Index < PageView{
P`This is a paragraph`
}
We haven’t covered imports in Live Elements to keep this article shorter and more focused, but here’s a quick overview: an import uses the import
keyword followed by the module URI, and it brings the module into the global scope. For example, importing live-elements-web-server.view
makes PageView
globally accessible, and importing live-web.dom
will make DOM elements like P
and Div
globally accessible.
Here's another example that demonstrates creating a <div>
with a paragraph and a header:
import live-web.dom
import live-elements-web-server.view
component Index < PageView{
Div{
H1`This is a header.`
P`This is a paragraph`
}
}
Creating a Button
Components also support events and listeners. You can declare a listener for an event using the on
keyword, and assign it a function. For example, this is how you can create a button that listens to for a click
event:
Button{
on click: () => {
console.log('button clicked')
}
T`Click here`
}
Referencing other components
Components inside hierarchies can reference each other using the id
property:
component D{
id: myid
string data: 'Welcome Message'
P{
T{ text: myid.data } // text is now 'Welcome Message'
}
}
Creating a Counter
So far, we've covered:
- Components, constructors and children
- Component property bindings
- Web DOM integration
- Event listeners
- Component ids
With these concepts, we can build a simple counter example to show how everything works together. You can paste the following code into the playground to see it in action:
import live-web.dom
import live-elements-web-server.view
import live-elements-web-server.style
component Counter{ // Counter state
number value: 0
}
component Index < PageView{
id: index // this can now be referenced via 'index'
// counter property is now a new Counter
Counter counter: Counter{}
Div{
// we reference index.counter.value
P{ T{ text: index.counter.value }}
Button{
// increment index.counter.value when button is clicked
on click: () => { index.counter.value++ }
T`Increment`
}
}
}
The paragraph P
has a text node T
, which is bound to index.counter.value
, which will update once the button is clicked.
Styling the counter
Components in Live Elements can use scoped styles to apply styles to themselves and their children. To add a scoped style to a component, include a ScopedStyle
component to the static use
property:
import live-web.dom
import live-elements-web-server.view
import live-elements-web-server.style
component Counter{
number value: 0
}
component Index < PageView{
id: index
static any[] use = [
// use index.css stylesheet
ScopedStyle{ src: './index.css'}
]
Counter counter: Counter{}
Div{
P{ T{ text: index.counter.value }}
Button{
on click: () => { index.counter.value++ }
T`Increment`
}
}
}
In the playground, we’ve included the index.css
stylesheet, which is already available as part of the playground workspace. This allows us to style the counter directly by defining styles in the index.css
file:
p{
font-size: 30px;
color: blue;
}
Components in Live Elements can use stylesheets, but they can also import styles from other components. For example, if component A
uses a a.css
stylesheet, and component Index
uses component A
, the styles from a.css
will be automatically included in component Index
.
To avoid conflicts, all style selectors in a.css
are prefixed with a unique selector for component A
, ensuring the styles remain scoped to A
and don’t interfere with other components.
This feature makes it easy to create and reuse ready-made components with their own encapsulated styles. For instance, there's a pre-styled button component in live-web-view.button
module called PrimaryButton
. Here’s how you can use it for the counter example:
import live-web.dom
import live-web-view.button
import live-elements-web-server.view
import live-elements-web-server.style
component Counter{
number value: 0
}
component Index < PageView{
id: index
static any[] use = [
// use PrimaryButton styles
PrimaryButton,
// use index.css stylesheet
ScopedStyle{ src: './index.css'}
]
Counter counter: Counter{}
Div{
P{ T{ text: index.counter.value }}
// use PrimaryButton instead of Button
PrimaryButton{
on click: () => { index.counter.value++ }
T`Increment`
}
}
}
Conclusion
In this article, we’ve explored some of the core concepts of Live Elements. We looked at building a pratical example like a counter and demonstrated how to leverage stylesheets and pre-styled components to create reusable, encapsulated UI elements.
By combining the declarative and logical aspects into a single, unified language, Live Elements simplifies the development process and makes it faster to build features.
Top comments (0)