Lightning 3: The Basics of Blits
Back in September, the first open beta of Lightning 3 was announced. As part of that announcement, two different options for developing Lightning applications were shown: SolidJS, and Blits. In this blog, we’ll take a look at some of the basics of developing an application with Blits.
Getting started
First and foremost, you’ll need to get a project scaffolded. That’s luckily rather easy, with the following command:
npx @lightningjs/blits@latest my_lightning3_app
After running that, your journey into Lightning3 is ready to get started. You’ll notice the following directory structure, which is used to separate your various files and folders in specifically the src directory:
- index.js → the entrypoint of your running application, executes the launching of your app.
- App.js → the app instance, used to define the base template of your app and specify the routes.
- fontloader.js → used for font purposes, can be left untouched.
- renderer.js → initialises the lightning renderer, can be left untouched.
- components → the place for your components, although this is not static (we are dealing with javascript after all, with regular imports).
- pages → define your pages (or routes) here, can also be changed.
index.js
Taking a look at the index.js, you’ll see that the App is launched with Blits, along with a bunch of configurable options:
Blits.Launch(App, 'app', {
w: 1920,
h: 1080,
multithreaded: false,
debugLevel: 1,
fontLoader: fontLoader,
fonts: [
{family: 'lato', type: 'msdf', png: '/fonts/Lato-Regular.msdf.png', json: '/fonts/Lato-Regular.msdf.json'},
{family: 'raleway', type: 'msdf', png: '/fonts/Raleway-ExtraBold.msdf.png', json: '/fonts/Raleway-ExtraBold.msdf.json'},
{family: 'opensans', type: 'web', file: '/fonts/OpenSans-Medium.ttf'}
],
})
In this config object, you can, among others, define the expected width and height of the application, whether or not to use multithreading with ThreadX, the debuglevel and of course the fonts. This is also the location to define a custom keymap you might have for a specific device you’re targeting.
App.js
As mentioned, App.js is the actual entry point of your application, and comes with the following scaffolded signature:
import Home from './pages/Home.js'
export default Blits.Application({
template: `
<Element>
<RouterView />
</Element>
`,
routes: [{ path: '/', component: Home }],
})
This is the perfect moment to define global widgets you might want to have (like a menu, modal or player), as well as setting up the routing for your application. The Blits compiler is smart enough to recognise what the ` RouterView
is, with it being a default component within Blits. So no need for imports of the RouterView
.
Basic element structure
You might have noticed the <Element></Element
tag in the App.js example as shown above. The Element
component is the base of all components written within Blits. Like the RouterView, the compiler recognises the Element
, no need to import this. And through that element tag, you can write, customise and style your own elements. These elements come with a lot of styling and configuration options that allow you to customise the appearance. Say for example we wanted to create a simple button, we can define that with the following:
<Element w="300" h="150" color="#00FF00">
<Text align="center" w="300" y="55">My Button</Text>
</Element>
In my example above, I have created a green button of size 300x150. You can also see that I have used another built-in element called <Text>
that is used to, well, display text. One of the currently kind of annoying things is the centering of text, which isn’t super easy. Horizontally goes rather easy with the align
property. For vertical alignment, I had to resort to another important aspect of Blits (and Lightning in general): positioning with x
and y
coordinates. With TV applications being static in their size, most elements on the page can generally be positioned with x and y components; similarly how width and height values can often be defined in pixels, given that screens don’t resize.
So, now that we have a button, let’s take separation of concerns into account. Because we don’t want to keep writing buttons from scratch.
Custom Components
Starting with a new custom component, we’ll need to create a new file. In my case, I’ll create it in the components
folder as scaffolded at the start. We’ll name it Button.js, and populate our file with the button we defined earlier:
import Blits from '@lightningjs/blits'
export default Blits.Component('Button', {
template: `
<Element w="300" h="150" color="#00FF00">
<Text w="300" y="55" align="center">My Button</Text>
</Element>
`,
})
As you can see in the above snippet, we’ve defined our button and exported it as aButton
component with export default Blits.Component('Button', {})
. Now, all we need to do is inject it into our page. For that we’ll have to import the file into any page (or other component) we want to use our newly created Button, and define it in the components
object:
import Blits from '@lightningjs/blits'
import Button from '../components/Button.js'
export default Blits.Component('Home', {
components: {
Button,
},
template: `
<Element w="1920" h="1080" color="#1e293b">
<Button/>
</Element>
`,
})
This is our Home page, which, as you can see, is also defined as a component. Pages don’t actually exist in Blits, isn’t that neat? You can throw any component into the router you might want. Whether you should… I’ll leave that up to you ;). But here you can also see how we import, pass along and finally use our button.
Advanced custom components
As much as I like having a full app that only has 300 ‘my button’ texts, that’s not how real apps work. So let’s make our custom component a bit more useable in a real life scenario.
Passing props
Content changes, especially for our custom components that we’ll reuse in many places of our application. So let’s start by defining a set of properties that can be passed along to our button:
import Blits from '@lightningjs/blits'
export default Blits.Component('Button', {
template: `
<Element w="$width" h="$height" color="$color">
<Text w="$width" y="55" align="$align" content="$text"/>
</Element>
`,
props: ['text', 'width', 'height', 'align', 'color'],
})
So, all our values are now properties. You’ll see that, in order to reference our props, we use the $
prefix. Using the dollar-prefix, makes our properties dynamic, meaning the template changes based on the value of our property. With all this, it means we can start defining them in the place we are using our button:
<Element w="1920" h="1080" color="#1e293b">
<Button text="my custom button" width="300" height="150" align="center" color="#00FF00" textY="55"/>
<Button text="my second button" y="200"/>
</Element>
Which then gives us the following result:
Oh, yeah, that second button looks a bit shitty. That’s because we haven’t given it all the expected property values. One way to make things a bit nicer, is by defining default values for some (or all) properties:
props: [
{
key: 'text',
default: 'My button',
},
{
key: 'width',
default: 300,
},
{
key: 'height',
default: 150,
},
{
key: 'color',
default: '#FF0000',
},
'align',
'textY',
],
In the above example, we’ve defined some simple default values for some of our properties. Note there are more options available, you can for example apply constraints to what the values can be, for a bit more security and control. Regardless of that, our results look better immediately:
Reactive values, focus and input events
Our static components are looking nice, but I can imagine you need some more freedom for your components. For example, maybe the text should change when you have clicked enter. Imagine a favorite button that needs to change from ‘Add to favorites’ to ‘Remove from favorites’ on enter.
First, we’ll define a completely new component for our button. This time we’ll use the :
prefix for our content
. This makes the content property reactive, meaning it will rerender the text element when any of the values passed to it, change. In this case, we’ll use a ternary expression to change the text, based on a boolean value.
<Text :content="$isFavorited ? $unfavoriteText : $favoriteText"></Text>
But where is the isFavorited
value coming from? We might not want to pass it along as a property, but rather define it inside the component itself. For that, we can use the state
function/object and define our boolean value, as well as our texts:
state() {
return {
isFavorited: false,
favoriteText: 'Add to favorites',
unfavoriteText: 'Remove from favorites',
}
},
And finally, we have to make sure that we can trigger the enter key inside this component to change the text shown. Before that though, we have to make sure that our component has focus, as focus on a component determines which input event handlers get triggered. So first we’ll define our component in our Home page, to put focus on our newly created button:
export default Blits.Component('Home', {
components: {
FavoriteButton,
},
template: `
<Element w="1920" h="1080" color="#1e293b">
<FavoriteButton ref="btn"></FavoriteButton>
</Element>
`,
hooks: {
focus() {
this.select('btn').focus() // Select our button with the ref 'btn'
},
},
})
So now that we’ve put the focus on our favorite button, we can finalise the implementation and use the enter input event to change the state. Of course, if you want to handle different input for a similar action (or for example to shift focus), you can use the other input events like left, up, down and right.
export default Blits.Component('FavoriteButton', {
template: `
<Element>
<Text :content="$isFavorited ? $unfavoriteText : $favoriteText"></Text>
</Element>
`,
state() {
return {
isFavorited: false,
favoriteText: 'Add to favorites',
unfavoriteText: 'Remove from favorites',
}
},
input: {
enter() {
this.isFavorited = !this.isFavorited
},
},
})
For loops
Another important and very basic aspect of applications are for loops. You of course want to be able to loop through for example an API response, and display a certain amount of cards in a carousel. For this, we can use the :for
property as part of custom components. Making use of our earlier created Button
component, we can define a for loop within our template:
export default Blits.Component('Home', {
components: {
Button,
},
template: `
<Element w="1920" h="1080" color="#1e293b">
<Button :for="(item, index) in $items" :x="310 * $index" :text="$item"></Button>
</Element>
`,
state() {
return {
items: [1, 2, 3, 4, 5],
}
},
})
In this example, I’m looping over a simple array of indexes and passing along the text as a property to our Button component. Notice that I’m also setting the x
value based on the index. This can be very useful when you’re creating a carousel or list, and want to space the elements evenly next to each other. The above loop creates the following result:
And that’s the basics of Blits!
… sort of. Of course, there are more properties, nuances and options that I haven’t touched upon yet. If anything, Blits is still in development and some things might even become easier in the near future. For example, we don’t have a flexbox implementation in Blits yet, which would make certain things a lot easier. But the above examples and possibilities should, I hope, set you up for the majority of your development in Blits.
There are of course also official docs you can refer to, which you can find here: https://lightning-js.github.io/blits/. If something is missing from the docs, I’d recommend either joining the Discord Community, or even reaching out to me directly. Regardless, I hope this has helped you kickstart your development journey with Lightning3 and Blits!