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

    Todo list with Lenra views

    Now that you know the basics of a Lenra app, we can create our first app : a basic TODO List.

    Note : To follow this guide, you must have basic knowledge about JavaScript and Mongo query language (see Data management). To make it understandable for everyone, we will not use the components library and only describe the views in JSON, but we will use the app library since we have one for the languages managed by our templates.

    First of all, we want to organize our model (database).

    We will have only one collection : the task collection that will store all of our tasks.

    A task item will contain 4 properties :

    Our document will look like that :

    {
        "_id": "ObjectId(634d692fcbd9f6704818309c)",
        "description": "Code my first app with Lenra",
        "done": false,
        "user": "a ref to the user"
    }
    

    Create the model class

    Now that we know what a task looks like, we want to create it in our app. To do that, we will create a new file in the classes directory : Task.js

    import { Data } from "@lenra/app-server";
    
    export class Task extends Data {
      /**
       * @param {string} description The description of the task to be done
       * @param {boolean} done The done status
       * @param {string} user The task owner
       */
      constructor(description, done, user) {
        super();
        this.description = description;
        this.done = done;
        this.user = user;
      }
    }
    

    Note that the _id field does not have to be managed since this class extends the Data class from the app library. This class is a simple class that allows us to easily create a class that represents a document in our database.

    Create a new task

    We now want to create a new view to let the user create tasks. To do that, we will create a new file in the views directory : addTaskForm.js

    In the addTaskForm.js view, we create a new function (and export it). In this function, we will first add a form component.

    export default function(data, props) {
      return {
        _type: "form",
        onSubmit: {
          _type: "listener",
          name: "submitTask"
        },
        child: {...}
      }
    }
    

    In this view, we define the onSubmit listener. This tells the UI to submit the submitTask listener when the form is submitted.

    Then we will declare the inputs in our form.

    export default function(data, props) {
      return {
        _type: "form",
        onSubmit: {
          _type: "listener",
          name: "submitTask"
        },
        child:
        // 1 - Flex component
        {
          _type: "flex",
          crossAxisAlignment: "center",
          spacing: 2,
          children: [
            // 2 - label
            {
              _type: "text",
              value: "Your task : "
            },
            // 3 - The textfield to type the description
            {
              _type: "flexible",
              child: {
                _type: "textfield",
                value: "",
                name: "description"
              }
            },
            // 4 - The button to submit the form
            {
              _type: "button",
              text: "Add",
              submit: true,
            }
          ]
        }
      }
    }
    

    Let's see what happens here :

    1 : The Flex component

    The form allows only one child. The reason is simple, it does not infer how you want to place your inputs in the form. That is why we add a Flex component that describes how the children will be placed in the UI.

    By default, the flex is horizontal. We add the crossAxisAlignment: "center" to vertically center the children and a spacing: 2 to add some spaces between the children.

    2 : The label

    This one simply adds a label before the textfield. That’s it !

    3 : Textfield

    This is a two in one.

    The textfield component simply adds a textfield with a default value to empty (value: “”). The name: “description” connects this textfield first form up in the component tree. This way, when the form will be submitted, the event will contain a description field that contains the value of the textfield :

    {
        "value": {
            "description": "valueOfTheTextfield"
        }
    }
    

    Then we have the flexible component. that allows the textfield to take all the remaining space in the flex above. As simple as that !

    4 : The submit button

    This component is a simple button in which we add the submit: true property. This connects the form to this button. This way, when the button is pressed, the form will be automatically submitted (the onSubmit listener will be called).

    Call the form

    Now that our form is ready, we just have to call it in our main.js component using the view component.

    export default function(data, props) {
      return {
        _type: "flex",
        direction: "vertical",
        crossAxisAlignment: "center",
        padding: {
          top: 16, 
          bottom: 16,
          left: 32,
          right: 32
        },
        spacing: 8,
        children: [
          {
            _type: "text",
            value: "Lenra Todo List",
            style: {
              fontWeight: "w800",
              fontSize: 22
            }
          },
          {
            _type: "view",
            name: "addTaskForm",
          }
        ]
      }
    }
    

    Now you can start your app in your terminal using

    lenra dev
    

    Then open your browser at http://localhost:4000

    Your app should look like that. You should be able to type some text in the textfield. But for now the “add” button does nothing. Let's change that !

    Create the submitTask listener

    We will now create the listener that will react to the “add” button pressed.

    Remember the name: "submitTask" property in the form onSubmit listener ? That’s the name of our listener. So create a submitTask.js file in the listeners directory and add this code to it :

    // 1 - import our Task model class
    import { Task } from "../classes/Task.js";
    
    /**
     * @param {import("@lenra/app-server").props} _props The listener props
     * @param {import("@lenra/app-server").event} event The event
     * @param {import("@lenra/app-server").Api} api The Lenra API interface
     * @returns 
     */
    // 2 - create the listener function with "props", "event" and "api" arguments
    export default function (_props, event, api) {
      // 3 - Call the API to create a new task document.
      return api.data.createDoc(
        new Task(event.value.description, false, "@me")
      );
    }
    

    1 : Import the Task model class

    We will use the Task class to create a new task document in our database by creating a new instance of it and give it to the createDoc method.

    2 : Create the function

    A listener function takes 3 parameters : props, event and api.

    In this function body we can call our Data API to create our new task.

    Keep in mind that the listener must execute relatively fast in order to avoid long loader. (less than a second ideally).

    3 : Create our task

    This is where the magic starts. The api.data.createDoc function will create a new document in the task collection.

    Remember our database model. The user field should contain the current user ID to be able to filter the task that belongs to the user. To do this, Lenra offers a shortcut to access contextual data. In our example @me is a reference to the current user ID and will be replaced automatically.

    When the document is added, the UI will be rebuilt in order to instantly display the updated interface to the user.

    You can restart the app by pressing the R key, refresh your browser and you should be able to create a new task now !

    But wait… The task is still not visible yet.

    List the user tasks

    Now that we can add new tasks in our database, let's list them in the interface.

    To do this, create a new taskList.js view that gets the tasks ids in the data parameter.

    import { DataApi } from "@lenra/app-server"
    import { Task } from "../classes/Task.js"
    
    export default function (data, _props) {
      return {
        _type: "flexible",
        child: {
          _type: "flex",
          direction: "vertical",
          scroll: true,
          children: taskList(data),
        }
      }
    }
    
    function taskList(tasks) {
      if (tasks == undefined || tasks.length <= 0) {
        return [{
          _type: "text",
          value: "No tasks"
        }]
      }
    
      return tasks.map(task => {
        return {
          _type: "view",
          name: "taskCard",
          find: {
            coll: DataApi.collectionName(Task),
            query: task
          }
        }
      })
    }
    

    As you can see, we use the same view component that we used to call the addTaskForm view. The only difference is the find property. This property is used to query the database and get the data we need to display.

    The coll property defines the collection where we want to run the query.

    Then the query is a simple mongo query with some adaptation due to our realtime update system (see the Data management limitations). This query will filter the task collection to give us only the task corresponding to the task _id. The result of this query is the data argument in our view function.

    I’m sure you will be able to create the taskCard view by yourself ! If you have some trouble, you can find the code below.

    /**
     * @param {import("../classes/Task.js").Task[]} param0 The task query result
     * @param {import("@lenra/app-server").props} _props 
     * @returns 
     */
    export default function ([task], _props) {
      return {
        _type: "actionable",
        onPressed: {
          _type: "listener",
          name: "toggleTask",
          props: {
            task: task._id,
          }
        },
        child: {
          _type: "container",
          padding: {
            bottom: 2,
            left: 5,
            right: 5,
            top: 2,
          },
          border: {
            bottom: {}
          },
          child: {
            _type: "flex",
            spacing: 2,
            fillParent: true,
            crossAxisAlignment: "center",
            mainAxisAlignment: "spaceBetween",
            children: [
              {
                _type: "flexible",
                child: {
                  _type: "text",
                  value: task.description,
                  style: {
                    decoration: task.done ? "lineThrough" : "none",
                  }
                }
              }, {
                _type: "actionable",
                onPressed: {
                  _type: "listener",
                  name: "deleteTask",
                  props: {
                    task: task._id,
                  }
                },
                child: {
                  _type: "icon",
                  value: "delete",
                  color: 0xFFFF0000
                }
              }]
          }
        }
      }
    }
    

    Now we want to call our taskList with some data in it. To do this, go to the main.js view.

    import { DataApi } from "@lenra/app-server";
    import { Task } from "../classes/Task.js";
    
    export default function (data, props) {
      return {
        _type: "flex",
        direction: "vertical",
        crossAxisAlignment: "center",
        padding: {
          top: 16, 
          bottom: 16,
          left: 32,
          right: 32
        },
        spacing: 8,
        children: [
          {
            _type: "text",
            value: "Lenra Todo List",
            style: {
              fontWeight: "w800",
              fontSize: 22
            }
          },
          // Call the taskList view with some data.
          {
            _type: "view",
            name: "taskList",
            find: {
              coll: DataApi.collectionName(Task),
              query: {
                "user": "@me"
              },
              // We only need the _id field to display the task list.
              projection: { "_id": true }
            }
          },
          {
            _type: "view",
            name: "addTaskForm",
          }
        ]
      }
    }
    

    To reduce data load and avoid unchanged view rebuild, we can use the projection property to only get the _id field of the task.

    And we’re done ! Restart the Lenra CLI (lenra dev). The task list should now be visible !

    Add some features

    We now have a simple list. But a todo list is more than just a list !

    With your new knowledge, you should be able to create the next features.

    Delete a task

    Add a button to delete the task next to it. When the user clicks this button, remove the task from the database.

    You will need :

    Toggle the tasks

    When the user clicks on a task, toggle thedone boolean. Then in the TaskCard, depending of this boolean, cross the description of the task.

    You will need :