DEV Community

Cover image for I Created a Programming Language to Build Front-End Web Apps Much Faster
Dinu SV
Dinu SV

Posted on

I Created a Programming Language to Build Front-End Web Apps Much Faster

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{}
Enter fullscreen mode Exit fullscreen mode

To inherit from another component, we use the < symbol:

component MySecondComponent < MyComponent{}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Inside the component body, we can declare properties:

component MyComponent{
    number a: 100
};
const m = MyComponent{}
console.log(m.a) // Output: 100
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

This syntax allows you to write:

const c = ComponentWithString`Hello!` // Output: Initialized with: Hello
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

P has a children property that is treated as default, the above is actually short for:

P{ children: [T{ text: 'This is a paragraph' }] }
Enter fullscreen mode Exit fullscreen mode

To shorten the expression even more, T can be created with a tagged string constructor mentioned above:

P{ T`This is a paragraph` }
Enter fullscreen mode Exit fullscreen mode

P can also be created with a tagged string constructor, so the above can be shortened even more:

P`This is a paragraph`
Enter fullscreen mode Exit fullscreen mode

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`
}
Enter fullscreen mode Exit fullscreen mode

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`
  }
}
Enter fullscreen mode Exit fullscreen mode

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`
}
Enter fullscreen mode Exit fullscreen mode

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'
    }
}
Enter fullscreen mode Exit fullscreen mode

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`
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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`
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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`
      }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)