Building a Web App

Introduction

In this tutorial we will learn how to create a chat application using WebPlatform and Alusus. We will assume that you are familiar with Alusus basics. If you are new to Alusus language then we recommend that you read the documentation to get to kmow it better and to install it before this tutorial. At first, we will learn how to create a simple UI that contains only a text, then we will improve this UI to contain a text input that recieve a text from the user and display it. After that, we will improve this app to use a backend so that we can store the user input there, which perserve it in case the user reloads the page. This will allow us to save multiple messages instead of just saving the last one. This what a chat app must do. Then we will refactor the code to use components, which organize our code. Finally, we will add some styles to the site to make it look like a simple chat app.

Necessary Libraries

We should import some libraries necessary for this tutorial. We can do this as follows:

import "Apm";
Apm.importFile("Alusus/WebPlatform");
use Srl;
use WebPlatform;

Create a UI Endpoint

To define a UI endpoint we only need to write a regular function and decorate it with the modifier @uiEndpoint. Also, we can give a title to this endpoint by using the modifier @title. So we can write the following to define a simple empty UI endpoint:

@uiEndpoint["/"]
@title["WebPlatform Example - Simple Text"]
func main {

}

Note how we write the route we want to this endpoint by using the modifier @uiEndpoint. Now we should define what we need to display when the user enters this page. We can do that by setting the view of the window as Text. We'll start by displaying a simple text:

Window.instance.setView(
    Text(String("Hello world!"))
);

To start the application we need to run the server, as follows:

Console.print("Starting server on port 8020...\nURL: http://localhost:8020/\n");
runServer({ "listening_ports", "8020"});

Note how we print a message to the user that shows the server address, then we run the server by using the function runServer and pasing the port we want this server to work on. The application now looks like this:

import "Apm";
Apm.importFile("Alusus/WebPlatform");
use Srl;
use WebPlatform;

@uiEndpoint["/"]
@title["WebPlatform Example - Simple Text"]
func main {
    Window.instance.setView(
        Text(String("Hello world!"))
    );
}


Console.print("Starting server on port 8020...\nURL: http://localhost:8020/\n");
runServer({ "listening_ports", "8020"});

Receiving and Displaying User Input

We can receive text input from the user using the TextInput widget, which displays a text entry field. We can use the Button widget to display a button for the user to click when the text entry is done. We will display what the user entered just like we displayed the static sentence "Hello World!" previously. We need to define two references to the text display and entry components so we can access them later.

def textInput: SrdRef[TextInput];
def textShow: SrdRef[Text];

Now we can add the components we want, we will start by adding the TextInput box just like we did earlier with the Text box:

Window.instance.setView(
    Box({}).{
        addChildren({
            TextInput().{
                textInput = this;
            },
        });
    }
);

We assigned the value of this, which refers to the TextInput instance, to the reference defined earlier. This way we can easily reference this widget later. We'll do the same thing with the Text widget that we'll use to display the text.

Box({}).{
    addChildren({
        Text(String("Here you will see the text you entered")).{
            textShow = this;
        }
    });
},

Notice that we defiend these widgets inside a Box widget. Boxes are widgets that can contain other widgets, including other boxes. A single box can contain multiple widgets. They can be used for various reasons like formatting or grouping mulitple widgets. Later in this tutorial we will learn how to apply styles to these widgets. Finally, we need to add a button and we need to listen to click events on that button:

Button(String("Send")).{
    onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) {
        def text: String = textInput.getText();
        textInput.setText(String());
        textShow.setText(text);
    });
}

Note that we can listen to the event of click on the button by linking a closure to the event onClick to be called when the button is clicked. In that closure, we first get the text from the input component using getText method, then we reset it to the default value. Finally, we set the text inside the display component using setText method. Now we just need to run the event loop so that events could reach its destinations. Without doing this the events will stay in the waiting queue for processing. To process the events we can call the following function after we finish creating the UI:

runEventLoop();

So the app becomes as follows:

import "Apm";
Apm.importFile("Alusus/WebPlatform");
use Srl;
use WebPlatform;

