Fork me on GitHub
#
Setup

Setup your Awly project with npx

The easiest way to create your Awly project is to create an empty folder and inside run the command:

npx awly init

This will install the awly-cli locally with npm, download the default bootstrap awly project and start the localhost server.

Note that you will then have to run every awly cli command prefixed with npx.


Code editor

The recomended code editor is Atom together with the MarkoJS plugin which provides syntax highlighting for MarkoJS templates. To install it, go to Edit -> Preferences -> Install, search for language-marko and click install.

#
Starting page

Let us start with creating a starting page and make it display the title and background. In this task, you will learn how to make a static page with awly and how to apply CSS to it.

You can find the page in /src/pages/home/index.marko. We will be editing this file for the most part.

The templating language in use is MarkoJS. You can read about its syntax in the official docs.

Making your first page:

Your body section in the template should look like this like this at the start:

<include("src/layouts/vendor/awly/blank")>
    <@title>
        Awly.io
    </@title>
    <@body>
        <div>Hello world!</div>
    </@body>
</include>

Let's add markup for the "todos" title. Replace the <@body> contents with the code below.

<@body>
    <div.page-center>
        <div.todo-title>todos</div>
    </div>
</@body>

MarkoJS syntax allows for HTML class and id shorthands like you can see above. The same code written in pure HTML would be:

<div class="page-center">
    <div class="todo-title">todos</div>
</div>

You can read more about this syntax here: MarkoJS Syntax


Let's also change the title while we are at it.

<@title>
    Awly todo app
</@title>

Notice that the title for the page changed and this text does not appear anywhere on the page body. This is because the page uses the template from src/layouts/vendor/awly/blank/index.marko and <@title> and <@body> are just placeholders. The way the layout gets included is with the include tag:

<include("src/layouts/vendor/awly/blank")>
    ...
</include>

Working with styles

Styles can be added to pages, layouts, and components and there are 3 possibilities:

  • plain CSS
  • SASS
  • LESS

style {
    ...
}
 
style.scss {
    ...
}
 
style.less {
    ...
}

You can use either one of the blocks or all of them.

Let's add SCSS block to our page.

style.scss {
    body {
        background-color: #f5f5f5;
        font-family: 'Helvetica Neue'HelveticaArialsans-serif;
    }
 
    .page-center {
        width: 100%;
 
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
    }
 
    .todo-title {
        font-size: 100px;
        font-weight: 100;
        text-align: center;
        color: rgba(175, 47, 47, 0.15);
    }
}

Notice that we included the rules for the body tag in the page template although we have it defined in the layout template. The rules get applied because Awly does not scope the styles to the template. This might change in the future, but for now, it is up to you how to structure your CSS throughout the templates.


Congratulations!

By now you should have the page looking like the one that we started with at the beginning of this chapter. You can get the complete page template for reference here.

#
Start your first component

To make a new component all you have to do, is a new folder in src/components directory with index.marko file inside.

The paths for components folders are configurable. You can change the configuration in the src/marko.json file.

Let's make a new todo folder in src/components/own and put the index.marko template file inside. For now, put just a placeholder markup inside:

<div>This is a todo component</div>

Currently, you have to restart the awly server when you create new components. Otherwise they won't get picked up

Now lets include the component into our page - src/pages/home/index.marko.

<@body>
    <div.page-center>
        <div.todo-title>todos</div>
        <todo />
    </div>
</@body>

The name of the component folder translates to the name of the component in the HTML. As two folders can have the same names in this configuration, make sure that components have unique names.

Now that we got our component displaying on the page lets start developing the actual functionality for the todo component.

Let's replace the current component HTML with the one with a textbox for out todo input.

<div.todo>
    <div.todo__input>
        <input type="text" placeholder="What needs to be done?" />
    </div>
</div>

We won't start styling the input box yet but let first make the JS logic for listening to the ENTER key and getting the value from the input box.

To add JS logic to our component we first need to include the "class" block. Inside that block, we can define our functions. For our case, we for now only need the "addTodoOnEnter" function which gets called on the "on-keypress" event on the input field.

class {
   addTodoOnEnter(event, input){
       if(event.keyCode === 13){ // ENTER key
           console.log("add todo"eventinput.value);
       }
   }
}
 
<div.todo>
    <div.todo__input>
        <input type="text"
               placeholder="What needs to be done?"
               on-keypress("addTodoOnEnter") />
    </div>
</div>

Now you should see the output in the browser console whenever you press the enter key.

