-
Notifications
You must be signed in to change notification settings - Fork 6
Kunafa Todo App
In this tutorial we will build a Todo app using Kunafa. This tutorial will cover the basic Kunafa concepts, including building views, styles, components, and components lifecycle. Also, we will be using MVVM architecture and Observables, though you don't have to. Here is what will be building.
And here is the complete code for the demo.
To get things going, create a new Kotlin/Js project (you can follow this guide), and inside the main()
call the page
function.
fun main(args: Array<String>) {
page {
}
}
The page
component correspond to an HTML body and it is the root of the app. Next, we will create a horizontal layout that has two vertical layouts children.
fun main(args: Array<String>) {
page {
horizontalLayout {
style {
width = matchParent
height = matchParent
}
verticalLayout {
style {
width = weightOf(1)
minWidth = 200.px
height = matchParent
backgroundColor = Color.white
padding = 32.px.toString()
alignItems = Alignment.Center
}
textView {
text = "Kunafa Todo"
style {
fontSize = 32.px
color = Color(100, 240, 100)
}
}
textInput {
style {
width = matchParent
backgroundColor = Color("#fafafa")
border = "1px solid #efefef"
padding = 8.px.toString()
borderRadius = 4.px.toString()
marginTop = 16.px
}
}
button {
id = "myButton"
text = "Add to do"
style {
marginTop = 16.px
}
}
}
verticalScrollLayout {
style {
width = weightOf(2)
height = matchParent
backgroundColor = Color("#ededed")
}
verticalLayout {
style {
width = matchParent
height = wrapContent
padding = 8.px.toString()
}
}
}
}
}
}
Notice that the style
function is used to define the views styles. The styles are mostly CSS with some helpful values to abstract the CSS border cases such as matchParent
, and wrapContent
. For example, the first vertical layout width is weightOf(1)
and the second is weightOf(2)
which means that the second is twice as big as the first one. Also, notice that the second vertical layout is a verticalScrollLayout
to allow its content to be scrollable. Keep in mind that for verticalScrollLayout
, its height should never be wrap content. It should be either match parent or fixed size.
Now the add button does nothing. We want it to create a new todo item when clicked. To do so, we will need to add a click listener and hold reference to the list verticalLayout
. To keep thing tight and clean, let's create a component to hold all these references and logic.
Extracting the above view to a component is pretty easy. Just create a component and move the view to the getView function as follows:
fun main(args: Array<String>) {
page {
mount(TodoComponent())
}
}
class TodoComponent : Component() {
override fun View?.getView() = horizontalLayout {
style {
width = matchParent
height = matchParent
}
verticalLayout {
style {
width = weightOf(1)
minWidth = 200.px
height = matchParent
backgroundColor = Color.white
padding = 32.px.toString()
alignItems = Alignment.Center
}
textView {
text = "Kunafa Todo"
style {
fontSize = 32.px
color = Color(100, 240, 100)
}
}
textInput {
style {
width = matchParent
backgroundColor = Color("#fafafa")
border = "1px solid #efefef"
padding = 8.px.toString()
borderRadius = 4.px.toString()
marginTop = 16.px
}
}
button {
id = "myButton"
text = "Add to do"
style {
marginTop = 16.px
}
}
}
verticalScrollLayout {
style {
width = weightOf(2)
height = matchParent
backgroundColor = Color("#ededed")
}
verticalLayout {
style {
width = matchParent
height = wrapContent
padding = 8.px.toString()
}
}
}
}
}
Now, we need a reference to the todo items list layout, and the text input (in order to clear it after a new todo item is added). Inside the TodoComponent
, define the following:
private var listLayout: LinearLayout? = null
private var todoTextInput: TextInput? = null
and then assigns to them their respective views.
todoTextInput = textInput {
style {
width = matchParent
backgroundColor = Color("#fafafa")
border = "1px solid #efefef"
padding = 8.px.toString()
borderRadius = 4.px.toString()
marginTop = 16.px
}
}
and
listLayout = verticalLayout {
style {
width = matchParent
height = wrapContent
padding = 8.px.toString()
}
}
To separate the logic from the view, we'll create a ViewModel to maintain the state and logic of the Todo component. But before doing so, we need to define what a Todo Item is. A todo presentation model is what holds the todo item data.
data class TodoPm(val text: String, var isDone: Boolean = false) {
val id: Int = nextId
companion object {
private var nextId = 0
get() = field++
}
}
The id is sequential and is automatically assigned (with the help of the companion object
).
Now, the view model can be defined as follows:
class TodoViewModel {
val onItemAdded = Observable<TodoPm>()
private val todoItemsList = mutableListOf<TodoPm>()
fun addNewTodo(todoText: String?) {
if (todoText.isNullOrBlank()) return
val pm = TodoPm(todoText)
todoItemsList.add(pm)
onItemAdded.value = pm
}
}
The view model holds the todoItemsList
and communicate the changes of the list through Observables. Notice that the view model does not hold reference to any view.
Going back to the view, the add button needs a click listener. We need to call the viewModel.addNewTodo()
function. First, let's pass the TodoViewModel to the TodoComponent.
class TodoComponent(private val viewModel: TodoViewModel) : Component()
and in the main()
page {
mount(TodoComponent(TodoViewModel()))
}
then inside the TodoComponent, let's create onButtonClicked()
function
private fun onButtonClicked() {
viewModel.addNewTodo(todoTextInput?.text)
todoTextInput?.text = ""
}
and finally, add the click listener:
button {
id = "myButton"
text = "Add to do"
style {
marginTop = 16.px
}
onClick = {
onButtonClicked()
}
}
Now, the TodoComponent
needs to be listening to the Observable
in the view model. This is go to the onViewCreated
lifecycle. This is called once when the view is created.
override fun onViewCreated(lifecycleOwner: LifecycleOwner) {
viewModel.onItemAdded.observe(::addItem)
}
private fun addItem(pm: TodoPm?) {
pm ?: return
val component = TodoItem(pm)
listLayout?.mount(component)
todoViews[pm.id] = component
}
where todoViews
is defined as
private val todoViews = mutableMapOf<Int, TodoItem>()
and TodoItem
is
class TodoItem(
private val todoPm: TodoPm
) : Component() {
override fun View?.getView() = horizontalLayout {
addRuleSet(Style.rootLayout)
view {
addRuleSet(Style.circleBasic)
}
textView {
style {
width = weightOf(1)
fontSize = 16.px
}
text = todoPm.text
}
}
companion object {
object Style {
val circleBasic = classRuleSet {
width = 8.px
height = 8.px
borderRadius = 8.px.toString()
border = "1px solid #888"
marginRight = 8.px
}
val rootLayout = classRuleSet {
width = matchParent
border = "1px solid #d4d4d4"
marginTop = 8.px
padding = 8.px.toString()
alignItems = Alignment.Center
cursor = "pointer"
backgroundColor = Color.white
hover {
boxShadow = "0px 4px 3px #bbb"
}
}
}
}
}
Notice how we did not use the style
function to define styles in TodoItem
and however we used rulesets inside the companion object
and then added it with addRuleSet(Style.circleBasic)
. This is because the TodoItem
is created multiple times and we don't want a new style created for each item.
Well, we've created the basic blocks of the app. To allow items to be deleted, you can do the same as we did before. Add delete button to the TodoItem
, and call the view model when it's clicked. Add a deleteItem(id: Int)
function inside the view model and communicate the changes to the TodoComponent
with Observable. The same goes for toggling the state.
You can find the full code here.
We've created a complete Todo App in this tutorial. We hope that this will give you an understanding of how Kunafa is used to create applications.