@uiEndpoint["/"]
@title["WebPlatform Example - Text Input and Button"]
func main {
    def textInput: SrdRef[TextInput];
    def textShow: SrdRef[Text];

    Window.instance.setView(
        Box({}).{
            addChildren({
                Box({}).{
                    addChildren({
                        Text(String("Here you will see the text you entered")).{
                            textShow = this;
                        }
                    });
                },
                TextInput().{
                    textInput = this;
                },
                Button(String("Send")).{
                    onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) {
                        def text: String = textInput.getText();
                        textInput.setText(String());
                        textShow.setText(text);
                    });
                }
            });
        }
    );

    runEventLoop();
}


Console.print("Starting server on port 8020...\nURL: http://localhost:8020/\n");
runServer({ "listening_ports", "8020"});

Adding Backend Endpoints to Store and Retrieve User Enteries

Until now, all data the user entered stays on their browser, so we couldn't use this app to chat between multiple users. Also, the data disappears when the user reaload the page. So we need to store the data on the server to be available for all users which allows them to communicate with each other. To do that we need to define two endpoints on the server, one for sending data to the server and the other for receiving data from the server. To define a backend endpoint we define a regular function which accepts an argument of type ptr[Http.Connection] that represents a pointer to the connection and allows us to return a response containing the status and data. Also, we should apply the modifier @beEndpoint on it with the method accepted by this endpoint ("POST" for example) and its route. At first, we define an endpoint for sending the message the user entered to the server. We will need a variable to store the message in addition to the backend endpoint function.

def message: String;

@beEndpoint["POST", "/message"]
func postMessages (conn: ptr[Http.Connection]) {

}

Note that we specify the method as "POST" since we will send data to this endpoint. Now we should read the data sent by the user in this function and store it in the variable message. We can read the data sent on the connection conn using Http.read, all we have to do is to define a buffer to store the data and call this method, as follows:

def postData: array[Char, 1024];
def postDataSize: Int = Http.read(conn, postData~ptr, postData~size);
message = String(postData~ptr, postDataSize);

Here we defined a buffer with enough size to hold the data. Of course, this depends on the goal of the app; in our app we build a simple short message chat, so this size is enough. After that, we read the data and store it in the buffer, the good thing is that method Http.read returns the size of the data it read, which helps us to convert this data into a String object. Finally, we return the status of this request. We will return the status of success without any data:

Http.print(conn, "HTTP/1.1 200 Ok\r\n\r\n");

Now we define another endpoint to retrieve the message from the server, as follows:

@beEndpoint["GET", "/message"]
func getMessages (conn: ptr[Http.Connection]) {

}

All we have to do is return the message in the response using the method Http.print. Of course, we should add the appropriate headers, as follows:

@beEndpoint["GET", "/message"]
func getMessages (conn: ptr[Http.Connection]) {
    // add the required headers
    Http.print(conn, "HTTP/1.1 200 Ok\r\n");
    Http.print(conn, "Content-Type: text/plain\r\n");
    Http.print(conn, "Cache-Control: no-cache\r\n");
    Http.print(conn, "Content-Length: %d\r\n\r\n", message.getLength());
    // put the buffer content which contains the messages
    Http.print(conn, message.buf);
}

Now we need to do some modifications on the UI to send and retrieve the data from the server instead of storign it locally in the browser, which preserves the data and allows any user connected to the server to see it. At first, we define a closure for retrieving the data.

def onFetch: closure (json: Json);

The response is in json format which is why we defined the argument to onFetch closure to be of that type. Now we should update the display component, this component should define what will happen when the data is fetched since that component should handle it. At first, we should check the response status, in case of no problems the displayed text will be updated accordingly. Otherwise, it will display the error message.

Text(String("Here you will see the text you entered")).{
    textShow = this;
    onFetch = closure (json: Json) {
        // first check if there is no error in fetching the data
        def status: Int = json("eventData")("status");
        if status >= 200 and status < 300 {
            // extract data
            def data: String = json("eventData")("body");
            // update the text
            if this.getText() != data {
                this.setText(data);
            }
        } else { // the case where an error occurred
            this.setText(String("Connection error. HTTP status: ") + status);
        }
    };
}

The input component will be the same.

TextInput().{
    textInput = this;
}