"class" is the main section for defining JS code in your components. Awly introduces some new blocks like "server" and "server-static" for more powerful server-side rendering. There is a more in-depth guide for those blocks later in the tutorial.


Like in every component based library there are lifecycle methods. Most important are:

class {
   onCreate(){ ... }
   onInput(input){ ... }
   onMount(){ ... }
}

We will go into detail about these methods in this tutorial.

Right now we get the output to the console, but we dont do anything with it. We want to store the todos as an array of strings. For that, let's use the component state.

The component state is a simple JS object which we can access in the component HTML markup through "state" variable.

Let's define our state in the onCreate lifecycle hook and include the for loop tag in our HTML, and define some default todos. Our component should look like this:

class {
    onCreate(){
        this.state = {
            todos: ['Buy milk''Clean the house''Walk the dog']
        };
    }
 
    addTodoOnEnter(event, input){
        if(event.keyCode === 13){
            console.log("add todo"eventinput.value);
        }
    }
}
 
<div.todo>
    <div.todo__input>
        <input type="text"
               placeholder="What needs to be done?"
               on-keypress("addTodoOnEnter") />
    </div>
 
    <div.todo__list>
        <for(todo in state.todos)>
            <div.todo__item>${todo}</div>
        </for>
    </div>
</div>

Our todos are displaying in the list, but we still cannot add the todo from the input box. To make it work, we now need to push the todo to the "state.todos" array. For this we need a small modification to our "addTodoOnEnter()" function:

addTodoOnEnter(eventinput){
    if(event.keyCode === 13){
        this.state.todos = this.state.todos.concat(input.value);
        input.value = "";
    }
}

Now that the basic functionality of the component is working it is time to add the styling to it. The whole component template file should look something like this in the end:

class {
    onCreate(){
        this.state = {
            todos: ['Buy milk''Clean the house''Walk the dog']
        };
    }
 
    addTodoOnEnter(event, input){
        if(event.keyCode === 13){
            this.state.todos = this.state.todos.concat(input.value);
            input.value = "";
        }
    }
}
 
<div.todo>
    <div.todo__input>
        <input type="text"
               placeholder="What needs to be done?"
               on-keypress("addTodoOnEnter") />
    </div>
 
    <div.todo__list>
        <for(todo in state.todos)>
            <div.todo__item>${todo}</div>
        </for>
    </div>
</div>
 
style.scss {
    $color-gray: #4d4d4d;
    $color-light-gray: #e6e6e6;
 
    .todo {
        width: 100%;
        max-width: 550px;
        margin-bottom: 20px;
 
        background: #ffffff;
        color: $color-gray;
 
        box-shadow: 
            0 25px 46px 0 rgba(97, 97, 97, 0.1),
            0 1px 1px rgba(0, 0, 0, 0.2),
            0 8px 0 -3px #fbfbfb,
            0 9px 1px -3px rgba(0, 0, 0, 0.2),
            0 16px 0 -6px #f6f6f6,
            0 17px 2px -6px rgba(0, 0, 0, 0.2);
 
        &__input {
            margin: 0;
            padding: 16px 16px 16px 60px;
            box-sizing: border-box;
 
            background-color: white;
            box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
 
            font-size: 24px;
            line-height: 1.4em;
 
            input {
                border: none;
                width: 100%;
 
                &:focus {
                    outline: none;
                }
            }
 
            & ::placeholder {
                font-style: italic;
                font-weight: 300;
                color: $color-light-gray;
                opacity: 1;
            }
 
            & :-ms-input-placeholder {
                font-style: italic;
                font-weight: 300;
                color: $color-light-gray;
            }
 
            & ::-ms-input-placeholder {
                font-style: italic;
                font-weight: 300;
                color: $color-light-gray;
            }
        }
 
        &__item {
            padding: 16px 16px 16px 60px;
            background-color: white;
            font-size: 24px;
            border-top: 1px solid $color-light-gray;
        }
    }
}
#
Adding more features

Our component MVP is ready. We will be adding features to make the component look and behave exactly like in the TodoMVC examples.

Lets first start by adding the footer with the count of items on the list. Add the footer element into the component HTML right after the todo__list and the styles for the footer into the styles.scss block inside the ".todo" selector.

...
<div.todo__list>
    <for(todo in state.todos)>
        <div>${todo}</div>
        <div.todo__item>${todo}</div>
    </for>
</div>
<div.todo__footer>
    $ var itemsLeft = state.todos.length;
    ${itemsLeft} ${itemsLeft === 1 ? 'item' : 'items'} left
