Skip to main content

Tutorial: A Chat Application

We are going to build a simple chat application during this tutorial. The chat application will use basic JavaScript in the browser with Lit. This concrete example will use lit-html which is able to render HTML without any specific convention.

How the chat is going to work is relatively simple. We will have a window with two text boxes and two chat blocks. Every time we write in a chat box, the message will be sent to the other chat window:

What we are going to do

Before we start

This tutorial assumes that you already know the basic theory of tarant and the actor model. You can read the basics in Thinking in Tarant.

What are you building

In this tutorial you will be building an interactive chat that runs on the same browser. There won't be any network connectivity outside (to simplify). It will look like:

Feel free to interact with the application and understand how it behaves as this tutorial will guide you on how to build it.

The Architecture

In tarant, it is common to map components to actors. Each component, at the end, has it's own lifecycle, state and behaviour, and you want it to be isolated and transactional. As we are going to have two big components (both chat windows), we can create one single Actor and create two instances of the actor.

How components interact

Setup for the tutorial

During this tutorial, we will be using CodeSandbox, so you don't need to prepare a local environment to start playing with tarant. To open the tutorial, click on the following button:

The Development Environment

When you clicked on the Fork button, it opened a new tab with a CodeSandbox environment. It has everything you need to start coding. You'll see something similar to this:

How components interact

  1. This is the folder structure of the project. All files will be stored there. For example, index.html is the root page of the project. index.js is where your JavaScript code will be living. You will be working in the JavaScript file.
  2. This is the current open file. You will be able to modify the code here.
  3. This is the preview. Every time you change the JavaScript file, your changes will be shown here,

Now, open the index.js file by clicking on it. If it's not visible in the panel, click on the src folder to open it, and you'll be able to see the index.js file. Essentially, this is your application! Let's go line by line on the code:

import { html, render } from "lit-html"; // (1)
import { Actor, ActorSystem } from "tarant"; // (2)
import "./styles.css"; // (3)

const system = ActorSystem.default(); // (4)
  1. This is what we call an import statement. It allows you to import a dependency. Here, we are importing the lit-html library, that will allows us to render our components.
  2. This is another import statement, but this time we are importing tarant's Actor and ActorSystem classes.
  3. This is another import statement, that includes CSS styles into the final bundle.
  4. This line creates an ActorSystem with the default configuration.

An ActorSystem is where all our actors are going to live. Actors are tied to the actor system, and allows us to interact easily with them. Right now, our actor system does not contain any actor, but we will fix that later.

The ChatWindow Actor

Now we are going to create our first actor. If you remember the diagram we did before, the actor is going to represent the chat window, that contains both the chat box (where all our messages are going to be rendered), the input box and the button that we will use to send messages to the next chat window.

This is the diagram

However, we are going to start with baby steps. We will create an actor called ChatWindow that will hold an array of messages. Actors look like ordinary classes, but they extend from Actor.

class ChatWindow extends Actor {

}

To initialise an actor, we use the constructor, as we would do with any ordinary JavaScript class. However, as we are inheriting actor, we need to call the super constructor.

class ChatWindow extends Actor {
constructor() {
super()
}
}

After the super call, we can initialise the actor state. Let's start with an empty array of messages.

class ChatWindow extends Actor {
constructor() {
super()

this.messages = []
}
}

In tarant, all properties are private to the actor. Even actors of the same type can not see the properties of other actors. This is called instance-private properties. Only the actor that owns it's properties can see them. This is required to ensure that consumers of the data do not read partial information.

Now, let's create an instance of our actor.

To create an instance of an actor we use the ActorSystem.actorOf(className, constructorParameters) factory method. It will return a new actor of the specified type. At the bottom of the file, add the following line:

const firstChat = system.actorOf(ChatWindow, []);

This will create a new ChatWindow actor and store it in a variable called firstChat. Now we should be able to interact with the actor!