The button's behavior will be updated so that it sends the text to the server using sendRequest function then retrieves the data from the server and passes it to the closure onFetch. ` Button(String("Send")).{ onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) { def text: String = textInput.getText(); textInput.setText(String()); // send the text to the server // we specify the method, route, and data type, then the data to be sent. sendRequest( "POST", "/message", "Content-Type: application/text", text, 10000, closure (Json) {} ); // retrieve the data from the server and call the closure onFetch sendRequest("GET", "/message", null, null, 500, onFetch); }); } ` So the full code will be as follows:

import "Apm";
Apm.importFile("Alusus/WebPlatform");
use Srl;
use WebPlatform;

//==============================================================================
// Server

def message: String;

@beEndpoint["POST", "/message"]
func postMessages (conn: ptr[Http.Connection]) {
    def postData: array[Char, 1024];

    def postDataSize: Int = Http.read(conn, postData~ptr, postData~size);

    message = String(postData~ptr, postDataSize);
    Http.print(conn, "HTTP/1.1 200 Ok\r\n\r\n");
}

@beEndpoint["GET", "/message"]
func getMessages (conn: ptr[Http.Connection]) {

    def response: String = message;

    Http.print(conn, "HTTP/1.1 200 Ok\r\n");
    Http.print(conn, "Content-Type: text/plain\r\n");
    Http.print(conn, "Cache-Control: no-cache\r\n");
    Http.print(conn, "Content-Length: %d\r\n\r\n", response.getLength());

    Http.print(conn, response.buf);
}


//==============================================================================
// UI Components

@uiEndpoint["/"]
@title["WebPlatform Example - Text Input and Button"]
func main {
    def textInput: SrdRef[TextInput];
    def textShow: SrdRef[Text];

    def onFetch: closure (json: Json);

    Window.instance.setView(
        Box({}).{
            addChildren({
                Box({}).{
                    addChildren({
                        Text(String("Here you will see the text you entered")).{
                            textShow = this;
                            onFetch = closure (json: Json) {
                                def status: Int = json("eventData")("status");
                                if status >= 200 and status < 300 {
                                    def data: String = json("eventData")("body");
                                    if this.getText() != data {
                                        this.setText(data);
                                    }
                                } else {
                                    this.setText(String("Connection error. HTTP status: ") + status);
                                }
                            };
                        }
                    });
                },
                TextInput().{
                    textInput = this;
                },
                Button(String("Send")).{
                    onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) {
                        def text: String = textInput.getText();
                        textInput.setText(String());
                        sendRequest(
                            "POST", "/message", "Content-Type: application/text", text, 10000,
                            closure (Json) {}
                        );
                        sendRequest("GET", "/message", null, null, 500, onFetch);
                    });
                }
            });
        }
    );

    sendRequest("GET", "/message", null, null, 500, onFetch);
    runEventLoop();
}

Console.print("Starting server on port 8020...\nURL: http://localhost:8020/\n");
runServer({ "listening_ports", "8020"});

Storing Multiple Messages Instead of One

Any chat app should allow you to see previous messages. To do that we need to modify the backend endpoints only. First, we will modify what is stored at the server from one message to an array of messages. Also, we will set an upper limit on the number of messages.

def MAX_MESSAGES: 12;
def messages: Array[String];

In the endpoint responsible for receiving messages from thse user, all we need to do is add the message to the array. Of course, in case we surpass the limit on the number of messages we will remove the oldest message before adding the new one. We do that as follows:

@beEndpoint["POST", "/messages"]
func postMessages (conn: ptr[Http.Connection]) {
    def postData: array[Char, 1024];
    def postDataSize: Int = Http.read(conn, postData~ptr, postData~size);

    if messages.getLength() >= MAX_MESSAGES messages.remove(0);

    messages.add(String(postData~ptr, postDataSize));
    Http.print(conn, "HTTP/1.1 200 Ok\r\n\r\n");
}

Regarding the other endpoint for getting the message, now it will return the whole array of messages. To do this, all we need to do is merging the messages together with the \n separator between each two consecutive messages. That way, messages are separated by newline characters.