</div>
...
 
style.scss {
    ...
 
    .todo {
        ...
 
        &__footer {
           padding: 10px 15px;
           background-color: white;
           font-size: 14px;
           border-top: 1px solid $color-light-gray;
        }
    }
}

The checkbox on the left side for each of the added todos will serve the purpose of marking the todo as completed.

Now we have added more logic to our component as todos have now two different states. Change the definition of the todos array in the component state to meet this requirement. Let's make it an array of objects instead. Also, let's add toggleTodoStatus function to our class definition and fix the "addTodoOnEnter" function to add the todo object to the array instead of a string.

class {
    onCreate(){
        this.state = {
            todos: [{
                text: 'Buy milk',
                completed: true
            },{
                text: 'Clean the house',
                completed: false
            },{
                text: 'Walk the dog',
                completed: false
            }]
        };
    }
 
    ...
 
    addTodoOnEnter(event, input){
        if(event.keyCode === 13){
            this.state.todos = this.state.todos.concat(input.value);
            this.state.todos = this.state.todos.concat({
                text: input.value,
                completed: false
            });
            input.value = "";
        }
    }
 
    ...
 
    toggleTodoStatus(todo, event, input){
        todo.completed = input.checked;
        this.setStateDirty('todos');
    }
 
    ...

Now that we have done that out HTML template does not work anymore because we are calling for the todo text directly from the array. Let's change that as well and also add the markup for the checkbox.

<for(todo in state.todos)>
    <div.todo__item
        class="${todo.completed ? 'todo__item--completed' : ''}">
       <label.todo__item-checkbox>
           <input type="checkbox"
                  checked=todo.completed
                  on-change("toggleTodoStatus", todo) />
       </label>
       <span>${todo.text}</span>
    </div>
</for>

And also the styles for the checkboxes.

style.scss {
    .todo {
        ...
 
        &__item {
            ...
            &--completed {
                text-decoration: line-through;
                color: $color-light-gray;
 
                .todo__item-checkbox {
                    background-image: 
                        url('/assets/svg/todo-complete.svg');
                }
            }
        }
 
        &__item-checkbox {
            width: 40px;
            margin-left: -55px;
            height: 40px;
            display: inline-block;
            margin-right: 15px;
            vertical-align: middle;
            cursor: pointer;
            background-image:
                url('/assets/svg/todo-incomplete.svg');
 
            input {
                display: none;
            }
        }
    }
}

As you can see the CSS above uses the SVG assets which you can download here and include them into "assets/svg" folder:

Now that we have our active/completed checkboxes and logic lets also add the filters to the footer so we can switch between the states and filter our todos. We will be adding filters for 3 states: All, Active and Completed.

Lets first add the template markup and deal with the logic later.

...
<div.todo__footer>
    $ var itemsLeft = state.todos.length;
    <div.todo__count>
        ${itemsLeft} ${itemsLeft === 1 ? 'item' : 'items'} left
    </div>
    <div.todo__filters>
        $ var todoState = state.showTodosState;
       <span.todo__filter-btn
            class=(todoState === "all" ? "active-filter" : "")
            on-click("setShowTodosState""all")>All</span>
       <span.todo__filter-btn
            class=(todoState === "active" ? "active-filter" : "")
            on-click("setShowTodosState""active")>Active</span>
       <span.todo__filter-btn
            class=(todoState === "completed" ? "active-filter" : "")
            on-click("setShowTodosState""completed")>Completed</span>
    </div>
</div>
 
style.scss {
    ...
 
    .todo {
        ...
 
        &__count {
            float: left;
        }
 
        &__filters {
            position: absolute;
            left: 0;
            right: 0;
            text-align: center;
        }
 
        &__filter-btn {
            border-radius: 4px;
            margin: 3px;
            padding: 3px 7px;
            font-size: 14px;
            box-sizing: border-box;
 
            &:hover {
               border: 1px solid rgba(175, 47, 47, 0.1);
               margin-left: 2px;
               margin-right: 2px;
            }
 
            &.active-filter {
               border: 1px solid rgba(175, 47, 47, 0.2);
               margin-left: 2px;
               margin-right: 2px;
            }
        }
 
        &__footer {
            position: relative;
            height: 20px;
            padding: 10px 15px;
            background-color: white;
            font-size: 14px;
            color: $color-dark-gray;
            border-top: 1px solid $color-light-gray;
            line-height: 1.5;
        }
    }
}

As you can see we are invoking the "setShowTodosState" whenever a user clicks on one of the filter buttons. Let's make this function right now. This function sets the variable in the component state to keep track of which todo state is selected. We also have to introduce this variable in the state object.

class {
    onCreate() {
        this.state = {
            todos: [{
                text: 'Buy milk',
                completed: true
            },{
                text: 'Clean the house',
                completed: false
            },{
                text: 'Walk the dog',
                completed: false
            }],
            showTodosState: 'all'
        };
    }
    ...
 
