When one mentions server-side JavaScript technology, Node.js is what comes to our mind first. Node.js is an extremely powerful and robust platform. Using this JavaScript platform, we can build server-side applications very easily. In today's tutorial, we will focus on creating a chat application that uses Kotlin by using the Node.js technology. So, basically, we will transpile Kotlin code to JavaScript on the server side.
This article is an excerpt from the book, Kotlin Blueprints, written by Ashish Belagali, Hardik Trivedi, and Akshay Chordiya. With this book, you will get to know the building blocks of Kotlin and best practices when using quality world-class applications.
Kotlin is a modern language and is gaining popularity in the JavaScript community day by day. The Kotlin language with modern features and statically typed; is superior to JavaScript. Similar to JavaScript, developers that know Kotlin can use the same language skills on both sides but, they also have the advantage of using a better language. The Kotlin code gets transpiled to JavaScript and that in turn works with the Node.js. This is the mechanism that lets you use the Kotlin code to work with a server-side technology, such as Node.js.
Our chat app will have following functionalities:
To visualize the app that we will develop, take a look at the following screenshots.
The following screenshot is a page where the user will enter a nickname and gain an entry in our chat app:
In the following screen, you can see a chat window and a list of online users:
We have slightly configured this application in a different way. We have kept the backend code module and frontend code module separate using the following method:
The backend module will contain all the Kotlin code that will be converted into Node.JS code later, and the webapp module will contain all the Kotlin code that will later be converted into the JavaScript code. We have referred to the directory structure from Github.
After performing the previous steps correctly, your project will have three build.gradle files. We have highlighted all three files in the project explorer section, as shown in the following screenshot:
We need to initialize our root directory for the node. Execute npm init and it will create package.json. Now our login page is created. To run it, we need to set up the Node.js server. We want to create the server in such a way that by executing npm start, it should start the server.
To achieve it, our package.json file should look like the following piece of code:
{ "name": "kotlin_node_chat_app", "version": "1.0.0", "description": "", "main": "backend/server/app.js", "scripts": { "start": "node backend/server/app.js" }, "author": "Hardik Trivedi", "license": "ISC", "dependencies": { "ejs": "^2.5.7", "express": "^4.16.2", "kotlin": "^1.1.60", "socket.io": "^2.0.4" } }
We have specified a few dependencies here as well:
Execute npm install on the Terminal/Command Prompt and it should trigger the download of all these dependencies.
Now, it's very important where your output will be generated once you trigger the build. For that, build.gradle will help us. Specify the following lines in your module-level build.gradle file.
The backend module's build.gradle will have the following lines of code:
compileKotlin2Js { kotlinOptions.outputFile = "${projectDir}/server/app.js" kotlinOptions.moduleKind = "commonjs" kotlinOptions.sourceMap = true }
The webapp module's build.gradle will have the following lines of code:
compileKotlin2Js { kotlinOptions.metaInfo = true kotlinOptions.outputFile = "${projectDir}/js/main.js" kotlinOptions.sourceMap = true kotlinOptions.main = "call" }
In both the compileKotlin2Js nodes, kotlinOptions.outputFile plays a key role. This basically tells us that once Kotlin's code gets compiled, it will generate app.js and main.js for Node.js and JavaScript respectively.
In the index.ejs file, you should define a script tag to load main.js. It will look something like the following line of code:
<script type="text/javascript" src="js/main.js"></script>
Along with this, also specify the following two tags:
<script type="text/javascript" src="lib/kotlin/kotlin.js"></script> <script type="text/javascript" src="lib/kotlin/kotlinx-html-js.js"> </script>
The kotlin.js and kotlinx-html-js.js files are nothing but the Kotlin output files. It's not compilation output, but actually transpiled output. The following are output compilations:
Let's assume our final Main.kt file will look like this:
fun main(args: Array<String>) {
val socket: dynamic = js("window.socket")
val chatWindow = ChatWindow {
println("here")
socket.emit("new_message", it)
}
val loginWindow = LoginWindow {
chatWindow.showChatWindow(it)
socket.emit("add_user", it)
}
loginWindow.showLogin()
socket.on("login", { data ->
chatWindow.showNewUserJoined(data)
chatWindow.showOnlineUsers(data)
})
socket.on("user_joined", { data ->
chatWindow.showNewUserJoined(data)
chatWindow.addNewUsers(data)
})
socket.on("user_left", { data ->
chatWindow.showUserLeft(data)
})
socket.on("new_message", { data ->
chatWindow.showNewMessage(data)
})
}
For this, inside main.js, our main function will look like this:
function main(args) { var socket = window.socket; var chatWindow = new ChatWindow(main$lambda(socket)); var loginWindow = new LoginWindow(main$lambda_0(chatWindow, socket)); loginWindow.showLogin(); socket.on('login', main$lambda_1(chatWindow)); socket.on('user_joined', main$lambda_2(chatWindow)); socket.on('user_left', main$lambda_3(chatWindow)); socket.on('new_message', main$lambda_4(chatWindow)); }
The actual main.js file will be bulkier because it will have all the code transpiled, including other functions and LoginWindow and ChatWindow classes. Keep a watchful eye on how the Lambda functions are converted into simple JavaScript functions. Lambda functions, for all socket events, are transpiled into the following piece of code:
function main$lambda_1(closure$chatWindow) { return function (data) { closure$chatWindow.showNewUserJoined_qk3xy8$(data); closure$chatWindow.showOnlineUsers_qk3xy8$(data); }; } function main$lambda_2(closure$chatWindow) { return function (data) { closure$chatWindow.showNewUserJoined_qk3xy8$(data); closure$chatWindow.addNewUsers_qk3xy8$(data); }; } function main$lambda_3(closure$chatWindow) { return function (data) { closure$chatWindow.showUserLeft_qk3xy8$(data); }; } function main$lambda_4(closure$chatWindow) { return function (data) { closure$chatWindow.showNewMessage_qk3xy8$(data); }; }
As can be seen, Kotlin aims to create very concise and readable JavaScript, allowing us to interact with it as needed.
We need to write a behavior in the route.kt file. This will let the server know which page to load when any request hits the server. The router.kt file will look like this:
fun router() {
val express = require("express")
val router = express.Router()
router.get("/", { req, res ->
res.render("index")
})
return router
}
This simply means that whenever a get request with no name approaches the server, it should display an index page to the user. We are told to instruct the framework to refer to the router.kt file by writing the following line of code:
app.use("/", router())
Now let's create a server. We should create an app.kt file under the backend module at the backend/src/kotlin path. Refer to the source code to verify. Write the following piece of code in app.kt:
external fun require(module: String): dynamic external val process: dynamic external val __dirname: dynamic fun main(args: Array<String>) {
println("Server Starting!")
val express = require("express")
val app = express()
val path = require("path")
val http = require("http")
/**
* Get port from environment and store in Express.
*/
val port = normalizePort(process.env.PORT)
app.set("port", port)
// view engine setup
app.set("views", path.join(__dirname, "../../webapp"))
app.set("view engine", "ejs")
app.use(express.static("webapp"))
val server = http.createServer(app)
app.use("/", router())
app.listen(port, {
println("Chat app listening on port http://localhost:$port")
})
}
fun normalizePort(port: Int) = if (port >= 0) port else 7000
These are multiple things to highlight here:
external class Node {
val firstChild: Node
fun append(child: Node): Node
fun removeChild(child: Node): Node
// etc
}
A value of this type can be assigned to any variable or passed anywhere as a parameter.
Any value can be assigned to a variable of dynamic type or passed to a function that takes dynamic as a parameter. Null checks are disabled for such values.
process.env.PORT: This will find an available port on the server, as simple as that. This line is required if you want to deploy your application on a utility like Heroku. Also, notice the normalizePort function. See how concise it is. The if…else condition is written as an expression. No explicit return keyword is required. Kotlin compiler also identifies that if (port >= 0) port else 7000 will always return Int, hence no explicit return type is required. Smart, isn't it!
Now, it's time to kick-start the server. Hit the Gradle by using ./gradlew build. All Kotlin code will get compiled into Node.js code. On Terminal, go to the root directory and execute npm start. You should be able to see the following message on your Terminal/Command Prompt:
Now, let's begin with the login page. Along with that, we will have to enable some other settings in the project as well. If you refer to a screenshot that we mentioned at the beginning of the previous section, you can make out that we will have the title, the input filed, and a button as a part of the login page. We will create the page using Kotlin and the entire HTML tree structure, and by applying CSS to them, the will be part of our Kotlin code. For that, you should refer to the Main.kt and LoginWindow files.
We will use EJS (effective JavaScript templating) to render HTML content on the page. EJS and Node.js go hand in hand. It's simple, flexible, easy to debug, and increases development speed. Initially, index.ejs would look like the following code snippet:
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial- scale=1.0"/> </head> <body> <div id="container" class="mainContainer"> </div> </body> </html>
The <div> tag will contain all different views, for example, the Login View, Chat Window View, and so on.
DSL stands for domain-specific language. As the name indicates, it gives you the feeling as if you are writing code in a language using terminology particular to a given domain without being geeky, but then, this terminology is cleverly embedded as a syntax in a powerful language. If you are from the Groovy community, you must be aware of builders. Groovy builders allow you to define data in a semi-declarative way. It's a kind of mini-language of its own. Builders are considered good for generating XML and laying out UI components. The Kotlin DSL uses Lambdas a lot.
The DSL in Kotlin is a type-safe builder. It means we can detect compilation errors in IntelliJ's beautiful IDE. The type-check builders are much better than the dynamically typed builders of Groovy.
The DSL to build HTML trees is a pluggable dependency. We, therefore, need to set it up and configure it for our project. We are using Gradle as a build tool and Gradle has the best way to manage the dependencies. We will define the following line of code in our build.gradle file to use kotlinx.html:
compile("org.jetbrains.kotlinx:kotlinx-html-js:$html_version")
Gradle will automatically download this dependency from jcenter(). Build your project from menu Build | Build Project. You can also trigger a build from the terminal/command prompt. To build a project from the Terminal, go to the root directory of your project and then execute ./gradlew build.
Now create the index.ejs file under the webapp directory. At this moment, your index.ejs file may look like the following:
Inside your LoginWindow class file, you should write the following piece of code:
class LoginWindow(val callback: (String) -> Unit) {
fun showLogin() {
val formContainer = document.getElementById("container") as
HTMLDivElement
val loginDiv = document.create.div {
id = "loginDiv"
h3(classes = "title") {
+"Welcome to Kotlin Blueprints chat app"
}
input(classes = "nickNameInput") {
id = "nickName"
onInputFunction = onInput()
maxLength = 16.toString()
placeholder = "Enter your nick name"
}
button(classes = "loginButton") {
+"Login"
onClickFunction = onLoginButtonClicked()
}
}
formContainer.appendChild(loginDiv)
}
}
Observe how we have provided the ID, input types, and a default ZIP code value. A default ZIP code value is optional. Let's spend some time understanding the previous code. The div, input, button, and h3 all these are nothing but functions. They are basically Lambda functions.
The following are the functions that use Lambda as the last parameter. You can call them in different ways:
someFunction({})
someFunction("KotlinBluePrints",1,{})
someFunction("KotlinBluePrints",1){}
someFunction{}
Lambda functions are nothing but functions without a name. We used to call them anonymous functions. A function is basically passed into a parameter of a function call, but as an expression. They are very useful. They save us a lot of time by not writing specific functions in an abstract class or interface.
Lambda usage can be as simple the following code snippet, where it seems like we are simply binding a block an invocation of the helloKotlin function:
fun main(args: Array<String>) { val helloKotlin={println("Hello from KotlinBlueprints team!")} helloKotlin() }
At the same time, lambda can be a bit complex as well, just like the following code block:
fun <T> lock(lock: Lock, body: () -> T): T { lock.lock() try { return body() } finally { lock.unlock() } }
In the previous function, we are acquiring a lock before executing a function and releasing it when the function gets executed. This way, you can synchronously call a function in a multithreaded environment. So, if we have a use case where we want to execute sharedObject.someCrucialFunction() in a thread-safe environment, we will call the preceding lock function like this:
lock(lock,{sharedObject.someCrucialFunction()})
Now, the lambda function is the last parameter of a function call, so it can be easily written like this:
lock(lock) { sharedObject.someCrucialFunction() }
Look how expressive and easy to understand the code is. We will dig more into the Lambda in the upcoming section.
In the index.ejs page, we will have an input field with the ID nickName when it is rendered. We can simply read the value by writing the following lines of code:
val nickName = (document.getElementById("nickName") as? HTMLInputElement)?.value
However, to cover more possibilities, we have written it in a slightly different way. We have written it as if we are taking the input as an event.
The following code block will continuously read the value that is entered into the nickName input field:
private fun onInput(): (Event) -> Unit { return { val input = it.currentTarget as HTMLInputElement when (input.id) { "nickName" -> nickName = input.value "emailId" -> email = input.value } } }
Check out, we have used the when function, which is a replacement for the switch case. The preceding code will check whether the ID of the element is nickName or emailId, and, based on that, it will assign the value to the respective objects by reading them from the in-out field. In the app, we will only have the nickname as the input file, but using the preceding approach, you can read the value from multiple input fields.
In its simplest form, it looks like this:
when (x) { 1 -> print("x == 1") 2 -> print("x == 2") else -> { // Note the block print("x is neither 1 nor 2") } }
The when function compares its argument against all branches, top to bottom until some branch condition is met. The when function can be used either as an expression or as a statement.
The else branch is evaluated if none of the other branch conditions are satisfied. If when is used as an expression, the else branch is mandatory, unless the compiler can prove that all possible cases are covered with branch conditions.
If many cases should be handled in the same way, the branch conditions may be combined with a comma, as shown in the following code:
when (x) { 0, 1 -> print("x == 0 or x == 1") else -> print("otherwise") }
The following uses arbitrary expressions (not only constants) as branch conditions:
when (x) { parseInt(s) -> print("s encodes x") else -> print("s does not encode x") }
The following is used to check a value for being in or !in a range or a collection:
when (x) { in 1..10 -> print("x is in the range") in validNumbers -> print("x is valid") !in 10..20 -> print("x is outside the range") else -> print("none of the above") }
Once our setup is done, we are able to start the server and see the login page. It's time to pass the nickname or server and enter the chat room.
We have written a function named onLoginButtonClicked(). The body for this function should like this:
private fun onLoginButtonClicked(): (Event) -> Unit { return { if (!nickName.isBlank()) { val formContainer = document.getElementById("loginDiv") as HTMLDivElement formContainer.remove() callback(nickName) } } }
The preceding function does two special things:
Unlike any other programming language, Kotlin also provides class cast support. The document.getElementById() method returns an Element type if instance. We basically want it to cast into HTMLDivElement to perform some <div> related operation. So, using as, we cast the Element into HTMLDivElement.
With the as keyword, it's unsafe casting. It's always better to use as?. On a successful cast, it will give an instance of that class; otherwise, it returns null. So, while using as?, you have to use Kotlin's null safety feature. This gives your app a great safety net onLoginButtonClicked can be refactored by modifying the code a bit. The following code block is the modified version of the function. We have highlighted the modification in bold:
private fun onLoginButtonClicked(): (Event) -> Unit { return { if (!nickName.isBlank()) { val formContainer = document.getElementById("loginDiv") as? HTMLDivElement formContainer?.remove() callback(nickName) } } }
Oftentimes, we need a function to notify us when something gets done. We prefer callbacks in JavaScript. To write a click event for a button, a typical JavaScript code could look like the following:
$("#btn_submit").click(function() { alert("Submit Button Clicked"); });
With Kotlin, it's simple. Kotlin uses the Lambda function to achieve this. For the LoginWindow class, we have passed a callback as a constructor parameter. In the LoginWindow class (val callback: (String) -> Unit), the class header specifies that the constructor will take a callback as a parameter, which will return a string when invoked.
To pass a callback, we will write the following line of code:
callback(nickName)
To consume a callback, we will write code that will look like this:
val loginWindow = LoginWindow { chatWindow.showChatWindow(it) socket.emit("add_user", it) }
So, when callback(nickName) is called, chatWindow.showChatWindow will get called and the nickname will be passed. Without it, you are accessing nothing but the nickname.
We shall be using the Socket.IO library to set up sockets between the server and the clients. Socket.IO takes care of the following complexities:
Read more about Socket.IO at https://socket.io/.
We have already specified the dependency for Socket.IO in our package.json file. Look at this file. It has a dependency block, which is mentioned in the following code block:
"dependencies": { "ejs": "^2.5.7", "express": "^4.16.2", "kotlin": "^1.1.60", "socket.io": "^2.0.4" }
When we perform npm install, it basically downloads the socket-io.js file and keeps node_modules | socket.io inside.
We will add this JavaScript file to our index.ejs file.
There we can find the following mentioned <script> tag inside the <body> tag:
<script type="text/javascript" src="/socket.io/socket.io.js"> </script>
Also, initialize socket in the same index.js file like this:
<script> window.socket = io(); </script>
With the Socket.IO library, you should open a port and listen to the request using the following lines of code. Initially, we were directly using app.listen(), but now, we will pass that function as a listener for sockets:
val io = require("socket.io").listen(app.listen(port, { println("Chat app listening on port http://localhost:$port") }))
The server will listen to the following events and based on those events, it will perform certain tasks:
The Socket.IO library works on a simple principle—emit and listen. Clients emit the messages and a listener listens to those messages and performs an action associated with them.
So now, whenever a user successfully logs in, we will emit an event named add_user and the server will add it to an online user's list.
The following code line emits the message:
socket.emit("add_user", it)
The following code snippet listens to the message and adds a user to the list:
socket.on("add_user", { nickName -> socket.nickname= nickName numOfUsers = numOfUsers.inc() usersList.add(nickName as String) })
The socket.on function will listen to the add_user event and store the nickname in the socket.
There are a lot of things operator overloading can do, and we have used quite a few features here. Check out how we increment a count of online users:
numOfUsers = numOfUsers.inc()
It is a much more readable code compared to numOfUsers = numOfUsers+1, umOfUsers += 1, or numOfUsers++.
Similarly, we can decrement any number by using the dec() function.
Operator overloading applies to the whole set of unary operators, increment-decrement operators, binary operators, and index access operator. Read more about all of them here.
Now we need to show the list of online users. For this, we need to pass the list of all online users and the count of users along with it.
The data class is one of the most popular features among Kotlin developers. It is similar to the concept of the Model class.
The compiler automatically derives the following members from all properties declared in the primary constructor:
A simple version of the data class can look like the following line of code, where name and age will become properties of a class:
data class User(val name: String, val age: Int)
With this single line and, mainly, with the data keyword, you get equals()/hasCode(), toString() and the benefits of getters and setters by using val/var in the form of properties. What a powerful keyword!
In our app, we have chosen the Pair class to demonstrate its usage. The Pair is also a data class. Consider the following line of code:
data class Pair<out A, out B> : Serializable
It represents a generic pair of two values. You can look at it as a key–value utility in the form of a class. We need to create a JSON object of a number of online users with the list of their nicknames. You can create a JSON object with the help of a Pair class. Take a look at the following lines of code:
val userJoinedData = json(Pair("numOfUsers", numOfUsers), Pair("nickname", nickname), Pair("usersList", usersList))
The preceding JSON object will look like the following piece of code in the JSON format:
{ "numOfUsers": 3, "nickname": "Hardik Trivedi", "usersList": [ "Hardik Trivedi", "Akshay Chordiya", "Ashish Belagali" ] }
The user's list that we have passed inside the JSON object will be iterated and rendered on the page. Kotlin has a variety of ways to iterate over the list. Actually, anything that implements iterable can be represented as a sequence of elements that can be iterated. It has a lot of utility functions, some of which are mentioned in the following list:
There are some really useful extension functions, such as the following:
We have used forEachIndexed; this gives the extracted value at the index and the index itself. Check out the way we have iterated the user list:
fun showOnlineUsers(data: Json) { val onlineUsersList = document.getElementById("onlineUsersList") onlineUsersList?.let { val usersList = data["usersList"] as? Array<String> usersList?.forEachIndexed { index, nickName -> it.appendChild(getUserListItem(nickName)) } } }
Now, here comes the interesting part: sending and receiving a chat message. The flow is very simple: The client will emit the new_message event, which will be consumed by the server, and the server will emit it in the form of a broadcast for other clients. When the user clicks on Send Message, the onSendMessageClicked method will be called. It sends the value back to the view using callback and logs the message in the chat window. After successfully sending a message, it clears the input field as well.
Take a look at the following piece of code:
private fun onSendMessageClicked(): (Event) -> Unit { return { if (chatMessage?.isNotBlank() as Boolean) { val formContainer = document.getElementById("chatInputBox") as HTMLInputElement callback(chatMessage!!) logMessageFromMe(nickName = nickName, message = chatMessage!!) formContainer.value = "" } } }
We have defined chatMessage as nullable. Check out the declaration here:
private var chatMessage: String? = null
Kotlin is, by default, null safe. This means that, in Kotlin, objects cannot be null. So, if you want any object that can be null, you need to explicitly state that it can be nullable. With the safe call operator ?., we can write if(obj !=null) in the easiest way ever. The if (chatMessage?.isNotBlank() == true) can only be true if it's not null, and does not contain any whitespace. We do know how to use the Elvis operator while dealing with null. With the help of the Elvis operator, we can provide an alternative value if the object is null. We have used this feature in our code in a number of places. The following are some of the code snippets that highlight the usage of the safe call operator.
Removing the view if not null:
formContainer?.remove()
Iterating over list if not null:
usersList?.forEachIndexed { _, nickName -> it.appendChild(getUserListItem(nickName)) }
Appending a child if the div tag is not null:
onlineUsersList?.appendChild(getUserListItem (data["nickName"].toString()))
Getting a list of all child nodes if the <ul> tag is not null:
onlineUsersList?.childNodes
Checking whether the string is not null and not blank:
chatMessage?.isNotBlank()
Sometimes, you will have to face a situation where you will be sure that the object will not be null at the time of accessing it. However, since you have declared nullable at the beginning, you will have to end up using force unwraps. Force unwraps have the syntax of !!. This means you have to fetch the value of the calling object, irrespective of it being nullable. We are explicitly reading the chatMessage value to pass its value in the callback. The following is the code:
callback(chatMessage!!)
Force unwraps are something we should avoid. We should only use them while dealing with interoperability issues. Otherwise, using them is basically nothing but throwing away Kotlin's beautiful features.
With the help of Lambda and extension functions, Kotlin is providing yet another powerful feature in the form of let functions. The let() function helps you execute a series of steps on the calling object. This is highly useful when you want to perform some code where the calling object is used multiple times and you want to avoid a null check every time.
In the following code block, the forEach loop will only get executed if onlineUsersList is not null. We can refer to the calling object inside the let function using it:
fun showOnlineUsers(data: Json) { val onlineUsersList = document.getElementById("onlineUsersList") onlineUsersList?.let { val usersList = data["usersList"] as? Array<String> usersList?.forEachIndexed { _, nickName -> it.appendChild(getUserListItem(nickName)) } } }
What if we told you that while calling, it's not mandatory to pass the parameter in the same sequence that is defined in the function signature? Believe us. With Kotlin's named parameter feature, it's no longer a constraint. Take a look at the following function that has a nickName parameter and the second parameter is message:
private fun logMessageFromMe(nickName: String, message: String) { val onlineUsersList = document.getElementById("chatMessages") val li = document.create.li { div(classes = "sentMessages") { span(classes = "chatMessage") { +message } span(classes = "filledInitialsMe") { +getInitials(nickName) } } } onlineUsersList?.appendChild(li) }
If you call a function such as logMessageForMe(mrssage,nickName), it will be a blunder. However, with Kotlin, you can call a function without worrying about the sequence of the parameter. The following is the code for this:
fun showNewMessage(data: Json) { logMessage(message = data["message"] as String, nickName = data["nickName"] as String) }
Note how the showNewMessage() function is calling it, passing message as the first parameter and nickName as the second parameter.
Whenever any user leaves the chat room, we will show other online users a message saying x user left. Socket.IO will send a notification to the server when any client disconnects. Upon receiving the disconnect, the event server will remove the user from the list, decrement the count of online users, and broadcast the event to all clients. The code can look something like this:
socket.on("disconnect", { usersList.remove(socket.nicknameas String) numOfUsers = numOfUsers.dec() val userJoinedData = json(Pair("numOfUsers", numOfUsers), Pair("nickName", socket.nickname)) socket.broadcast.emit("user_left", userJoinedData) })
Now, it's the client's responsibility to show the message for that event on the UI. The client will listen to the event and the showUsersLeft function will be called from the ChatWindow class.
The following code is used for receiving the user_left broadcast:
socket.on("user_left", { data -> chatWindow.showUserLeft(data) })
The following displays the message with the nickname of the user who left the chat and the count of the remaining online users:
fun showUserLeft(data: Json) { logListItem("${data["nickName"]} left") logListItem(getParticipantsMessage(data["numOfUsers"] as Int)) }
We saw how to build a chat application using Kotlin, but without showing the data on a beautiful UI, the user will not like the web app. We have used some simple CSS to give a rich look to the index.ejs page. The styling code is kept inside webapp/css/ styles.css.
However, we have done everything so far entirely and exclusively in Kotlin. So, it's better we apply CSS using Kotlin as well. You may have already observed that there are a few mentions of classes. It's nothing but applying the CSS in a Kotlin way. Take a look at how we have applied the classes while making HTML tree elements using a DSL:
fun showLogin() { val formContainer = document.getElementById("container") as HTMLDivElement val loginDiv = document.create.div { id = "loginDiv" h3(classes = "title") { +"Welcome to Kotlin Blueprints chat app" } input(classes = "nickNameInput") { id = "nickName" onInputFunction = onInput() maxLength = 16.toString() placeholder = "Enter your nick name" } button(classes = "loginButton") { +"Login" onClickFunction = onLoginButtonClicked() } } formContainer.appendChild(loginDiv) }
We developed an entire chat application using Kotlin. If you liked this extract, read our book Kotlin Blueprints to build a REST API using Kotlin.
Top 4 chatbot development frameworks for developers
How to build a basic server-side chatbot using Go