How to Build a Real-Time GitHub Issue To-Do List with CanJS

CanJS is a collection of front-end libraries that make it easier to build complex and innovative web apps that are maintainable over a long period of time. It’s broken up into dozens of individual packages, so you can pick-and-choose what you’d like in your application without being bogged down by a huge 100kb+ dependency.

CanJS promotes the MVVM (Model-View-ViewModel) architecture with the following key packages:

In this tutorial, we’re going to make a to-do list app that uses a GitHub repository’s issue list as its source. Our app will update in real-time thanks to GitHub’s Webhook API and we’ll be able to reorder issues thanks to jQuery UI’s sortable interaction.

You can find the finished source code for this app on GitHub. Here’s what the final app will look like:

Gif of adding issues and sorting them in our example app

If you’re interested in taking your JavaScript skills to the next level, sign up for SitePoint Premium and check out our latest book, Modern JavaScript

MVVM in CanJS

Before we start our project for this tutorial, let’s dive into what MVVM means within a CanJS application.

Data Models

The “Model” in MVVM is for your data model: a representation of the data within your application. Our app deals with individual issues and a list of issues, so these are the data types that we have in our model.

In CanJS, we use can-define/list/list and can-define/map/map to represent arrays and objects, respectively. These are observable types of data that will automatically update the View or ViewModel (in MVVM) when they change.

For example, our app will have an Issue type like this:

import DefineMap from 'can-define/map/map';
const Issue = DefineMap.extend('Issue', {
  id: 'number',
  title: 'string',
  sort_position: 'number',
  body: 'string'
});

Each instance of Issue will have four properties: id, title, sort_position, and body. When a value is set, can-define/map/map will convert that value to the type specified above, unless the value is null or undefined. For example, setting the id to the string "1" will give the id property the number value 1, while setting it to null will actually make it null.

We’ll define a type for arrays of issues like this:

import DefineList from 'can-define/list/list';
Issue.List = DefineList.extend('IssueList', {
  '#': Issue
});

The # property on a can-define/list/list will convert any item in the list to the specified type, so any item in an Issue.List will be an Issue instance.

View Templates

The “view” in a web application is the HTML user interface with which users interact. CanJS can render HTML with a few different template syntaxes, including can-stache, which is similar to Mustache and Handlebars.

Here’s a simple example of a can-stache template:

<ol>
  {{#each issues}}
    <li>
      {{title}}
    </li>
  {{/each}}
</ol>

In the above example, we use {{#each}} to iterate through a list of issues, then show the title of each issue with {{title}}. Any changes to the issues list or the issue titles will cause the DOM to be updated (e.g. an li will be added to the DOM if a new issue is added to the list).

View Models

The ViewModel in MVVM is the glue code between the Model and View. Any logic that can’t be contained within the model but is necessary for the view is provided by the ViewModel.

In CanJS, a can-stache template is rendered with a ViewModel. Here’s a really simple example:

import stache from 'can-stache';
const renderer = stache('{{greeting}} world');
const viewModel = {greeting: 'Hello'};
const fragment = renderer(viewModel);
console.log(fragment.textContent);// Logs “Hello world”

Components

The concept that ties all of these things together is a component (or custom element). Components are useful for grouping functionality together and making things reusable across your entire app.

In CanJS, a can-component is made up of a view (can-stache file), a view-model (can-define/map/map), and (optionally) an object that can listen for JavaScript events.

import Component from 'can-component';
import DefineMap from 'can-define/map/map';
import stache from 'can-stache';

const HelloWorldViewModel = DefineMap.extend('HelloWorldVM', {
  greeting: {value: 'Hello'},
  showExclamation: {value: true}
});

Component.extend({
  tag: 'hello-world',
  view: stache('{{greeting}} world{{#if showExclamation}}!{{/if}}'),
  ViewModel: HelloWorldViewModel,
  events: {
    '{element} click': () => {
      this.viewModel.showExclamation = !this.viewModel.showExclamation;
    }
  }
});

const template = stache('hello-world');
document.body.appendChild(template);

In the example above, our template will either show “Hello world!” or just “Hello world” (no exclamation mark), depending on whether the user has clicked our custom element.

These four concepts are all you need to know to build a CanJS app! Our example app will use these four ideas to build a full-fledged MVVM app.

Prerequisites for this tutorial

Before getting started, install a recent version of Node.js. We’ll use npm to install a backend server that will handle the communication with GitHub’s API.

Additionally, if you don’t already have a GitHub account, sign up for one.

Set up our local project

Let’s start by creating a new directory for our project and switching to that new directory:

mkdir canjs-github
cd canjs-github

Now let’s create the files we’ll need for our project:

touch app.css app.js index.html

We’ll use app.css for our styles, app.js for our JavaScript, and index.html for the user interface (UI).

CanJS Hello World

Let’s get coding! First, we’re going to add this to our index.html file:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>CanJS GitHub Issues To-Do List</title>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
  <link rel="stylesheet" href="app.css">
</head>
<body>

<script type="text/stache" id="app-template">
  <div class="container">
    <div class="row">
      <div class="col-md-8 col-md-offset-2">
        <h1 class="page-header text-center">
          {{pageTitle}}
        </h1>
      </div>
    </div>
  </div>
</script>

<script type="text/stache" id="github-issues-template">
</script>

<script src="https://unpkg.com/jquery@3/dist/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>
<script src="https//canjs.com/socket.io/socket.io.js"></script>
<script src="app.js"></script>
</body>
</html>

This has a bunch of different parts, so let’s break it down:

  • The two link elements in the head are the stylesheets for our project. We’re using Bootstrap for some base styles and we’ll have some customizations in app.css
  • The first script element (with id="app-template") contains the root template for our app
  • The second script element (with id="github-issues-template") will contain the template for the github-issues component we will create later in this tutorial
  • The script elements at the end of the page load our dependencies: jQuery, jQuery UI, CanJS, Socket.io, and our app code

In our app, we’ll use jQuery UI (which depends on jQuery) to sort the issues with drag and drop. We’ve included can.all.js so we have access to every CanJS module; normally, you would want to use a module loader like StealJS or webpack, but that’s outside the scope of this article. We’ll use Socket.io to receive events from GitHub to update our app in real-time.

Next, let’s add some styles to our app.css file:

form {
  margin: 1em 0 2em 0;
}

.list-group .drag-background {
  background-color: #dff0d8;
}

.text-overflow {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

Lastly, let’s add some code to our app.js file:

var AppViewModel = can.DefineMap.extend('AppVM', {
  pageTitle: {
    type: "string",
    value: "GitHub Issues",
  }
});

var appVM = new AppViewModel();
var template = can.stache.from('app-template');
var appFragment = template(appVM);
document.body.appendChild(appFragment);

Let’s break the JavaScript down:

  • can.DefineMap is used for declaring custom observable object types
  • AppViewModel is the observable object type that will serve as the root view-model for our app
  • pageTitle is a property of all AppViewModel instances that defaults to the value GitHub Issues
  • appVM is a new instance of our app’s view-model
  • can.stache.from converts the contents of a script tag into a function that renders the template
  • appFragment is a document fragment of the rendered template with the appVM data
  • document.body.appendChild takes a DOM node and appends it to the HTML body

Continue reading %How to Build a Real-Time GitHub Issue To-Do List with CanJS%


Source: Sitepoint