Lenra Docs
Register on Lenra
  • Home
  • Getting started
    Open/Close
  • Guides
    Open/Close
  • Features
    Open/Close
  • References
    Open/Close
  • Contribute

    Create an Lenra template from scratch

    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/lenra
    # 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 a library that let you define your app resources, but will manage the rest.

    You can find equivalent libraries for your favorite language on GitHub.

    For JavaScript, we've created the @lenra/app library that contains the app server and the Lenra API client and Lenra components for both JSON views and Lenra views (still in beta).

    npm i @lenra/app
    

    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'
    

    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 routes and there corresponding views. The app-lib manages the manifest by importing the src/manifest.js file.

    There is two kind of routes and views in Lenra apps:

    If you want to know more about the views differences, see the principles page.

    The app templates use both kind routes and views, but you can use only one of them if you want.

    import { View } from "@lenra/app";
    
    /**
     * @type {import("@lenra/app").Manifest["json"]}
     */
    export const json = {
        routes: [
            {
                path: "/hello",
                view: View("hello")
            }
        ]
    };
    
    /**
     * @type {import("@lenra/app").Manifest["lenra"]}
     */
    export const lenra = {
        routes: [
            {
                path: "/",
                view: View("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 that will be used by both Lenra and JSON routes. 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/app
    

    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
        # 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 JSON views implementation

    Now that we have a basic app we will implement our counter app elements starting with the JSON view.

    If you want to get the app result look at the examples in our client lib projects:

    The counter JSON view

    First we'll create the counter view. Its purpose is to display the value of a counter and define a listener to increment it.

    Let's define it in the new src/views/counter.js file:

    /**
     * 
     * @param {*} _data 
     * @param {*} _props 
     * @returns {import("@lenra/app").JsonViewResponse}
     */
    export default function (_data, _props) {
      const count = 0;
      return {
        value: count,
        onIncrement: {}
      };
    }
    

    To see it, we need to define the view in the manifest.

    /**
     * @type {import("@lenra/app").Manifest["json"]}
     */
    export const json = {
        routes: [
            {
                path: "/counter/global",
                view: View("counter")
            }
        ]
    };
    

    To reload (rebuild and restart) your app just press the R key while your terminal is in interactive mode.

    Refresh the client and 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. To manage a state with Lenra, 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";
    
    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:

    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) {
        const counterColl = api.data.coll(Counter);
        // get the global counters
        let counters = await counterColl.find(Counter, { user: "global" })
        // if there is none, create one
        if (counters.length == 0) {
            await counterColl.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 directly in the manifest.

    // add the Counter class import
    import { Counter } from "./classes/Counter.js";
    
    /**
     * @type {import("@lenra/app").Manifest["json"]}
     */
    export const json = {
        routes: [
            {
                path: "/counter/global",
                view: View("counter")
                  // add the find query
                  .find(Counter, {
                    "user": "global"
                  })
            }
        ]
    };
    

    We'll now adapt our counter view to use the response of the query. We also will call an increment listener with the counter id in the id property:

    import { Listener } from "@lenra/app";
    import { Counter } from "../classes/Counter.js";
    
    /**
     * 
     * @param {Counter[]} param0 
     * @param {*} _props 
     * @returns {import("@lenra/app").JsonViewResponse}
     */
    export default function ([counter], _props) {
      return {
        value: counter.count,
        onIncrement: Listener("increment")
          .props({
            id: counter._id
          })
      };
    }
    

    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").props} props 
     * @param {import("@lenra/app").event} _event 
     * @param {import("@lenra/app").Api} api
     * @returns 
     */
    export default async function (props, _event, api) {
        // We call the MongoDB updateMany method to increment the counter
        await api.data.coll(Counter).updateMany({ _id: props.id }, {
            $inc: {
                count: 1
            }
        });
    }
    

    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 create a second route to have the two counters (user specific and global) and some layout:

    /**
     * @type {import("@lenra/app").Manifest["json"]}
     */
    export const json = {
        routes: [
            {
                path: "/counter/global",
                view: View("counter").find(Counter, {
                    "user": "global"
                })
            },
            {
                path: "/counter/me",
                view: View("counter").find(Counter, {
                    "user": "@me"
                })
            }
        ]
    };
    

    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.

    localhost:4000?user=2

    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.

    Lenra views implementation

    Now that we have working JSON views we will create Lenra views that will do the same thing.

    The counter Lenra view

    We will now create the Lenra view that will do the same thing as the JSON one, but with interface components. The view will also need a text property to have a different text for each counter.

    import { Flex, Text, Button } from "@lenra/app";
    
    /**
     * 
     * @param {import("../../classes/Counter").Counter[]} param0 
     * @param {{text: string}} param1 
     * @returns 
     */
    export default function ([counter], { text }) {
      return Flex([
        Text(`${text}: ${counter.count}`),
        Button("+")
          .onPressed("increment", {
            "id": counter._id
          })
      ])
        .spacing(16)
        .mainAxisAlignment("spaceEvenly")
        .crossAxisAlignment("center")
    }
    

    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
    

    Let's create our home view that will contain two counters:

    import { DataApi, Flex, View } from "@lenra/app";
    import { Counter } from "../../classes/Counter.js";
    
    export default function (_data, _props) {
        return Flex([
            View("lenra.counter")
                .find(Counter, {
                    user: "@me"
                })
                .props({ text: "My personnal counter" }),
            View("lenra.counter")
                .find(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/app";
    
    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/app";
    
    export default function (_data, _props) {
      return Flex([
        View("lenra.menu"),
        View("lenra.home")
      ])
        .direction("vertical")
        .scroll(true)
        .spacing(4)
        .crossAxisAlignment("center")
    }
    

    We will now add the views to the manifest:

    /**
     * @type {import("@lenra/app").Manifest["lenra"]}
     */
    export const lenra = {
        routes: [
            {
                path: "/",
                view: View("lenra.main")
            }
        ]
    };
    

    So the full manifest is:

    import { View } from "@lenra/app";
    import { Counter } from "./classes/Counter.js";
    
    /**
     * @type {import("@lenra/app").Manifest["json"]}
     */
    export const json = {
        routes: [
            {
                path: "/counter/global",
                view: View("counter").find(Counter, {
                    "user": "global"
                })
            },
            {
                path: "/counter/me",
                view: View("counter").find(Counter, {
                    "user": "@me"
                })
            }
        ]
    };
    
    /**
     * @type {import("@lenra/app").Manifest["lenra"]}
     */
    export const lenra = {
        routes: [
            {
                path: "/",
                view: View("lenra.main")
            }
        ]
    };
    

    And that's it ! Your template app is finished.