In the previous part of this tutorial, we walked through how to make a very basic version of a ToDo app using Wikimedia’s OOUI library. Now it’s time to add a way to store and display information from our items.
Let’s first create a way to view the information we have about our items. We’ll start by adding a simple label to our page:
$( function () {
const input = new OO.ui.TextInputWidget( {
placeholder: 'Add a ToDo item'
} ),
list = new OO.ui.SelectWidget( {
classes: [ 'todo-list' ]
} ),
info = new OO.ui.LabelWidget( {
label: 'Information',
classes: [ 'todo-info' ]
} );
// ... code ...
// Append the app widgets
$( '.wrapper' ).append(
input.$element,
list.$element,
info.$element
);
} );
Once again, we’re adding a widget, and appending its
$element
to the DOM. Now we can use it to display the information stored
in our widget. The ToDo items all live inside an
OO.ui.SelectWidget
which emits a ‘choose’ event when an element is clicked
or chosen, with the reference to the chosen item as the parameter. We’ll attach
a listener to this event.
$( function () {
// ... code ...
list.on( 'choose', function ( item ) {
info.setLabel( item.getData() );
} );
// ... code ...
} );
Now we have a very simple way of presenting the data stored in our item.
This is a good start, but it doesn’t yet seem to be all that helpful, because the data stored in each of our items is the same as its label, and doesn’t quite give us any useful information. Let’s change that now.
In order to expand the functionality of the
OO.ui.OptionWidget
so we can store more information, we need to
extend it and create our own class.
Create a new file in your assets/
directory,
called ToDoItemWidget.js
. Reopen your
index.html
and add it in.
<script src="assets/ToDoItemWidget.js">
Back to ToDoItemWidget.js
. I this new file,
we'll create our new class:
const ToDoItemWidget = function ( config ) {
// Configuration object is optional
config = config || {};
// Call parent constructor
ToDoItemWidget.super.call( this, config );
};
// Inheritance
OO.inheritClass( ToDoItemWidget, OO.ui.OptionWidget );
Our new function extends OO.ui.OptionWidget
,
by declaring OO.inheritClass( ToDoItemWidget,
OO.ui.OptionWidget );
and by calling the parent constructor in the
new class’ constructor.
Now, change the code to use the new
ToDoItemWidget
class:
$( function () {
// ... code ...
// Respond to 'enter' keypress
input.on( 'enter', function () {
// ... code ...
// Add the item
list.addItems( [
new ToDoItemWidget( {
data: input.getValue(),
label: input.getValue()
} )
] );
} );
// ... code ...
} );
You can try out your app again, but nothing should be different just yet. We can now start developing our new class to add the functionality we want it to have.
Let’s add a property that stores the creation time of our todo item.
const ToDoItemWidget = function ( config ) {
config = config || {};
// Call parent constructor
ToDoItemWidget.super.call( this, config );
this.creationTime = config.creationTime;
};
OO.inheritClass( ToDoItemWidget, OO.ui.OptionWidget );
ToDoItemWidget.prototype.getCreationTime = function () {
return this.creationTime;
};
There are a many different ways to format time in JavaScript. For the purpose of learning how to extend a class with our own methods, we will write our own time formatting method.
Still in the same file, namely ToDoItemWidget.js
, just after the getCreationTime()
method, define a new method, getPrettyCreationTime()
.
We'll use this method to make the ToDo item's creation time look just the way we want.
ToDoItemWidget.prototype.getPrettyCreationTime = function () {
const time = new Date( this.creationTime ),
hour = time.getHours(),
minute = time.getMinutes(),
second = time.getSeconds(),
monthNames = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
];
let temp = String( ( hour > 12 ) ? hour - 12 : hour ),
if ( hour === 0 ) {
temp = '12';
}
temp += ( ( minute < 10 ) ? ':0' : ':' ) + minute;
temp += ( ( second < 10 ) ? ':0' : ':' ) + second;
temp += ( hour >= 12 ) ? ' P.M.' : ' A.M.';
return [
time.getDate(),
monthNames[ time.getMonth() ],
time.getFullYear() + ', ',
temp
].join( ' ' );
};
Now we can set this when we instantiate the object in our initialization script, and change the info label to display this new property.
$( function () {
// ... code ...
input.on( 'enter', function () {
// ... code ...
// Add the item
list.addItems( [
new ToDoItemWidget( {
data: input.getValue(),
label: input.getValue(),
creationTime: Date.now()
} )
] );
} );
// ... code ...
list.on( 'choose', function ( item ) {
info.setLabel( item.getData() + ' (' +
item.getPrettyCreationTime() + ')' );
} );
// ... code ...
} );
Try it out:
It's working!
But we’re not done.
We’ve already enhanced the items, so let’s add some real functionality in there.
Another of OOUI’s concepts is componentization—the ability to create bigger widgets from smaller ones. This can be pretty powerful and allow for advanced functionality.
We’re going to start small. Let’s add a ‘delete’ button to our list of items.
You can read about what OO.ui.ButtonWidget
expects in its configuration options in the
documentation.
In our ToDoItemWidget.js
we’ll add a button:
const ToDoItemWidget = function ( config ) {
// ... code ...
this.deleteButton = new OO.ui.ButtonWidget( {
label: 'Delete'
} );
this.$element.append( this.deleteButton.$element );
};
Just like any other widget in OOUI, OO.ui.ButtonWidget
has the $element
property that contains its jQuery
object. We’re attaching that to our own widget.
If you look at your app now, though, you’ll see that the button appears under the label. That’s because we need to add styles. Since we’re building our own widget, let’s do things properly and add a standalone style for it that we can then add and tweak in our CSS rules.
const ToDoItemWidget = function ( config ) {
// ... code ...
this.deleteButton = new OO.ui.ButtonWidget( {
label: 'Delete'
} );
this.$element
.addClass( 'todo-itemWidget' )
.append( this.deleteButton.$element );
};
And the CSS rules:
.todo-itemWidget {
display: inline-table;
}
.todo-itemWidget.oo-ui-optionWidget.oo-ui-labelElement >
.oo-ui-labelElement-label {
display: table-cell;
width: 100%;
padding: 0.5em;
}
.todo-itemWidget .oo-ui-buttonWidget {
display: table-cell;
}
There, that looks better.
Now, let’s add functionality to this button.
One of the best things about using an OO.ui.SelectWidget
for our list, is that it uses the OO.ui.mixin.GroupElement
that allows for really cool operations on a group of items.
One of those operations is an aggregation of events.
In effect, we can have each of our items emit a certain event, and have our list aggregate all of those events and respond whenever any of its items have emitted it. This means our logic can live “up” in the parent widget, consolidating our work with our items.
This means, however, that we will need to enhance our list object. We are going to do exactly what we did
for our items (by creating the ToDoItemWidget class) but with a new
ToDoListWidget class that extends OO.ui.SelectWidget
.
The new file ToDoListWidget.js
:
const ToDoListWidget = function ToDoListWidget( config ) {
config = config || {};
// Call parent constructor
ToDoListWidget.super.call( this, config );
};
OO.inheritClass( ToDoListWidget, OO.ui.SelectWidget );
Attach it to index.html
and change the initialization code:
<!-- ... -->
<!-- ToDo app -->
<link rel="stylesheet" href="todo.css">
<script src="assets/ToDoItemWidget.js"></script>
<script src="assets/ToDoListWidget.js"></script>
$( function () {
// ... code ...
list = new ToDoListWidget( {
classes: [ 'todo-list' ]
} ),
// ... code ...
} );
Now that we have our ToDoListWidget
we can aggregate its item events.
So how is it actually done? First, let’s have our button emit a “delete” event for our item:
const ToDoItemWidget = function ( config ) {
// ... code ...
this.deleteButton.connect( this, {
click: 'onDeleteButtonClick'
} );
// ... code ...
};
// ... code ...
ToDoItemWidget.prototype.onDeleteButtonClick = function () {
this.emit( 'delete' );
};
Notice that this time, we didn’t use .on( ... )
but rather
.connect( this, { ... } )
. This is because we are now connecting
the object we are currently “in” the context of, to the event. I’ve used “on” before when we were
in the general initialization script, and had no context to give the event emitter.
The string ‘onDeleteButtonClick’ refers to the method of the same name. When ‘click’ is emitted from that button, that method is invoked. It, in turn, will emit “delete” event.
Now, we need to make sure that the list is listening to this event from all of its sub-items. We will first aggregate the event and then listen to the aggregated event and respond to it:
const ToDoListWidget = function ToDoListWidget( config ) {
// ... code ...
this.aggregate( {
delete: 'itemDelete'
} );
this.connect( this, {
itemDelete: 'onItemDelete'
} );
};
OO.inheritClass( ToDoListWidget, OO.ui.SelectWidget );
ToDoListWidget.prototype.onItemDelete = function ( itemWidget ) {
this.removeItems( [ itemWidget ] );
};
We’ve used this.aggregate()
to tell the group which events to
listen to in its items, and we’ve used this.connect( this, { ... } );
to connect our own object to the event we aggregated.
Then, the responding method (onItemDelete
) removes the item from the list.
You can now add and remove items from your ToDo app, yay!
Now that the app has basic operations, we can call this tutorial over. We hope that you got a good taste as to what OOjs UI is like, and the potential it holds in quickly – but efficiently – developing JavaScript apps.
1. Full code for index.html
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>ToDo OOUI</title>
<meta name="description" content="A demo ToDo app made with OOUI">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- jQuery -->
<script src="node_modules/jquery/dist/jquery.min.js"></script>
<!-- OOjs -->
<script src="node_modules/oojs/dist/oojs.min.js"></script>
<!-- OOUI -->
<script src="node_modules/oojs-ui/dist/oojs-ui.min.js"></script>
<!-- OOUI theme -->
<script src="node_modules/oojs-ui/dist/oojs-ui-wikimediaui.min.js"></script>
<link rel="stylesheet" href="node_modules/oojs-ui/dist/oojs-ui-wikimediaui.min.css">
<!-- ToDo app custom -->
<link rel="stylesheet" href="todo.css">
<script src="assets/ToDoItemWidget.js"></script>
<script src="assets/ToDoListWidget.js"></script>
<script src="assets/init.js"></script>
</head>
<body>
<div class="wrapper">
<h1>Demo ToDo app with OOUI</h1>
</div>
</body>
</html>
2. Full code for init.js
$( function () {
const input = new OO.ui.TextInputWidget( {
placeholder: 'Add a ToDo item'
} ),
list = new ToDoListWidget( {
classes: [ 'todo-list' ]
} ),
info = new OO.ui.LabelWidget( {
label: 'Information',
classes: [ 'todo-info' ]
} );
// Respond to 'enter' keypress
input.on( 'enter', function () {
// Check for duplicates and prevent empty input
if ( list.findItemFromData( input.getValue() ) ||
input.getValue() === '' ) {
input.$element.addClass( 'todo-error' );
return;
}
input.$element.removeClass( 'todo-error' );
list.on( 'choose', function ( item ) {
info.setLabel( item.getData() + ' (' +
item.getPrettyCreationTime() + ')' );
} );
// Add the item
list.addItems( [
new ToDoItemWidget( {
data: input.getValue(),
label: input.getValue(),
creationTime: Date.now()
} )
] );
input.setValue( '' );
} );
// Append the app widgets
$( '.wrapper' ).append(
input.$element,
list.$element,
info.$element
);
} );
3. Full code for ToDoItemWidget.js
const ToDoItemWidget = function ( config ) {
config = config || {};
ToDoItemWidget.super.call( this, config );
this.creationTime = config.creationTime;
this.deleteButton = new OO.ui.ButtonWidget( {
label: 'Delete'
} );
this.$element
.addClass( 'todo-itemWidget' )
.append( this.deleteButton.$element );
this.deleteButton.connect( this, {
click: 'onDeleteButtonClick'
} );
};
OO.inheritClass( ToDoItemWidget, OO.ui.OptionWidget );
ToDoItemWidget.prototype.getCreationTime = function () {
return this.creationTime;
};
ToDoItemWidget.prototype.getPrettyCreationTime = function () {
const time = new Date( this.creationTime ),
hour = time.getHours(),
minute = time.getMinutes(),
second = time.getSeconds(),
monthNames = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
];
let temp = String( ( hour > 12 ) ? hour - 12 : hour );
if ( hour === 0 ) {
temp = '12';
}
temp += ( ( minute < 10 ) ? ':0' : ':' ) + minute;
temp += ( ( second < 10 ) ? ':0' : ':' ) + second;
temp += ( hour >= 12 ) ? ' P.M.' : ' A.M.';
return [
time.getDate(),
monthNames[ time.getMonth() ],
time.getFullYear() + ', ',
temp
].join( ' ' );
};
ToDoItemWidget.prototype.onDeleteButtonClick = function () {
this.emit( 'delete' );
};
4. Full code for ToDoListWidget.js
const ToDoListWidget = function ToDoListWidget( config ) {
config = config || {};
// Call parent constructor
ToDoListWidget.super.call( this, config );
this.aggregate( {
delete: 'itemDelete'
} );
this.connect( this, {
itemDelete: 'onItemDelete'
} );
};
OO.inheritClass( ToDoListWidget, OO.ui.SelectWidget );
ToDoListWidget.prototype.onItemDelete = function ( itemWidget ) {
this.removeItems( [ itemWidget ] );
};
5. Full code for todo.css
.todo-error input {
background-color: #ff9696;
}
.wrapper {
width: 60%;
margin-left: auto;
margin-right: auto;
}
.todo-list .oo-ui-optionWidget {
border-bottom: 1px solid #666;
}
.oo-ui-selectWidget-unpressed .oo-ui-optionWidget-selected {
background-color: #80ccff;
}
.oo-ui-optionWidget-highlighted {
background-color: #b9e3ff;
}
.oo-ui-inputWidget {
margin-bottom: 0.5em;
}
.todo-list .oo-ui-labelElement-label {
margin-left: 0.25em;
}
.oo-ui-labelElement .oo-ui-optionWidget {
padding: 0.25em;
}
.todo-itemWidget {
display: inline-table;
}
.todo-itemWidget.oo-ui-optionWidget.oo-ui-labelElement >
.oo-ui-labelElement-label {
display: table-cell;
width: 100%;
padding: 0.5em;
}
.todo-itemWidget .oo-ui-buttonWidget {
display: table-cell;
}