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:
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.
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:
You can also develop applications with tarant locally. You need to download Node.js. Before continuing, install the latest stable version for your operating system.
Now, you can download the tutorial by clicking the in the following button:
In the opened CodeSandbox tab, navigate to
Top Left Menu > File > Export To ZIP
.Decompress the ZIP File you just downloaded.
Navigate to the new folder using the terminal and run the following command:
npm i
To start the development environment, run the following command:
npm start
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:
- 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. - This is the current open file. You will be able to modify the code here.
- 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)
- This is what we call an
import
statement. It allows you to import a dependency. Here, we are importing thelit-html
library, that will allows us to render our components. - This is another
import
statement, but this time we are importingtarant
's Actor and ActorSystem classes. - This is another
import
statement, that includes CSS styles into the final bundle. - 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.
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:
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:
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:
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:
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?
- Get the value from my own input.
- Get a reference to the other chat window.
- Send the message to the chat window.
- 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.
Quiz!
There is another change we need to do, related to how we construct an actor, in our code. We added a new parameter to the constructor, so how do we pass the new argument to that constructor?
The code in the sample has this change fixed, but you can try to fix it yourself and check the answer when done.
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:
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.
In our example, the exposed language for the actor is the receive
method. So, when we want to send a message from a chat window to another, we
tell the target chat window to receive the new message.
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:
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.