Create an app from scratchContribute
Welcome to this guide on how to create your first application using Lenra technology. In this guide, we will take you through the process of building an application that works similar to the provided application templates. Whether you are a beginner or have some experience in programming, this guide will help you create your own functional application with ease.
Lenra is a cutting-edge technology that provides developers with the ability to create applications that are efficient, reliable, and scalable. With Lenra, you can build applications for a wide range of purposes, including web applications, mobile applications, and desktop applications.
This guide assumes that you have some basic knowledge of programming concepts and are familiar with at least one programming language. We will be using JavaScript as the primary language for this guide, but you can use any language of your choice as Lenra is language-agnostic.
So, if you are ready to embark on the journey of building your first Lenra application, let's get started!
Project configuration
Initialize the directory
We'll first create a new directory that will contain our app:
# create a new directory for your app
mkdir my-app
# go into it
cd my-app
# create the views directory
mkdir -p src/views
# create the listners directory
mkdir -p src/listeners
# create the classes directory
mkdir -p src/classes
# create the resources directory
mkdir -p src/resources
Initialize git:
# ignore some elements
echo '### Node
node_modules/
.npm
.config
.DS_Store
.lenra/' > .gitignore
# initialize git versionning
git init
And initialize npm:
# initialize npm project
npm init -y
# define the project type as module
npm pkg set type='module'
You can now open the project in your favorite IDE:
code .
Import dependencies
To make the app creation simpler we've created two libraries.
App lib
The first library is the app lib that contains all the elements to run the application and call the Lenra API. This makes the Lenra app simpler to understand since only the views, listeners and resources are kept in the app sources.
You can find one for your favorite language on GitHub.
npm i @lenra/app-server
We can now define two npm scripts:
# Start the app server
npm pkg set scripts.start='app-lenra'
# Index the views and listeners to have autocompletion
npm pkg set scripts.index='app-lenra index'
Components lib
The second library is the components lib that contains the interface components to create the views. It adds autocompletion and it's simpler to describe the views compared to describing it as JSON.
You can find one for your favorite language on GitHub.
npm i @lenra/components
Creation of the app
Now that your project is configured we will define your app manifest and your first view.
The manifest and system listeners
The manifest describes some static values for your app, like the root view. The app-lib manages the manifest by importing the src/manifest.js
file.
export const rootView = "hello";
We also will define a file containing the Lenra's system events (src/listeners/systemEvents.js
) that must be implemented by the apps:
export async function onEnvStart(_props, _event, api) {
// this event is dispatched when your app is deployed
}
export async function onUserFirstJoin(_props, _event, api) {
// this event is dispatched when a user joins your app for the first time
}
export async function onSessionStart(_props, _event, _api) {
// this event is dispatched when a user starts using your app
}
Your first view
We will now create the first view of our app in the file src/views/hello.js
. The app-lib will manage all the exported functions in the src/views
directory as views and in the src/listeners
directory as listeners. This view will only display the text Hello world
in the center of the screen.
import { Container, Text } from "@lenra/components";
export default function (_data, _props) {
return Container(
Text("Hello World")
).alignment("center")
}
Start your app server with the following command:
npm start
You can now test your app by like that:
# with wget
wget -q --output-document - --post-data='{"view": "hello"}' --header='Content-Type:application/json' http://localhost:3000/
# with curl
curl --header "Content-Type: application/json" --request POST --data '{"view": "hello"}' http://localhost:3000/
You should receive the following response:
{"type":"container","child":{"type":"text","value":"Hello World"},"alignment":"center"}
Packaging your app
Now that your app works (even if it's a basic app it's still an app) we will create the lenra.yml
file. It describes how to package your app and run it.
To get more informations about how to create this file, see the dedicated page.
For this guide we will just use the JavaScript template's one explaining it:
componentsApi: "1.0"
# Your app generator
generator:
# Using the dofigen generator
dofigen:
builders:
# creation of a build that will only install the dependencies to keep some cache
- from: "docker.io/bitnami/node:18"
workdir: /app
# change the /app directory owner
root:
script:
- chown -R 1000 /app
# add json files
adds:
- "*.json"
# install the dependencies
script:
- npm ci
# the main layer based on the previous builder
from: builder-0
# add all the files of the context
adds:
- .
# define how to start the app
cmd:
- npm
- start
# the exposed port of your app
ports:
- 3000
healthcheck:
cmd: curl --fail http://localhost:8080/_/health
start: 3s
interval: 3s
timeout: 1s
# manage elements ignored from the Docker build context
ignores:
- "**"
- "!/*.json"
- "!/src/"
You can now run the lenra dev
command to build and start your app, and your terminal will enter an interactive mode. In this mode, you will see your app's logs and will be able to run commands with keyboard shortcuts (we'll use one later).
lenra dev
To see your app, just go to localhost:4000
Great ! You've created an hello world app ! Let's see how to manage the views and data by adapting it to get the template counter app.
The application elements
Now that we have a basic app we will implement our counter app elements.
The counter view
First we'll create the counter view. Its purpose is to display the value of a counter in a text component and define a button to increment it. We'll put them together in a flex component.
Let's define it in the new src/views/counter.js
file:
import { Flex, Text, Button } from "@lenra/components";
/**
* @param {import("@lenra/app-server").data} _data
* @param {import("@lenra/app-server").props} _props
* @returns
*/
export default function (_data, _props) {
const count = 0;
return Flex([
Text(`Counter value: ${count}`),
Button("+")
])
// add some space between the children
.spacing(16)
// define how the elements are positionned
.mainAxisAlignment("spaceEvenly")
.crossAxisAlignment("center")
}
To see it, we need to define the rootView
field in the src/manifest.js
at the value "counter"
.
export const rootView = "counter";
To reload (rebuild and restart) your app just press the R
key while your terminal is in interactive mode.
You will see your counter, but nothing happens when you click on the button. Of course, we've defined the counter value with a constant value of 0. Let's see how to dynamise it.
The counter data
To dynamise our counter view we will need to manage a state to it. With Lenra manage a state, you will need to store it in the MongoDB database of your app.
To manage your app data you will need to use our API from a listener. The JavaScript app-lib eases the data management by using classes.
The Counter
class
We will create a new class file, src/classes/Counter.js
, for the Counter
class:
import { Data } from "@lenra/app-server";
export class Counter extends Data {
/**
*
* @param {string} user
* @param {number} count
*/
constructor(user, count) {
super();
this.user = user;
this.count = count;
}
}
Counter class contains two fields:
count
: the counter value.user
: the user that own the counter. We will use it later.
Create the global counter
We will use the system event listeners that we've created in the first part of this guide to create our first counter with the user
field defined to "global"
.
Edit the src/listeners/systemEvents.js
to implement the onEnvStart
listener:
import { Counter } from "../classes/Counter.js";
/**
* This event is dispatched when your app is deployed
* @param {import("@lenra/app-server").props} _props
* @param {import("@lenra/app-server").event} _event
* @param {import("@lenra/app-server").Api} api
*/
export async function onEnvStart(_props, _event, api) {
// get the global counters
const counters = await api.data.find(Counter, { user: "global" })
// if there is none, create one
if (counters.length == 0) {
await api.data.createDoc(new Counter("global", 0));
console.log("Global counter created");
}
}
export async function onUserFirstJoin(_props, _event, api) {
// this event is dispatched when a user joins your app for the first time
}
export async function onSessionStart(_props, _event, _api) {
// this event is dispatched when a user starts using your app
}
Since the event we use to create our first counter is only dispatched when the app is deployed, to dispatch it locally we have to stop our app and start it again.
If your in the interactive mode, just press the S
key to stop it. All your container will stop and the data will be cleared.
You can now re-start your app with lenra dev
.
Refresh your app in your browser to see the log that we've added in your app logs: Global counter created
Great, the data is created, but how to use it ?
Make the view dynamic
Now that we have a global counter data we will use it in our counter view. To use data in a view you have to query it in your view declaration.
Our first step is to create a parent view to declare our counter view with the query. We also give a text
property to our view.
Let's create the new src/views/home.js
file and set it as the root view:
import { DataApi } from "@lenra/app-server";
import { View } from "@lenra/components";
import { Counter } from "../classes/Counter.js";
export default function (_data, _props) {
return View("counter")
.data(DataApi.collectionName(Counter))
.props({ text: "The common counter" })
}
export const rootView = "home";
We'll now adapt our counter view to use the response of the query and the given text
property. We also will call an increment
listener with the counter id in the id
property when the button is pressed:
import { Flex, Text, Button } from "@lenra/components";
/**
*
* @param {import("../classes/Counter").Counter[]} param0
* @param {import("@lenra/app-server").props} param1
* @returns
*/
export default function ([counter], { text }) {
return Flex([
Text(`${text}: ${counter.count}`),
Button("+")
.onPressed("increment", { id: counter._id })
])
// add some space between the children
.spacing(16)
// define how the elements are positionned
.mainAxisAlignment("spaceEvenly")
.crossAxisAlignment("center")
}
Let's implement the increment
listener.
The increment
listener
Now that your counter view is based on your counter data we will implement the increment
listener to increment the given counter count
field in a new src/listeners/increment.js
file:
import { Counter } from "../classes/Counter.js";
/**
*
* @param {import("@lenra/app-server").props} props
* @param {import("@lenra/app-server").event} _event
* @param {import("@lenra/app-server").Api} api
* @returns
*/
export default async function(props, _event, api) {
// Get the wanted counter data
let counter = await api.data.getDoc(Counter, props.id);
// increment the count field
counter.count += 1;
// update the data in the base
await api.data.updateDoc(counter);
}
If you reload your app (press R
key), you'll now have a working counter.
A user specific counter
Now that we have a working counter we will create another one but this time it will be specific to the current user. To target the current user while managing data with Lenra you can use the @me
keyword that will be replaced by the current user id.
Let's create the data when the user join the app for the first time by editing our src/listeners/systemEvents.js
file:
import { Counter } from "../classes/Counter.js";
/**
* This event is dispatched when your app is deployed
* @param {import("@lenra/app-server").props} _props
* @param {import("@lenra/app-server").event} _event
* @param {import("@lenra/app-server").Api} api
*/
export async function onEnvStart(_props, _event, api) {
// get the global counters
const counters = await api.data.find(Counter, { user: "global" })
// if there is none, create one
if (counters.length == 0) {
await api.data.createDoc(new Counter("global", 0));
console.log("Global counter created");
}
}
/**
* This event is dispatched when a user joins your app for the first time
* @param {import("@lenra/app-server").props} _props
* @param {import("@lenra/app-server").event} _event
* @param {import("@lenra/app-server").Api} api
*/
export async function onUserFirstJoin(_props, _event, api) {
// get the current user counters
const counters = await api.data.find(Counter, { user: "@me" })
// if there is none, create one
if (counters.length == 0) {
await api.data.createDoc(new Counter("@me", 0))
console.log("User counter created");
}
}
export async function onSessionStart(_props, _event, _api) {
// this event is dispatched when a user starts using your app
}
Now we adapt the home view to have the two counters (user specific and global) and some layout:
import { DataApi } from "@lenra/app-server";
import { Flex, View } from "@lenra/components";
import { Counter } from "../classes/Counter.js";
export default function (_data, _props) {
return Flex([
View("counter")
.data(DataApi.collectionName(Counter), { user: "@me" })
.props({ text: "My personnal counter" }),
View("counter")
.data(DataApi.collectionName(Counter), { user: "global" })
.props({ text: "The common counter" }),
])
}
If you reload your app now, you'll have a problem since the current user you're making your tests with has already joined the app. To resolve this, you can also implement the onSessionStart
listener to check every time the user start the app that there is a counter for him or stop and restart your Lenra app to clear the data.
To explain how to test your like there is many users we will use a third solution: giving a user in the URL wth the user
query param.
If you want to try with more users just increment the user
query param. By opening many tabs you will see that when you increment the common counter it will automatically update the value for all the tabs at the same time. It works the same way for the user specific counter if you open many tabs with the same value for the user
query param.
Let's add some layout and autocompletion
Now that we have a working app we will make it a little prettier by adding some layout. But let's use the indexer to target our views and listeners with autocompletion.
To index your app elements you just have to run the next command:
npm run index
This command will generate a src/index.gen.js
file that contains some constants with the names of your views and listeners.
We now can use it by importing the file in our modules.
Let's add some layout to our home view and replace the counter view name by the constant:
import { DataApi } from "@lenra/app-server";
import { Flex, View } from "@lenra/components";
import { Counter } from "../classes/Counter.js";
import { views } from "../index.gen.js";
export default function (_data, _props) {
return Flex([
View(views.counter)
.data(DataApi.collectionName(Counter), { user: "@me" })
.props({ text: "My personnal counter" }),
View(views.counter)
.data(DataApi.collectionName(Counter), { user: "global" })
.props({ text: "The common counter" }),
])
.direction("vertical")
.spacing(16)
.mainAxisAlignment("spaceEvenly")
.crossAxisAlignment("center")
}
Now we will create a top menu bar containing the Lenra logo and a centered title:
import { Container, Flex, colors, padding, Image, Flexible, Text } from "@lenra/components";
export default function(_data, _props) {
return Container(
Flex([
Container(
Image("logo.png")
)
.width(32)
.height(32),
Flexible(
Container(
Text("Hello World")
.textAlign("center")
.style({
"fontWeight": "bold",
"fontSize": 24,
})
)
)
])
.fillParent(true)
.mainAxisAlignment("spaceBetween")
.crossAxisAlignment("center")
.padding({ right: 32 })
)
.color(colors.Colors.white)
.boxShadow({
blurRadius: 8,
color: 0x1A000000,
offset: {
dx: 0,
dy: 1
}
})
.padding(padding.symmetric(32, 16))
}
Download the Lenra logo like that:
wget -O src/resources/logo.png https://raw.githubusercontent.com/lenra-io/template-javascript/main/src/resources/logo.png
Now we can create a main view that contains our menu and home view:
import { Flex, View } from "@lenra/components";
import { views } from "../index.gen.js";
export default function (_data, _props) {
return Flex([
View(views.menu),
View(views.home)
])
.direction("vertical")
.scroll(true)
.spacing(4)
.crossAxisAlignment("center")
}
And that's it ! Your template app is finished.
You can test with the lenra check template
command.
From the interactive mode of Lenra CLI press Ctrl + C
to enter in the Lenra terminal. You can now run directly Lenra commands like those ones:
# Expose your application port (always 8080)
expose app
# check the template
check template