However, there is no behaviour in the actor yet. To add new behaviour we implement methods, as ordinary JavaScript methods. Let's create a new method called receive that will receive a new message from a sender:

class ChatWindow extends Actor {
constructor() {
super()

this.messages = []
}

receive({ sender, content }) {
this.messages.push({ sender, content });
}
}

Now we can interact with our actor by sending it a message.

const firstChat = system.actorOf(ChatWindow, []);
firstChat.receive({ sender: 'me :D', content: 'Some random message' })

Now, nothing seems to happen, but the actor got a new message and stored it into an array. Let's do something so we can see the result!

First iteration: rendering the actor

Your index.js file should look like this now:

import { html, render } from "lit-html";
import { Actor, ActorSystem } from "tarant";
import "./styles.css";

class ChatWindow extends Actor {
constructor() {
super()

this.messages = []
}

receive({ sender, content }) {
this.messages.push({ sender, content });
}
}

const system = ActorSystem.default();
const firstChat = system.actorOf(ChatWindow, []);
firstChat.receive({ sender: 'me :D', content: 'Some random message' })

The issue is that, right now, we can't see the messages of our actor! Let's do a first, ugly step, to render them. We will beautify them a bit later.

If you open the index.html file you will see with have two divs, with two ids:

<!DOCTYPE html>
<html>
<head>
<title>Tarant Chat</title>
<meta charset="UTF-8" />
</head>
<body>
<div id="window-1"></div> <!-- We will render our actor here! -->
<div id="window-2"></div>
</body>
<script src="src/index.js"></script>
</html>

We will render our actor inside the window-1 div. To do so, we need to tell our actor where to render. The simplest way is by passing a parameter to the actor constructor.

class ChatWindow extends Actor {
constructor(chatWindow) { // <-- chatWindow here is the first element of the array
super()

this.root = document.getElementById(chatWindow);
this.messages = []
}

receive({ sender, content }) {
this.messages.push({ sender, content });
}
}

const firstChat = system.actorOf(ChatWindow, [ 'window-1' ]); // 'window-1' is the chatWindow parameter in the constructor

With this small change, now we have a reference to the DOM element where the actor is going to render. However, this is not enough, now we want to render it. This means, that we need to tell the actor how its state is going to become HTML. To do so, we will create a new method render, that will get the actor state and render it into the root DOM element.

class ChatWindow extends Actor {
constructor(chatWindow) { // <-- chatWindow here is the first element of the array
super()

this.root = document.getElementById(chatWindow);
this.messages = []
}

receive({ sender, content }) {
this.messages.push({ sender, content });
}

render() {
render(html`<pre>${JSON.stringify(this.messages, null, 2)}</pre>`, this.root)
}
}

Now we are going to render some ugly JSON with the state of the actor. However, with these changes, nothing is yet rendered in the browser. This is because tarant is not a frontend framework: even if we have some integrations like with Vue that can take care of the complexities of rendering, now we are not using any of them.

To start rendering, we will need to tell the actor to render every time we receive a message:

class ChatWindow extends Actor {
constructor(chatWindow) {
super()

this.root = document.getElementById(chatWindow);
this.messages = []
}

receive({ sender, content }) {
this.messages.push({ sender, content });
this.render()
}

render() {
render(html`<pre>${JSON.stringify(this.messages, null, 2)}</pre>`, this.root)
}
}

Your application now will look like:

Your application now

But we have two chats, so we need to render two actors. To render an additional actor, we will use the same strategy as we've done with the first actor.

const secondChat = system.actorOf(ChatWindow, [ 'window-2' ]);
secondChat.receive({ sender: 'another me :D', content: 'Some random message to chat 2' })

Now you'll see both messages, side by side:

Now two chats

Adding interaction: sending messages

However, few applications are useful if you can not interact with them. Now we are going to implement the functionality of sending messages between chats. Let's recall how the application is going to look like:

How components interact

Each ChatWindow will have a text input and a button, and once we click the button, the message is sent to the other ChatWindow. That will also show exactly how actors interact between them.