    setShowTodosState(todoState, event){
        this.state.showTodosState = todoState;
    }
}

It is quite easy to change the component to display only the todos for the selected state. All we have to do is make the function that will filter the todos array and return only todos with the state that we want to see. Then we wrap our todos array with this function in the HTML template.

class {
    ...
 
    filterTodos(todos, todoState){
        if (todoState === 'active') {
            return todos.filter(todo => todo.completed === false);
        } else if (todoState === 'completed') {
            return todos.filter(todo => todo.completed === true);
        } else {
            return todos;
        }
    }
}
 
...
    <for(todo in
        component.filterTodos(state.todosstate.showTodosState))>
        ...
    </for>
...

Now that we have implemented the filters lets also fix some things. First, the text on the left side of the footer is still showing that there are all todos left even if we completed something. We can quickly fix this with our "filterTodos" function. The other issue is that the HTML template changed a bit and we need to fix the CSS.

<div.todo__footer>
    $ var todosActive = component.filterTodos(state.todos'active');
    $ var todosLeft = todosActive.length;
    <div.todo__count>
        ${todosLeft} ${todosLeft === 1 ? 'item' : 'items'} left
    </div>
    ...
</div>
 
style.scss {
    ...
    .todo {
        ...
 
        &__item {
            &--completed {
                .todo__item-text {
                    text-decoration: line-through;
                    color: $color-light-gray;
                }
            }
        }
    }
}

To add the functionality for removing todos and clearing all the completed todos, we create two functions that manipulate the state.todos array. Since our component is getting quite complex, we have to change the todos array again a little bit. However, this is the last time in this tutorial.

Since we can delete a todo from any of the filtered views, we cannot rely on the position of the object in the state.todos array. To solve this problem, we add the id property to our todo objects and the counter of which is the last todo id.

class {
    onCreate(){
        this.state = {
            todos: [{
                id: 0,
                text: 'Buy milk',
                completed: true
            },{
                id: 1,
                text: 'Clean the house',
                completed: false
            },{
                id: 2,
                text: 'Walk the dog',
                completed: false
            }],
            lastTodoId: 2,
            showTodosState: 'all'
        };
    }
 
    addTodoOnEnter(event, input){
        if(event.keyCode === 13){
            this.state.todos = this.state.todos.concat({
                id: ++this.state.lastTodoId,
                text: input.value,
                completed: false
            });
            input.value = "";
        }
    }
 
    ...
}

Now let's add the delete and clear all functions called "removeTodo" and "clearCompleted" lets add them to our class definition.

class {
    ...
    removeTodo(todoId, event){
        const todo = this.state.todos.find(
            item => item.id === todoId
        );
 
        const todoPos = this.state.todos.indexOf(todo);
        this.state.todos.splice(todoPos, 1);
 
        this.setStateDirty('todos');
    }
 
    clearCompleted(){
        for(let i = this.state.todos.length-1; i >= 0 ; i--){
            if(this.state.todos[i].completed === true){
                this.state.todos.splice(i, 1);
            }
        }
        this.setStateDirty('todos');
    }
}
...
 
    <for(todo in
        component.filterTodos(state.todosstate.showTodosState))>
        <div.todo__item
            class="${todo.completed ? 'todo__item--completed' : ''}">
            ...
            <span.todo__item-remove
                on-click("removeTodo", todo.id)>×</span>
        </div>
    </for>
 
    ...
 
    <div.todo__footer>
       ...
       <div.todo__clear on-click("clearCompleted")>Clear completed</div>
    </div>
 
   ...

We left the "complete all" button and editing of todos on double click for last. Let's first do the complete all button and leave the more complex task for last.

class {
    ...
 