@beEndpoint["GET", "/messages"]
func getMessages (conn: ptr[Http.Connection]) {
    def response: String = String.merge(messages, "\n");

    Http.print(conn, "HTTP/1.1 200 Ok\r\n");
    Http.print(conn, "Content-Type: text/plain\r\n");
    Http.print(conn, "Cache-Control: no-cache\r\n");
    Http.print(conn, "Content-Length: %d\r\n\r\n", response.getLength());
    Http.print(conn, response.buf);
}

Refactoring the Code

In this section we will write our own component which is the input component. This will help grouping the code for this component in one place. Also, if we need to use that component in multiple places we can use it directly instead of repeating the same code. To define a component we need to define a class that inherits from Component class, as follows:

class TextEntry {
    @injection def component: Component;
}

Now we will define the variables this class will need. We need to define two properties in our component, one is s reference to the TextInput widget within this component so it can be accessed by other widgets within the component (in this case it'll be the button that needs to reach the text input), and the other property is a closure to be called when a new entry is available. The user can then listen to user entries by setting this closure property.

class TextEntry {
    @injection def component: Component;

    def onNewEntry: closure (String);

    def textInput: SrdRef[TextInput];
}

Then we will define the view that this component will display in the UI. We will do that in the constructor. Which means that we will copy what we wrote previously and rearrange it inside this class. We can write the constructor as follows:

handler this~init() {
    def self: ref[this_type](this);

    this.view = Box({}).{
        addChildren({
            TextInput().{
                self.textInput = this;
            },
            Button(String("Send")).{
                onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) {
                    def text: String = self.textInput.getText();
                    self.textInput.setText(String());
                    if text.trim() != String() {
                        self.onNewEntry(text);
                    }
                });
            }
        });
    };
}

There are several simple differences between this code and what we wrote previously. The first difference is the use of self reference which points to the object that called the constructor. This reference is used to access the members of this object. We can note how it was used to access the variable textInput. We need this definition since the keyword this inside any statements pack (i.e. inside .{}) will point to the object that this operator is applied to, not to the component object which contains the variable textInput. Also, we did a little improvement in which we added a check for empty entered text. In that case we do not send this text. Of course, this is optional and depends on the application specifications. Last thing we should add to this class to be able to use it in UI endpoints is the method this_type() which returns a shared reference of this class. This method changes the way TextEntry() expression works, from a defining a temporary variable in the stack (which is the default behaviour of the compiler) to creating an object of this class in the heap memory and return a shared reference to it, which will be needed when building the UI tree. Defining this method makes writing UI trees easier and clearer to the user.

handler this_type(): SrdRef[TextEntry] {
    return SrdRef[TextEntry].construct();
}

Note that the definition of handler this_type() is merely a syntatic sugar. We can use the expression SrdRef[TextEntry].construct() in our UI trees, but it's cleaner to write TextEntry() instead. Now we should replace the previous input tree with the new component we wrote, which we can do as follows:

TextEntry().{
    onNewEntry = closure (newData: String) {
        sendRequest(
            "POST", "/messages", "Content-Type: application/text", newData, 10000,
            closure (Json) {}
        );
        sendRequest("GET", "/messages", null, null, 500, onFetch);
    };
}

Here we assigned a closure to onNewEntry. In that way we can, from outside that component, define what the component will do when the button is clicked. In our case we will send the data to the server. This way the component remains a general one unrelated to how we use the entered data. Later we can use the component in other places for other purposes. The code for onNewEntry closure is the same as the old one, we don't need to change anything. In addition to refactoring the code, there is another problem. If two users are chatting and one send a message to the other, the other user must refresh the page to see the new messages. This is undesirable behavior. To solve this issue, we can update the data periodically using a timer as follows:

startTimer(500000, closure (json: Json) {
    sendRequest("GET", "/messages", null, null, 500, onFetch);
});

So the code becomes as follows:

import "Apm";
Apm.importFile("Alusus/WebPlatform");
use Srl;
use WebPlatform;

//==============================================================================
// الخادم

def MAX_MESSAGES: 12;
def messages: Array[String];


@beEndpoint["POST", "/messages"]
func postMessages (conn: ptr[Http.Connection]) {
    def postData: array[Char, 1024];

    def postDataSize: Int = Http.read(conn, postData~ptr, postData~size);

    if messages.getLength() >= MAX_MESSAGES messages.remove(0);

    messages.add(String(postData~ptr, postDataSize));
    Http.print(conn, "HTTP/1.1 200 Ok\r\n\r\n");
}