To start with something, let's create the new input and button. Let's change the render method to add the new components:

render() {
render(
html`
<div class="chat-window">
<h2>Chat Window from ${this.name}</h2>
<div class="message-list">
${JSON.stringify(this.messages, null, 2)}
</div>
<div class="input-box">
<input type="text" name="text" />
<button>Send</button>
</div>
</div>
`,
this.root
);
}

In the previous code we are beautifying a bit the HTML and wrapper the messages in a div called message-list. We are also creating the input-box that contains both the input type="text" and the button we would like to click to send a message.

The application now will look like this:

Two chat windows, less uglier

Now we need to add interaction to our components. But, how do we connect an onclick of a button with an actor message? With lit-html is straightforward, as it allows us to declaratively define our event handlers on any element:

<button @click=${() => this.send()}>Send</button>

Now let's create the send method. I won't do anything, yet, but will act as a placeholder for the business logic.

send() {

}

Your complete actor will look like:

class ChatWindow extends Actor {
constructor(chatWindow) {
super();

this.root = document.getElementById(chatWindow);
this.messages = [];
}

receive({ sender, content }) {
this.messages.push({ sender, content });
this.render();
}

send() {

}

render() {
render(
html`
<div class="chat-window">
<h2>Chat Window from ${this.name}</h2>
<div class="message-list">
${JSON.stringify(this.messages, null, 2)}
</div>
<div class="input-box">
<input type="text" name="text" />
<button @click=${() => this.send()}>Send</button>
</div>
</div>
`,
this.root
);
}
}

Now we need to step back for a second and think on what the actor needs to do to fulfill it's request. What we want to achieve is that the other chat window receives the message so it can be rendered. Also, we want to render our own message, like a chat application. So, what are the steps?

  1. Get the value from my own input.
  2. Get a reference to the other chat window.
  3. Send the message to the chat window.
  4. Render my own message.

Let's go step by step.

1. Get the value from my own input.

lit-html does not use a VDOM, so we can access the rendered input by just using querySelector:

const element = this.root.querySelector("input");
const message = element.value

The code, before the next step, will look like:

import { html, render } from "lit-html";
import { Actor, ActorSystem } from "tarant";
import "./styles.css";

class ChatWindow extends Actor {
constructor(chatWindow) {
super();

this.root = document.getElementById(chatWindow);
this.messages = [];
}

receive({ sender, content }) {
this.messages.push({ sender, content });
this.render();
}

send() {
const element = this.root.querySelector("input");
const message = element.value
}

render() {
render(
html`
<div class="chat-window">
<h2>Chat Window from ${this.name}</h2>
<div class="message-list">
${JSON.stringify(this.messages, null, 2)}
</div>
<div class="input-box">
<input type="text" name="text" />
<button @click=${() => this.send()}>Send</button>
</div>
</div>
`,
this.root
);
}
}

const system = ActorSystem.default();
const firstChat = system.actorOf(ChatWindow, ["window-1"]);
firstChat.receive({ sender: "me :D", content: "Some random message" });
const secondChat = system.actorOf(ChatWindow, ["window-2"]);
secondChat.receive({
sender: "another me :D",
content: "Some random message to chat 2"
});

2. Get a reference to the other chat window.

To get a reference to the other chat window, we will need first the name of the chat window. The easiest way is just by passing it as a parameter in the constructor of the actor. We will be also storing our own name so we can use send it to the receiver of our messages.

constructor(chatWindow, receiverName) {
super(chatWindow); // this specifies the ID of the actor, we will need it later

this.root = document.getElementById(chatWindow);
this.name = chatWindow;
this.receiver = receiverName;
this.messages = [];
}

Now we need to change the send method, so it gets a reference to the other chat window, using the actor system. All actors contain a reference to the actor system they belong, so finding another actor is relatively easy:

  async send() { // because we are going to interact with the external world, let's mark the method as async
const element = this.root.querySelector("input");
const message = element.value;

const otherChatWindow = await this.system.actorFor(this.receiver);
}