    completeAll(){
       for(let i = 0; i < this.state.todos.length; i++){
           this.state.todos[i].completed = true;
       }
       this.setStateDirty('todos');
    }
}
 
<div.todo>
    <div.todo__input>
        <div.todo__selectAll on-click("completeAll")></div>
        ...
    </div>
</div>
 
style.scss {
    ...
    .todo {
        ...
        &__input {
            ...
            position: relative;
            ...
        }
 
        &__selectAll {
            display: inline-block;
            position: absolute;
            width: 20px;
            top: 9px;
            padding: 10px 20px;
            left: 0px;
            transform: rotate(90deg);
            cursor: pointer;
            color: $color-light-gray;;
        }
 
        ...
    }
 
}

To finish this part of the tutorial we need to do only one more thing, which is to display an edit box if the user double-clicks on the todo text.

Let's add another property to the component state object to keep track of which todo we are editing.

class {
    onCreate(){
        this.state = {
            ...
            showTodosState: 'all',
            editingTodo: null
        };
    }
    ...
}

Editing the todo in place requires quite a lot more logic in the HTML template than before. Replace the todo item block altogether.

...
<div.todo__list>
    <for(todo in
        component.filterTodos(state.todosstate.showTodosState))>
        <div.todo__item
            class="${todo.completed ? 'todo__item--completed' : ''}"
            on-dblclick("showEditTextbox", todo.id)
        >
            <label.todo__item-checkbox
                class="${state.editingTodo === todo.id ? 'hidden' : ''}"
            >
                <input type="checkbox"
                       checked=todo.completed
                       on-change("toggleTodoStatus", todo) />
            </label>
            <if(state.editingTodo === todo.id)>
                <span.clickzone on-click("finishEditing")></span>
                <span.todo__edit-input>
                    <input value=todo.text
                           key='editInput_'+todo.id
                           on-focus("putCursorOnEnd")
                           on-keypress("editTodoOnEnter", todo.id) />
                </span>
            </if>
            <else>
                <span.todo__item-text>${todo.text}</span>
            </else>
 
            <span.todo__item-remove
                class="${state.editingTodo === todo.id ? 'hidden' : ''}"
                on-click("removeTodo", todo.id)>×</span>
        </div>
    </for>
</div>
...

More CSS rules are required as well to display the edit box correctly.

style.scss {
    ...
    .hidden {
        display: none !important;
    }
 
    .todo {
        ...
        &__edit-input {
            position: absolute;
            top: 0;
            height: 100%;
            box-sizing: border-box;
            border: 1px solid $color-gray;
            box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
            padding: 0px 10px;
            margin-left: -10px;
            width: calc(100% - 50px);
            z-index: 1;
 
            input {
                height: 100%;
                border: none;
                box-sizing: border-box;
                width: 100%;
                margin-left: -1px;
                margin-top: -2px;
                background-color: transparent;
                color: $color-gray;
 
                &:focus {
                    outline-width: 0;
                }
            }
        }
 
        &__item {
            height: 40px;
            ...
        }
    }
    ...
    .clickzone {
        // background-color: rgba(255, 0, 0, 0.1);
        position: fixed;
        top: 0;
        left: 0;
        height: 100%;
        width: 100%;
        z-index: 1;
    }
}

For the finish we need to add the functions into our code for:

  • Saving the edited TODO on the enter key
  • Showing the showing the textbox on double-click
  • Putting text cursor on the end of TODO when the user starts editing
  • Disabling the editing when the user clicks outside the textbox

All we need to do is to add those functions to the class definition

onCreate() {
    this.state = {
        todos: [{
            text: 'Buy milk',
            completed: true
        },{
            text: 'Clean the house',
            completed: false
        },{
            text: 'Walk the dog',
            completed: false
        }],
        showTodosState: 'all'
    };
}
class {
  onCreate(){
    var i = 0;
  }
 
    editTodoOnEnter(todoId, event, input){
        if(event.keyCode === 13){
            this.state.editingTodo = null;
            return;
        }
 
        const todo = this.state.todos.find(
            item => item.id === todoId
        );
 
        todo.text = input.value + event.key;
    }
 
 
    showEditTextbox(todoId){
        this.state.editingTodo = todoId;
 
        setTimeout(() => { // wait for state to rerender the view
           this.getEl('editInput_'+todoId).focus();
        });
    }
 
    putCursorOnEnd(event, input){
        let tmp = input.value;
        input.value = "";
        input.value = tmp;
    }
 
    finishEditing(event){
        this.state.editingTodo = null;
    }

Congratulations!

This is it! You completed the first chapter of the tutorial on how to develop a todo app with awly.io and MarkoJS templating language.

Although we covered a lot, we only scraped the surface of awly.io possibilities. We only covered the development of the frontend part. Stay tuned for further todo app tutorial chapters on deploying your app to AWS cloud and making use of backend services like DynamoDB for data storage and authentication. They will be coming shortly.