@beEndpoint["GET", "/messages"]
func getMessages (conn: ptr[Http.Connection]) {

    def response: String = String.merge(messages, "
"); Http.print(conn, "HTTP/1.1 200 Ok\r\n"); Http.print(conn, "Content-Type: text/plain\r\n"); Http.print(conn, "Cache-Control: no-cache\r\n"); Http.print(conn, "Content-Length: %d\r\n\r\n", response.getLength()); Http.print(conn, response.buf); } //============================================================================== // UI Components class TextEntry { @injection def component: Component; def onNewEntry: closure (String); def textInput: SrdRef[TextInput]; handler this~init() { def self: ref[this_type](this); this.view = Box({}).{ addChildren({ TextInput().{ self.textInput = this; }, Button(String("Send")).{ onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) { def text: String = self.textInput.getText(); self.textInput.setText(String()); if text.trim() != String() { self.onNewEntry(text); } }); } }); }; } handler this_type(): SrdRef[TextEntry] { return SrdRef[TextEntry].construct(); } } //============================================================================== // UI Endpoints @uiEndpoint["/"] @title["WebPlatform Example - Text Input and Button"] func main { def textShow: SrdRef[Text]; def onFetch: closure (json: Json); Window.instance.setView( Box({}).{ addChildren({ Box({}).{ addChildren({ Text(String("Here you will see the text you entered")).{ textShow = this; onFetch = closure (json: Json) { def status: Int = json("eventData")("status"); if status >= 200 and status < 300 { def data: String = json("eventData")("body"); if this.getText() != data { this.setText(data); } } else { this.setText(String("Connection error. HTTP status: ") + status); } }; } }); }, TextEntry().{ onNewEntry = closure (newData: String) { sendRequest( "POST", "/messages", "Content-Type: application/text", newData, 10000, closure (Json) {} ); sendRequest("GET", "/messages", null, null, 500, onFetch); }; } }) } ); startTimer(500000, closure (json: Json) { sendRequest("GET", "/messages", null, null, 500, onFetch); }); sendRequest("GET", "/messages", null, null, 500, onFetch); runEventLoop(); } Console.print("Starting server on port 8020...\nURL: http://localhost:8020/\n"); runServer({ "listening_ports", "8020"});

UI Improvements

In this section we will add some styles to make the app look better. Also, until now the app shows the errors in the chat area which is unreasonable. So we will add a special area to shows errors notifications. At first, let's define some variables that represent the main colors we will use. It is recommended that we do this instead of hard coding the color code each time.

def PRIMARY_COLOR: Color("8e50ef");
def LIGHT_COLOR: Color("c380ff");

Now let's add the width and height for the component we created earlier so that we can control its size. We can do this by adding the following to the class:

handler this.width = SrdRef[Length] {
    this.view.style.width = value;
    return value;
}

handler this.height = SrdRef[Length] {
    this.view.style.height = value;
    return value;
}

Then we apply some styles to this component. Mainly we will use the Flex display to show the widgets of this component beside each other. Also, we will specify the width, the color, and the edges style of this component box in addition to its background color.

style.{
    display = Display.FLEX;
    layout = Layout.ROW;
    justify = Justify.SPACE_BETWEEN;
    borderWidth = Length4.pt(1.5);
    borderStyle = BorderStyle.SOLID;
    borderColor = PRIMARY_COLOR;
    background = Background(PRIMARY_COLOR);
};

We will also apply some styles to the child widgets inside this component. For the input component we will specify its dimensions, background color, and font size as follows:

TextInput().{
    self.textInput = this;
    style.{
        width = Length.percent(100);
        height = Length.percent(100);
        background = Background(Color("fff"));
        fontSize = Length.pt(12.0);
    };
}

Wheras for the button we will specify the same previous properties in addition to how to justify its content.

style.{
    height = Length.percent(100);
    width = Length.pt(50);
    background = Background(Color(200, 200, 200));
    fontSize = Length.pt(16.0);
    justify = Justify.CENTER;
};

After these modifications this component will be as follows:

class TextEntry {
    @injection def component: Component;

    def onNewEntry: closure (String);

    def textInput: SrdRef[TextInput];

    handler this.width = SrdRef[Length] {
        this.view.style.width = value;
        return value;
    }

    handler this.height = SrdRef[Length] {
        this.view.style.height = value;
        return value;
    }

    handler this~init() {
        def self: ref[this_type](this);

        this.view = Box({}).{
            style.{
                display = Display.FLEX;
                layout = Layout.ROW;
                justify = Justify.SPACE_BETWEEN;
                borderWidth = Length4.pt(1.5);
                borderStyle = BorderStyle.SOLID;
                borderColor = PRIMARY_COLOR;
                background = Background(PRIMARY_COLOR);
            };
            addChildren({
                TextInput().{
                    self.textInput = this;
                    style.{
                        width = Length.percent(100);
                        height = Length.percent(100);
                        background = Background(Color("fff"));
                        fontSize = Length.pt(12.0);
                    };
                },
                Button(String("Send")).{
                    style.{
                        height = Length.percent(100);
                        width = Length.pt(50);
                        background = Background(Color(200, 200, 200));
                        fontSize = Length.pt(16.0);
                        justify = Justify.CENTER;
                    };
                    onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) {
                        def text: String = self.textInput.getText();
                        self.textInput.setText(String());
                        if text.trim() != String() {
                            self.onNewEntry(text);
                        }
                    });
                }
            });
        };
    }
    handler this_type(): SrdRef[TextEntry] {
        return SrdRef[TextEntry].construct();
    }
}