actorFor is a method, inside the ActorSystem, that resolves an actor by it's unique identifier. Every actor can specify it's unique identifier during it's construction time by calling the parent constructor with the id they want. We did earlier, and it looks like:

constructor(chatWindow, receiverName) {
super(chatWindow); // this specifies the ID of the actor
// ...
}

actorFor does not return an actor, but a Proxy to an actor. The state of the actor is isolated inside the proxy, so it's not accessible, ensuring that peers of the actor can not access internal state and break the consistency. However, the Proxy looks like the actor, so it shares the same interface as the underlying actor.

Your code should look like this now:

import { html, render } from "lit-html";
import { Actor, ActorSystem } from "tarant";
import "./styles.css";

class ChatWindow extends Actor {
constructor(chatWindow, receiverName) {
super(chatWindow); // this specifies the ID of the actor, we will need it later

this.root = document.getElementById(chatWindow);
this.name = chatWindow;
this.receiver = receiverName;
this.messages = [];
}

receive({ sender, content }) {
this.messages.push({ sender, content });
this.render();
}

async send() { // because we are going to interact with the external world, let's mark the method as async
const element = this.root.querySelector("input");
const message = element.value;

const otherChatWindow = await this.system.actorFor(this.receiver);
}

render() {
render(
html`
<div class="chat-window">
<h2>Chat Window from ${this.name}</h2>
<div class="message-list">
${JSON.stringify(this.messages, null, 2)}
</div>
<div class="input-box">
<input type="text" name="text" />
<button @click=${() => this.send()}>Send</button>
</div>
</div>
`,
this.root
);
}
}

const system = ActorSystem.default();
const firstChat = system.actorOf(ChatWindow, ["window-1", "window-2"]); // <-- this is the answer to the quiz. We add the name of the other chat window!
firstChat.receive({ sender: "me :D", content: "Some random message" });
const secondChat = system.actorOf(ChatWindow, ["window-2", "window-1"]); // <-- this is the answer to the quiz. We add the name of the other chat window!
secondChat.receive({
sender: "another me :D",
content: "Some random message to chat 2"
});

3. Send the message to the chat window.

Now that we have a reference to the other chat window, adding a new message is straightforward. We already did a few times! One thing to consider is that actors are interaction-based (or message-based). When an actor interacts with another, it usually uses a mirrored language. For example:

Exposed Language

In this case, an actor that wants to communicate uses the exposed language of the receiver actor. In Domain Driven Design, the exposed language is called published language and it's an important pattern to ensure encapsulation.

So we will change the send method to reflect the change:

otherChatWindow.receive({ sender: this.name, content: message });

Easy, right? Now the whole code will look like:

import { html, render } from "lit-html";
import { Actor, ActorSystem } from "tarant";
import "./styles.css";

class ChatWindow extends Actor {
constructor(chatWindow, receiverName) {
super(chatWindow); // this specifies the ID of the actor, we will need it later

this.root = document.getElementById(chatWindow);
this.name = chatWindow;
this.receiver = receiverName;
this.messages = [];
}

receive({ sender, content }) {
this.messages.push({ sender, content });
this.render();
}

async send() { // because we are going to interact with the external world, let's mark the method as async
const element = this.root.querySelector("input");
const message = element.value;

const otherChatWindow = await this.system.actorFor(this.receiver);
otherChatWindow.receive({ sender: this.name, content: message });
}


render() {
render(
html`
<div class="chat-window">
<h2>Chat Window from ${this.name}</h2>
<div class="message-list">
${JSON.stringify(this.messages, null, 2)}
</div>
<div class="input-box">
<input type="text" name="text" />
<button @click=${() => this.send()}>Send</button>
</div>
</div>
`,
this.root
);
}
}

