OOUI ToDo App Tutorial - Part 2

# OOUI Basics: Part 2

## Custom Widgets for a ToDo List App

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.

## Displaying Info

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:

    $( document ).ready( function () {
    	var 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.

    $( document ).ready( 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.

## Creating a Custom Item Widget

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:

    var ToDoItemWidget = function ( config ) {
    	// Configuration object is optional
    	config = config || {};

    	// Call parent constructor
    	ToDoItemWidget.parent.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:

    $( document ).ready( 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.

## Adding Functionality

Let’s add a property that stores the creation time of our todo item.

    var ToDoItemWidget = function ( config ) {
    	config = config || {};

    	// Call parent constructor
    	ToDoItemWidget.parent.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 () {
    	var time = new Date( this.creationTime ),
    		hour = time.getHours(),
    		minute = time.getMinutes(),
    		second = time.getSeconds(),
    		temp = String( ( hour > 12 ) ? hour - 12 : hour ),
    		monthNames = [
    			'Jan',
    			'Feb',
    			'Mar',
    			'Apr',
    			'May',
    			'Jun',
    			'Jul',
    			'Aug',
    			'Sep',
    			'Oct',
    			'Nov',
    			'Dec'
    		];

    	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.

    $( document ).ready( 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.

## Adding a ‘Delete’ Button to Items

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:

    var 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.

    var 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.

## Aggregating Events

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`.

## Creating a ToDoListWidget

The new file `ToDoListWidget.js`:

    var ToDoListWidget = function ToDoListWidget( config ) {
    	config = config || {};

    	// Call parent constructor
    	ToDoListWidget.parent.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>

    $( document ).ready( function () {
    	// ... code ...
    		list = new ToDoListWidget( {
    			classes: [ 'todo-list' ]
    		} ),
    	// ... code ...
    } );

## Responding to aggregation of events

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:

    var 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:

    var 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.

## The complete code

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.jquery.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`

    $( document ).ready( function () {
    	var 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`

    var ToDoItemWidget = function ( config ) {
    	config = config || {};
    	ToDoItemWidget.parent.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 () {
    	var time = new Date( this.creationTime ),
    		hour = time.getHours(),
    		minute = time.getMinutes(),
    		second = time.getSeconds(),
    		temp = String( ( hour > 12 ) ? hour - 12 : hour ),
    		monthNames = [
    			'Jan',
    			'Feb',
    			'Mar',
    			'Apr',
    			'May',
    			'Jun',
    			'Jul',
    			'Aug',
    			'Sep',
    			'Oct',
    			'Nov',
    			'Dec'
    		];

    	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`

    var ToDoListWidget = function ToDoListWidget( config ) {
    	config = config || {};

    	// Call parent constructor
    	ToDoListWidget.parent.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-depressed .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;
    }