When creating this component we can pass the appropriate width and height, as follows:

TextEntry().{
    width = Length.percent(100) - Length.pt(3);
    height = Length.pt(50);
    onNewEntry = closure (newData: String) {
        sendRequest(
            "POST", "/messages", "Content-Type: application/text", newData, 10000,
            closure (Json) {}
        );
        sendRequest("GET", "/messages", null, null, 500, onFetch);
    };
}

Now let's add a component for displaying the error notifications instead of displaying it within the chat area. At first, we will define a reference for this widget so that we can access it from other components.

def notificationLabel: SrdRef[Text];

We can put this definition anywhere inside the function of the UI endpoint. Now let's define the component responsible for that and put it directly before the component that displays the chat messages. We will apply some styles to it, like making its height relativly small, change its font color to red, and decrease its font size. Of course, these styles can be different based on personal preferences; you can refer to WebPlatform documentation for more details about styling.

Text(String()).{
    notificationLabel = this;
    style.{
        width = Length.percent(100);
        height = Length.pt(20);
        fontColor = Color(200, 50, 50);
        fontSize = Length.pt(10.0);
    };
}

When defining the closure onFetch isnide the messages disaply component, we checked if there is an error or not and displayed it directly in the chat area. We will change that to instead display the error message in the notifications widget.

if status >= 200 and status < 300 {
    def data: String = json("eventData")("body");

    if this.getText() != data {
        this.setText(data);
    }

    if notificationLabel.getText() != "" {
        notificationLabel.setText(String(""));
    }
} else {
    notificationLabel.setText(String("Connection error. HTTP status: ") + status);
}

What remains is applying styles on the page, we can do that by applying styles on the Box components which contains the other components. Doing this is the same as applying styles to any widget.

The following is the final version of the app:

import "Apm";
Apm.importFile("Alusus/WebPlatform");
use Srl;
use WebPlatform;

//==============================================================================
// Server

def MAX_MESSAGES: 12;
def messages: Array[String];

@beEndpoint["POST", "/messages"]
func postMessages (conn: ptr[Http.Connection]) {
    def postData: array[Char, 1024];

    def postDataSize: Int = Http.read(conn, postData~ptr, postData~size);

    if messages.getLength() >= MAX_MESSAGES messages.remove(0);

    messages.add(String(postData~ptr, postDataSize));
    Http.print(conn, "HTTP/1.1 200 Ok\r\n\r\n");
}