const system = ActorSystem.default();
const firstChat = system.actorOf(ChatWindow, ["window-1", "window-2"]);
firstChat.receive({ sender: "me :D", content: "Some random message" });
const secondChat = system.actorOf(ChatWindow, ["window-2", "window-1"]);
secondChat.receive({
sender: "another me :D",
content: "Some random message to chat 2"
});

For the next step in the tutorial, we can remove the initial messages, and keep the chat window empty unless we interact with it.

import { html, render } from "lit-html";
import { Actor, ActorSystem } from "tarant";
import "./styles.css";

class ChatWindow extends Actor {
constructor(chatWindow, receiverName) {
super(chatWindow); // this specifies the ID of the actor, we will need it later

this.root = document.getElementById(chatWindow);
this.name = chatWindow;
this.receiver = receiverName;
this.messages = [];
}

receive({ sender, content }) {
this.messages.push({ sender, content });
this.render();
}

async send() { // because we are going to interact with the external world, let's mark the method as async
const element = this.root.querySelector("input");
const message = element.value;

const otherChatWindow = await this.system.actorFor(this.receiver);
otherChatWindow.receive({ sender: this.name, content: message });
}


render() {
render(
html`
<div class="chat-window">
<h2>Chat Window from ${this.name}</h2>
<div class="message-list">
${JSON.stringify(this.messages, null, 2)}
</div>
<div class="input-box">
<input type="text" name="text" />
<button @click=${() => this.send()}>Send</button>
</div>
</div>
`,
this.root
);
}
}

const system = ActorSystem.default();
const firstChat = system.actorOf(ChatWindow, ["window-1", "window-2"]);
const secondChat = system.actorOf(ChatWindow, ["window-2", "window-1"]);

firstChat.render()
secondChat.render()

4. Render my own message.

Now the application is able to send new messages to the other chat window, and then get rendered into the screen. This is how the application looks like:

How the App Looks Like

However, in every chat application, you can see also your own messages. Let's implement that feature now that we have the send method ready.

In our current implementation, every time that an actor receives a message it renders the messages array it contains. There are two possible solutions then:

  • We can call our own receive method with the message.
  • We add our already generated message into the messages array and render.

They have different implications.

Calling our own receive.

That would be the first option as it adds the message to the array and renders. In tarant, actors are reentrant which means that they can send themselves messages which are also transactional. However, these messages run in another transaction, so we might send messages to other actors but don't refresh our own chat window.

Adding the message to messages and render.

As it's part of the same method, and we are not sending additional messages (only for render, which doesn't mutate the state) this ensures that once the message is received by the second Chat Window, the message is instantly added to our message list.

For the purpose of the tutorial, we will be adding the message directly, as it seems to be the most correct implementation.

The implementation of send now will look like:

  async send() {
const element = this.root.querySelector("input");
const message = element.value;
const otherChatWindow = await this.system.actorFor(this.receiver);

this.messages.push({ content: message });
otherChatWindow.receive({ sender: this.name, content: message });

this.render();
}

Now the application will show all messages, both that you sent and the ones that you received. Now we can tidy up a little bit the HTML to render the messages a little more friendlier.

Change the render method to this:

render() {
render(
html`
<div class="chat-window">
<h2>Chat Window from ${this.name}</h2>
<div class="message-list">
${this.messages.map(
(message) =>
html`
<div class="${message.sender ? "received" : "sent"}">
<p>${message.sender || "Me"}: ${message.content}</p>
</div>
`
)}
</div>
<div class="input-box">
<input type="text" name="text" />
<button @click="${() => this.send()}">Send</button>
</div>
</div>
`,
this.root,
{ host: this }
);

Congratulations! You've just implemented your first application in tarant! The whole application code will now look like the one we saw during the beginning of the tutorial:

What's Next

This tutorial walked you through how to create a simple frontend application with tarant and lit-html. However, you can use tarant also in the backend, so you might want to see how to create a simple web server or to check a list of modules available for tarant.