Creating a Single Page Todo App with Liquid
Wed, Dec 3, 2014Before starting this tutorial you should be familiar with the Dart language.
In this tutorial I’ll show how to build a simple Todo application with Liquid library.
The primary goal of the Liquid library is to create general purpose library for User Interfaces with reusable Components, and it does not enforce you to make everything reactive, immutable, or stateless, you are free to choose any way to structure your application.
Setup
Make sure that you have Dart SDK installed and running, the minimum version of the SDK is 1.6.
File Structure
File structure of our application will conform to the Pub Package Layout Conventions.
.
├── lib
│ ├── src
│ │ ├── models
│ │ │ ├── item.dart
│ │ │ └── item_list.dart
│ │ └── views
│ │ ├── app.dart
│ │ ├── header.dart
│ │ ├── item.dart
│ │ └── item_list.dart
│ ├── models.dart
│ └── views.dart
├── pubspec.yaml
└── web
├── index.dart
└── index.html
Installing Packages
Open pubspec.yaml
file in the project root
directory and make sure that you have this dependencies:
dependencies:
browser: any
liquid: any
Now run $ pub get
command from the project’s root directory to install all dependencies.
Data Model
We will start writing our application by defining Data Model.
Item
Item is an entry in our Todo List. It is quite simple, the only important thing is that it should have unique key, so we can easily find it. This key will be used in the Virtual DOM to find which Node represents this item.
class Item {
static int _nextId = 0; // Used for Auto-Incremental Unique Keys
final int id;
String title;
Item(this.title) : id = _nextId++;
}
ItemList
ItemList will contain all entries and will be responsible for all modifications. It also provides an event stream that emits events when something is changed.
class ItemList {
// Here we are creating Dart Streams to listen for
// notifications when something is changed.
//
// If you are not familiar with Dart Stream,
// you can read about them in this articles:
//
// https://www.dartlang.org/docs/tutorials/streams/
// https://www.dartlang.org/articles/creating-streams/
StreamController _onChangesController = new StreamController();
Stream get onChanges => _onChangesController.stream;
List<Item> items = [];
// Actions:
/// Create a new Todo Item
void createItem(String title) {
if (title.trim().isNotEmpty) {
items.add(new Item(title));
_onChangesController.add(null);
}
}
/// Update title property for Todo item
void updateItemTitle(int id, String newTitle) {
if (newTitle.trim().isEmpty) {
items.removeWhere((i) => i.id == id);
} else {
final item = items.firstWhere((i) => i.id == id);
item.title = newTitle;
}
_onChangesController.add(null);
}
}
Introduction to Virtual DOM
If you ever worked with the DOM directly, you understand how hard is to apply modification to the DOM when UI Component goes from one state to another.
There are couple solutions for this problem, and the most popular is the data-binding, that is used in libraries like Angular.
In the Liquid library we are using Virtual DOM with its diff/patch algorithm to apply changes to the actual DOM. When state is changed, we just rebuilding the Virtual DOM from the ground up and the diff/patch takes care of all changes.
Steven Luscher: Decomplexifying Code with React is a great explanation of complexity in UI Components.
Header Element
Now we will create our first Virtual DOM Node for Header.
final vHeader = v.staticTreeFactory(() =>
v.h1(id: 'header')('TODO Application'));
staticTreeFactory(buildFunction)
returns factory function that will
generate virtual dom nodes.
All Nodes that accepts children are implementing function call
interface to specify children Node()(children)
. Children argument
can be a simple String, single Node, or List of Nodes.
Introduction to Components
Components is just an extension to html Elements, they have an additional state, slightly more complex lifecycle and can render and update itself using Virtual DOM.
Application Component
It is time to build Component for our Application.
class App extends Component {
@property models.ItemList data;
v.VTextInput _input;
String _title = '';
void init() {
data.onChanges.listen((_) {
// Invalidate Component when data is changed.
//
// When we invalidate Component, it means that it will
// be updated on the next rendering frame.
//
// This way we can update DOM in batches, no need to
// update it as soon as possible, especially when the
// state can be changed mutiple times before browser
// starts to render new frame.
invalidate();
});
// Add Event Listeners using Event-Delegation.
element.onKeyPress.matches('input').listen((e) {
if (e.keyCode == KeyCode.ENTER) {
if (_input.value.isNotEmpty) {
data.createItem(_input.value);
_title = '';
}
e.stopPropagation();
e.preventDefault();
}
});
}
build() {
// Here we are assigning VTextInput to [_input] property, so we can
// reference it from the event listeners.
_input = v.textInput(value: _title);
return v.root()([
vHeader(),
vItemList(data: data),
_input
]);
}
}
ItemList
Item List will be a simple Virtual Dom Tree, no need to create a
stateful Component. But because it can change, we will use
dynamicTreeFactory
. By default all named arguments have the same
behavior as @property
. If you want to use immutable data structures,
just prepend @immutable
annotation before named argument.
final vItemList = v.dynamicTreeFactory(({data}) =>
v.ul()(data.items.map((i) =>
vItem(key: i.id,
data: data,
title: i.title,
itemId: i.id)).toList()));
Item Component
Item will be implemented as a Component because it has internal
state. To create Components inside of VirtualDOM trees we need to
create a factory for this Component with
componentFactory(Component)
function.
final vItem = v.componentFactory(Item);
class Item extends Component {
@property models.ItemList data;
@property int itemId;
@property String title;
bool _editing = false;
v.VTextInput _input;
void create() { element = new LIElement(); }
void init() {
element.onDoubleClick.matches('span').listen((e) {
_editing = true;
// We can't focus _input Element right now, because it will be created
// on the next frame. So we can use special [after] Future and wait
// until next frame is rendered.
domScheduler.nextFrame.after().then((_) {
if (_editing) {
_input.ref.focus();
}
});
invalidate();
e.stopPropagation();
e.preventDefault();
});
element.onBlur.capture((e) {
if (_editing) {
_editing = false;
data.updateItemTitle(itemId, _input.value);
}
});
}
build() {
var children;
if (_editing) {
_input = v.textInput(value: title);
children = [_input];
} else {
_input = null;
children = [v.span()(title)];
}
return v.root()(children);
}
}
Inserting Components into the DOM
Now we need to insert Application Component into the DOM, and we have
a special method for this injectComponent(component, parentElement)
.
void main() {
final data = new models.ItemList();
injectComponent(new views.App()..data = data, document.body);
}
Source Code
Source code is available at GitHub repository.