@beEndpoint["GET", "/messages"]
func getMessages (conn: ptr[Http.Connection]) {

    def response: String = String.merge(messages, "
"); Http.print(conn, "HTTP/1.1 200 Ok\r\n"); Http.print(conn, "Content-Type: text/plain\r\n"); Http.print(conn, "Cache-Control: no-cache\r\n"); Http.print(conn, "Content-Length: %d\r\n\r\n", response.getLength()); Http.print(conn, response.buf); } //============================================================================== // UI Components def PRIMARY_COLOR: Color("8e50ef"); def LIGHT_COLOR: Color("c380ff"); class TextEntry { @injection def component: Component; def onNewEntry: closure (String); def textInput: SrdRef[TextInput]; handler this.width = SrdRef[Length] { this.view.style.width = value; return value; } handler this.height = SrdRef[Length] { this.view.style.height = value; return value; } handler this~init() { def self: ref[this_type](this); this.view = Box({}).{ style.{ display = Display.FLEX; layout = Layout.ROW; justify = Justify.SPACE_BETWEEN; borderWidth = Length4.pt(1.5); borderStyle = BorderStyle.SOLID; borderColor = PRIMARY_COLOR; background = Background(PRIMARY_COLOR); }; addChildren({ TextInput().{ self.textInput = this; style.{ width = Length.percent(100); height = Length.percent(100); background = Background(Color("fff")); fontSize = Length.pt(12.0); }; }, Button(String("Send")).{ style.{ height = Length.percent(100); width = Length.pt(50); background = Background(Color(200, 200, 200)); fontSize = Length.pt(16.0); justify = Justify.CENTER; }; onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) { def text: String = self.textInput.getText(); self.textInput.setText(String()); if text.trim() != String() { self.onNewEntry(text); } }); } }); }; } handler this_type(): SrdRef[TextEntry] { return SrdRef[TextEntry].construct(); } } //============================================================================== // UI Endpoints @uiEndpoint["/"] @title["WebPlatform Example - Text Input and Button"] func main { def textInput: SrdRef[TextInput]; def textShow: SrdRef[Text]; def onFetch: closure (json: Json); def onNewEntry: closure (String); Window.instance.style.{ padding = Length4.pt(0); margin = Length4.pt(0); }; Window.instance.setView( Box({}).{ style.{ height = Length.percent(100); justify = Justify.SPACE_BETWEEN; display = Display.FLEX; layout = Layout.COLUMN; }; addChildren({ Box({}).{ style.{ width = Length.percent(100) - Length.pt(10); padding = Length4.pt(5); display = Display.FLEX; layout = Layout.COLUMN; flex = Flex(1); }; def notificationLabel: SrdRef[Text]; addChildren({ Text(String()).{ notificationLabel = this; style.{ width = Length.percent(100); height = Length.pt(20); fontColor = Color(200, 50, 50); fontSize = Length.pt(10.0); }; }, Text(String()).{ style.{ width = Length.percent(100); height = Length.percent(100); fontColor = Color(50, 50, 50); fontSize = Length.pt(20.0); }; onFetch = closure (json: Json) { def status: Int = json("eventData")("status"); if status >= 200 and status < 300 { def data: String = json("eventData")("body"); if this.getText() != data { this.setText(data); } if notificationLabel.getText() != "" { notificationLabel.setText(String("")); } } else { notificationLabel.setText(String("Connection error. HTTP status: ") + status); } }; } }); }, TextEntry().{ width = Length.percent(100) - Length.pt(3); height = Length.pt(50); onNewEntry = closure (newData: String) { sendRequest( "POST", "/messages", "Content-Type: application/text", newData, 10000, closure (Json) {} ); sendRequest("GET", "/messages", null, null, 500, onFetch); }; } }); } ); startTimer(500000, closure (json: Json) { sendRequest("GET", "/messages", null, null, 500, onFetch); }); sendRequest("GET", "/messages", null, null, 500, onFetch); runEventLoop(); } Console.print("Starting server on port 8020...\nURL: http://localhost:8020/\n"); runServer({ "listening_ports", "8020"});

Thus we now have a chat application that allows unlimited number of users to communicate. Of course, this app is just a simple example which misses some important things like storing the data in a database, authenticating users, creating multiple chat rooms, and more things required in any production grade chat application. We will visit those areas in other tutorials.