Skip to content

Instantly share code, notes, and snippets.

@justinbmeyer
Last active October 24, 2018 17:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save justinbmeyer/40c92425b679018f58404fbf6464dffa to your computer and use it in GitHub Desktop.
Save justinbmeyer/40c92425b679018f58404fbf6464dffa to your computer and use it in GitHub Desktop.

Intro

CanJS 5.0 (https://www.bitovi.com/blog/canjs-5) is out and it makes building CRUD apps easier than should be possible. In this talk, we'll learn the basics of CanJS and show how to build an app that Creates, Reads, Updates, and Deletes (CRUD) data. The app will handle all the things folks normally forget too. Things like server errors, slow loading, and disabling buttons to prevent repeat form submissions. Getting in the CRUD will never feel so good.

CRUD is all around us

The CodePen we are building. It has:

  • The ability to Create, Read, Update, and Delete data.
  • Most of the important mechanics work.
    • When you create a todo, it appears in the list. In the right spot.
    • Pagination, Sorting, Filtering
    • When you edit a todo, it moves to the right spot.
    • When todos are being deleted or updated, their controls are disabled.
    • When there are no items, it says "no todos".
    • Say "Loading" while loading. Say "deleting" while deleting.

Outline:

Setup

List Data

  • Show empty
  • Show rejection

HTML:

<div class="container-fluid">
  <h1>Todos</h1>
  <todo-list></todo-list>
</div>

JS:

import {Component} from "//unpkg.com/can@5/core.mjs";

Component.extend({
  tag: "todo-list",
  view: `
    <table class="table">
      <tbody>
        {{# if(todosPromise.isResolved) }}
          {{# for(todo of todosPromise.value) }}
            <tr>
              <td>{{ todo.complete }}</td>
              <td>{{ todo.name }}</td>
              <td>{{ todo.dueDate }}</td>
            </tr>
          {{ else }}
            <tr><td class='table-info'>No todos</td></tr>
          {{/ for }}
        {{/ if }}
        {{# if(todosPromise.isPending) }}
          <tr><td class='table-active'>Loading</td></tr>
        {{/ if }}
      </tbody>
    </table>
  `,
  ViewModel: {
    get todosPromise() {
      // fetch("/api/todos")
      return new Promise((resolve) => {
        setTimeout(()=>{
          resolve([
            {id: 1, name: "fold laundry",
             dueDate: new Date(2018,2,2).toString(), complete: false},
            {id: 2, name: "clean carpet",
             dueDate: new Date(2018,3,3).toString(), complete: true},
            {id: 4, name: "dust fan",
             dueDate: new Date(2018,4,4).getTime(), complete: false}
          ]);
        },2000);
      });
    }
  }
});

Model your data

  • Shows converting to actual Date type allows toLocaleDateString

JS:

import {Component, DefineMap, DefineList} from "//unpkg.com/can@5/core.mjs";

const Todo = DefineMap.extend("Todo",{
  id: { type: "number", identity: true },
  complete: { type: "boolean", default: false },
  dueDate: {type: "date", Default: Date},
  name: "string"
});

Todo.List = DefineList.extend("TodoList",{
  "#": Todo
});

Component.extend({
  tag: "todo-list",
  view: `
    <table class="table">
      <tbody>
        {{# if(todosPromise.isResolved) }}
          {{# for(todo of todosPromise.value) }}
            <tr>
              <td>{{ todo.complete }}</td>
              <td>{{ todo.name }}</td>
              <td>{{ todo.dueDate.toLocaleDateString() }}</td>
            </tr>
          {{ else }}
            <tr><td class='table-info'>No todos</td></tr>
          {{/ for }}
        {{/ if }}
        {{# if(todosPromise.isPending) }}
          <tr><td class='table-active'>Loading</td></tr>
        {{/ if }}
      </tbody>
    </table>
  `,
  ViewModel: {
    get todosPromise() {
      // fetch("/api/todos")
      return new Promise((resolve) => {
        setTimeout(()=>{
          resolve(new Todo.List([
            {id: 1, name: "fold laundry",
             dueDate: new Date(2018,2,2).toString(), complete: false},
            {id: 2, name: "clean carpet",
             dueDate: new Date(2018,3,3).toString(), complete: true},
            {id: 4, name: "dust fan",
             dueDate: new Date(2018,4,4).getTime(), complete: false}
          ]));
        },2000);
      });
    }
  }
});

Create Services Pt 1

  • make an ajax request to a fake data store!

JS:

import {Component, DefineMap, DefineList, fixture, ajax} from "//unpkg.com/can@5/core.mjs";

const Todo = DefineMap.extend("Todo",{
  id: { type: "number", identity: true },
  complete: { type: "boolean", default: false },
  dueDate: {type: "date", Default: Date},
  name: "string"
});

Todo.List = DefineList.extend("TodoList",{
  "#": Todo
});

mockService();

Component.extend({
  tag: "todo-list",
  view: `
    <table class="table">
      <tbody>
        {{# if(todosPromise.isResolved) }}
          {{# for(todo of todosPromise.value) }}
            <tr>
              <td>{{ todo.complete }}</td>
              <td>{{ todo.name }}</td>
              <td>{{ todo.dueDate.toLocaleDateString() }}</td>
            </tr>
          {{ else }}
            <tr><td class='table-info'>No todos</td></tr>
          {{/ for }}
        {{/ if }}
        {{# if(todosPromise.isPending) }}
          <tr><td class='table-active'>Loading</td></tr>
        {{/ if }}
      </tbody>
    </table>
  `,
  ViewModel: {
    get todosPromise() {
      // fetch("/api/todos")
      return ajax({
        url: "/api/todos"
      }).then(function(response){
        return new Todo.List(response.data);
      });
    }
  }
});


function mockService(){

  var todoStore = fixture.store([
    {id: 1, name: "fold laundry",
     dueDate: new Date(2018,2,2), complete: false},
    {id: 2, name: "clean carpet",
     dueDate: new Date(2018,3,3), complete: true},
    {id: 4, name: "get gas",
     dueDate: new Date(2018,4,4), complete: false}
  ], Todo);

  fixture("/api/todos/{id}", todoStore);
  fixture.delay = 1000;
}

Create Services Pt 2

  • Create lots of fake data
import {Component, DefineMap, DefineList, ajax} from "//unpkg.com/can@5/core.mjs";

const Todo = DefineMap.extend("Todo",{
  id: { type: "number", identity: true },
  complete: { type: "boolean", default: false },
  dueDate: {type: "date", Default: Date},
  name: "string"
});

Todo.List = DefineList.extend("TodoList",{
  "#": Todo
});

mockService();

Component.extend({
  tag: "todo-list",
  view: `
    <table class="table">
      <tbody>
        {{# if(todosPromise.isResolved) }}
          {{# for(todo of todosPromise.value) }}
            <tr>
              <td>{{ todo.complete }}</td>
              <td>{{ todo.name }}</td>
              <td>{{ todo.dueDate.toLocaleDateString() }}</td>
            </tr>
          {{ else }}
            <tr><td class='table-info'>No todos</td></tr>
          {{/ for }}
        {{/ if }}
        {{# if(todosPromise.isPending) }}
          <tr><td class='table-active'>Loading</td></tr>
        {{/ if }}
      </tbody>
    </table>
  `,
  ViewModel: {
    get todosPromise() {
      return ajax({
        url: "/api/todos"
      }).then(function(response){
        return new Todo.List(response.data);
      });
    }
  }
});


function mockService(){

  var terms = ["can you","please","","","",""],
    verbs = ["clean","walk","do","vacuum","organize","fold",
          "wash","dust","pay","cook","get","take out"],
    subjects = ["dog","laundry","diapers","clothes","car",
          "windows","carpet","taxes","food","gas","trash"];

  var dayInMS = 24*60*60*1000;
  var lastWeek = new Date() - (7*dayInMS);
  var fourWeeks = new Date().getTime() + (4*7*dayInMS);

  var todoStore = fixture.store(100, function(){
    return {
      complete: fixture.rand([true, false],1)[0],
      dueDate: new Date( fixture.rand(lastWeek, fourWeeks) ).toString(),
      name: (fixture.rand(terms,1)[0]+" "+
        fixture.rand(verbs,1)[0]+" "+
        fixture.rand(subjects,1)[0]).trim()
    }
  }, Todo);

  fixture("/api/todos/{id}", todoStore);
  fixture.delay = 1000;
}

Request data with a model

Using a model:

import {Component, DefineMap, DefineList, fixture, restModel} from "//unpkg.com/can@5/core.mjs";

const Todo = DefineMap.extend("Todo",{
  id: { type: "number", identity: true },
  complete: { type: "boolean", default: false },
  dueDate: {type: "date", Default: Date},
  name: "string"
});

Todo.List = DefineList.extend("TodoList",{
  "#": Todo
});

restModel({
  Map: Todo,
  List: Todo.List,
  url: "/api/todos/{id}"
});

mockService();

Component.extend({
  tag: "todo-list",
  view: `
    <table class="table">
      <tbody>
        {{# if(todosPromise.isResolved) }}
          {{# for(todo of todosPromise.value) }}
            <tr>
              <td>{{ todo.complete }}</td>
              <td>{{ todo.name }}</td>
              <td>{{ todo.dueDate.toLocaleDateString() }}</td>
            </tr>
          {{ else }}
            <tr><td class='table-info'>No todos</td></tr>
          {{/ for }}
        {{/ if }}
        {{# if(todosPromise.isPending) }}
          <tr><td class='table-active'>Loading</td></tr>
        {{/ if }}
      </tbody>
    </table>
  `,
  ViewModel: {
    get todosPromise() {
      return Todo.getList({});
    }
  }
});


function mockService(){

  var terms = ["can you","please","","","",""],
    verbs = ["clean","walk","do","vacuum","organize","fold",
          "wash","dust","pay","cook","get","take out"],
    subjects = ["dog","laundry","diapers","clothes","car",
          "windows","carpet","taxes","food","gas","trash"];

  var dayInMS = 24*60*60*1000;
  var lastWeek = new Date() - (7*dayInMS);
  var fourWeeks = new Date().getTime() + (4*7*dayInMS);

  var todoStore = fixture.store(100, function(){
    return {
      complete: fixture.rand([true, false],1)[0],
      dueDate: new Date( fixture.rand(lastWeek, fourWeeks) ).toString(),
      name: (fixture.rand(terms,1)[0]+" "+
        fixture.rand(verbs,1)[0]+" "+
        fixture.rand(subjects,1)[0]).trim()
    }
  }, Todo);

  fixture("/api/todos/{id}", todoStore);
  fixture.delay = 1000;
}

Paginating

import {Component, DefineMap, DefineList, fixture, restModel} from "//unpkg.com/can@5/core.mjs";

const Todo = DefineMap.extend("Todo",{
  id: { type: "number", identity: true },
  complete: { type: "boolean", default: false },
  dueDate: {type: "date", Default: Date},
  name: "string"
});

Todo.List = DefineList.extend("TodoList",{
  "#": Todo
});

restModel({
  Map: Todo,
  List: Todo.List,
  url: "/api/todos/{id}"
});

mockService();

Component.extend({
  tag: "todo-list",
  view: `
    <form class="form-inline mb-3">
      Results:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="count">
        <option value="">All</option>
        <option value="5">5</option>
        <option value="20">20</option>
      </select>
    </form>

    <table class="table">
      <tbody>
        {{# if(todosPromise.isResolved) }}
          {{# for(todo of todosPromise.value) }}
            <tr>
              <td>{{ todo.complete }}</td>
              <td>{{ todo.name }}</td>
              <td>{{ todo.dueDate.toLocaleDateString() }}</td>
            </tr>
          {{ else }}
            <tr><td class='table-info'>No todos</td></tr>
          {{/ for }}
        {{/ if }}
        {{# if(todosPromise.isPending) }}
          <tr><td class='table-active'>Loading</td></tr>
        {{/ if }}
      </tbody>
    </table>
  `,
  ViewModel: {
    // Stateful properties
    count: {type:"string", default: "5"},

    // Derived properties
    get todosPromise() {
      var query = {filter: { }};
      if(this.count) {
        query.page = {
          start: 0,
          end: (+this.count)-1
        };
      }
      return Todo.getList(query);
    }
  }
});


function mockService(){

  var terms = ["can you","please","","","",""],
    verbs = ["clean","walk","do","vacuum","organize","fold",
          "wash","dust","pay","cook","get","take out"],
    subjects = ["dog","laundry","diapers","clothes","car",
          "windows","carpet","taxes","food","gas","trash"];

  var dayInMS = 24*60*60*1000;
  var lastWeek = new Date() - (7*dayInMS);
  var fourWeeks = new Date().getTime() + (4*7*dayInMS);

  var todoStore = fixture.store(100, function(){
    return {
      complete: fixture.rand([true, false],1)[0],
      dueDate: new Date( fixture.rand(lastWeek, fourWeeks) ).toString(),
      name: (fixture.rand(terms,1)[0]+" "+
        fixture.rand(verbs,1)[0]+" "+
        fixture.rand(subjects,1)[0]).trim()
    }
  }, Todo);

  fixture("/api/todos/{id}", todoStore);
  fixture.delay = 1000;
}

Sorting

import {Component, DefineMap, DefineList, fixture, restModel} from "//unpkg.com/can@5/core.mjs";

const Todo = DefineMap.extend("Todo",{
  id: { type: "number", identity: true },
  complete: { type: "boolean", default: false },
  dueDate: {type: "date", Default: Date},
  name: "string"
});

Todo.List = DefineList.extend("TodoList",{
  "#": Todo
});

restModel({
  Map: Todo,
  List: Todo.List,
  url: "/api/todos/{id}"
});

mockService();

Component.extend({
  tag: "todo-list",
  view: `
    <form class="form-inline mb-3">
      Results:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="count">
        <option value="">All</option>
        <option value="5">5</option>
        <option value="20">20</option>
      </select>

      Sort By:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="sort">
        <option value="">none</option>
        <option value="name">name</option>
        <option value="dueDate">dueDate</option>
      </select>
    </form>

    <table class="table">
      <tbody>
        {{# if(todosPromise.isResolved) }}
          {{# for(todo of todosPromise.value) }}
            <tr>
              <td>{{ todo.complete }}</td>
              <td>{{ todo.name }}</td>
              <td>{{ todo.dueDate.toLocaleDateString() }}</td>
            </tr>
          {{ else }}
            <tr><td class='table-info'>No todos</td></tr>
          {{/ for }}
        {{/ if }}
        {{# if(todosPromise.isPending) }}
          <tr><td class='table-active'>Loading</td></tr>
        {{/ if }}
      </tbody>
    </table>
  `,
  ViewModel: {
    // Stateful properties
    count: {type:"string", default: "5"},
    sort: "string",

    // Derived properties
    get todosPromise() {
      var query = {filter: { }};
      if(this.count) {
        query.page = {
          start: 0,
          end: (+this.count)-1
        };
      }
      if(this.sort) {
        query.sort =  this.sort;
      }
      return Todo.getList(query);
    }
  }
});


function mockService(){

  var terms = ["can you","please","","","",""],
    verbs = ["clean","walk","do","vacuum","organize","fold",
          "wash","dust","pay","cook","get","take out"],
    subjects = ["dog","laundry","diapers","clothes","car",
          "windows","carpet","taxes","food","gas","trash"];

  var dayInMS = 24*60*60*1000;
  var lastWeek = new Date() - (7*dayInMS);
  var fourWeeks = new Date().getTime() + (4*7*dayInMS);

  var todoStore = fixture.store(100, function(){
    return {
      complete: fixture.rand([true, false],1)[0],
      dueDate: new Date( fixture.rand(lastWeek, fourWeeks) ).toString(),
      name: (fixture.rand(terms,1)[0]+" "+
        fixture.rand(verbs,1)[0]+" "+
        fixture.rand(subjects,1)[0]).trim()
    }
  }, Todo);

  fixture("/api/todos/{id}", todoStore);
  fixture.delay = 1000;
}

Filtering Complete

import {Component, DefineMap, DefineList, fixture, restModel} from "//unpkg.com/can@5/core.mjs";

const Todo = DefineMap.extend("Todo",{
  id: { type: "number", identity: true },
  complete: { type: "boolean", default: false },
  dueDate: {type: "date", Default: Date},
  name: "string"
});

Todo.List = DefineList.extend("TodoList",{
  "#": Todo
});

restModel({
  Map: Todo,
  List: Todo.List,
  url: "/api/todos/{id}"
});

mockService();

Component.extend({
  tag: "todo-list",
  view: `
    <form class="form-inline mb-3">
      Results:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="count">
        <option value="">All</option>
        <option value="5">5</option>
        <option value="20">20</option>
      </select>

      Sort By:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="sort">
        <option value="">none</option>
        <option value="name">name</option>
        <option value="dueDate">dueDate</option>
      </select>

      Show:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="completeFilter">
        <option value="">All</option>
        <option value="complete">Complete</option>
        <option value="incomplete">Incomplete</option>
      </select>

    </form>

    <table class="table">
      <tbody>
        {{# if(todosPromise.isResolved) }}
          {{# for(todo of todosPromise.value) }}
            <tr>
              <td>{{ todo.complete }}</td>
              <td>{{ todo.name }}</td>
              <td>{{ todo.dueDate.toLocaleDateString() }}</td>
            </tr>
          {{ else }}
            <tr><td class='table-info'>No todos</td></tr>
          {{/ for }}
        {{/ if }}
        {{# if(todosPromise.isPending) }}
          <tr><td class='table-active'>Loading</td></tr>
        {{/ if }}
      </tbody>
    </table>
  `,
  ViewModel: {
    // Stateful properties
    count: {type:"string", default: "5"},
    sort: "string",
    completeFilter: "string",

    // Derived properties
    get todosPromise() {
      var query = {filter: { }};
      if(this.count) {
        query.page = {
          start: 0,
          end: (+this.count)-1
        };
      }
      if(this.sort) {
        query.sort =  this.sort;
      }
      if(this.completeFilter) {
        query.filter.complete = this.completeFilter === "complete";
      }
      return Todo.getList(query);
    }
  }
});


function mockService(){

  var terms = ["can you","please","","","",""],
    verbs = ["clean","walk","do","vacuum","organize","fold",
          "wash","dust","pay","cook","get","take out"],
    subjects = ["dog","laundry","diapers","clothes","car",
          "windows","carpet","taxes","food","gas","trash"];

  var dayInMS = 24*60*60*1000;
  var lastWeek = new Date() - (7*dayInMS);
  var fourWeeks = new Date().getTime() + (4*7*dayInMS);

  var todoStore = fixture.store(100, function(){
    return {
      complete: fixture.rand([true, false],1)[0],
      dueDate: new Date( fixture.rand(lastWeek, fourWeeks) ).toString(),
      name: (fixture.rand(terms,1)[0]+" "+
        fixture.rand(verbs,1)[0]+" "+
        fixture.rand(subjects,1)[0]).trim()
    }
  }, Todo);

  fixture("/api/todos/{id}", todoStore);
  fixture.delay = 1000;
}

Filtering Due Date

JS:

import {Component, DefineMap, DefineList, fixture, restModel} from "//unpkg.com/can@5/core.mjs";

const Todo = DefineMap.extend("Todo",{
  id: { type: "number", identity: true },
  complete: { type: "boolean", default: false },
  dueDate: {type: "date", Default: Date},
  name: "string"
});

Todo.List = DefineList.extend("TodoList",{
  "#": Todo
});

restModel({
  Map: Todo,
  List: Todo.List,
  url: "/api/todos/{id}"
});

mockService();

Component.extend({
  tag: "todo-list",
  view: `
    <form class="form-inline mb-3">
      Results:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="count">
        <option value="">All</option>
        <option value="5">5</option>
        <option value="20">20</option>
      </select>

      Sort By:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="sort">
        <option value="">none</option>
        <option value="name">name</option>
        <option value="dueDate">dueDate</option>
      </select>

      Show:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="completeFilter">
        <option value="">All</option>
        <option value="complete">Complete</option>
        <option value="incomplete">Incomplete</option>
      </select>

      Due:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="dueFilter">
        <option value="">Anytime</option>
        <option value="today">Today</option>
        <option value="week">This Week</option>
      </select>

    </form>

    <table class="table">
      <tbody>
        {{# if(todosPromise.isResolved) }}
          {{# for(todo of todosPromise.value) }}
            <tr>
              <td>{{ todo.complete }}</td>
              <td>{{ todo.name }}</td>
              <td>{{ todo.dueDate.toLocaleDateString() }}</td>
            </tr>
          {{ else }}
            <tr><td class='table-info'>No todos</td></tr>
          {{/ for }}
        {{/ if }}
        {{# if(todosPromise.isPending) }}
          <tr><td class='table-active'>Loading</td></tr>
        {{/ if }}
      </tbody>
    </table>
  `,
  ViewModel: {
    // Stateful properties
    count: {type:"string", default: "5"},
    sort: "string",
    completeFilter: "string",
    dueFilter: "string",

    // Derived properties
    get todosPromise() {
      var query = {filter: { }};
      if(this.count) {
        query.page = {
          start: 0,
          end: (+this.count)-1
        };
      }
      if(this.sort) {
        query.sort =  this.sort;
      }
      if(this.completeFilter) {
        query.filter.complete = this.completeFilter === "complete";
      }
      if(this.dueFilter) {
        var day = 24*60*60*1000;
        var now = new Date();
        var today = new Date(now.getFullYear(), now.getMonth(), now.getDate() );
        if(this.dueFilter === "today") {

          query.filter.dueDate = {
            $gte: today.toString(),
            $lt: new Date(today.getTime() + day).toString()
          }
        }
        if(this.dueFilter === "week") {
          var start = today.getTime() - (today.getDay() * day);
          query.filter.dueDate = {
            $gte: new Date(start).toString(),
            $lt: new Date(start + 7*day).toString()
          };
        }
      }
      return Todo.getList(query);
    }
  }
});


function mockService(){

  var terms = ["can you","please","","","",""],
    verbs = ["clean","walk","do","vacuum","organize","fold",
          "wash","dust","pay","cook","get","take out"],
    subjects = ["dog","laundry","diapers","clothes","car",
          "windows","carpet","taxes","food","gas","trash"];

  var dayInMS = 24*60*60*1000;
  var lastWeek = new Date() - (7*dayInMS);
  var fourWeeks = new Date().getTime() + (4*7*dayInMS);

  var todoStore = fixture.store(100, function(){
    return {
      complete: fixture.rand([true, false],1)[0],
      dueDate: new Date( fixture.rand(lastWeek, fourWeeks) ).toString(),
      name: (fixture.rand(terms,1)[0]+" "+
        fixture.rand(verbs,1)[0]+" "+
        fixture.rand(subjects,1)[0]).trim()
    }
  }, Todo);

  fixture("/api/todos/{id}", todoStore);
  fixture.delay = 1000;
}

Delete Records

  1. show adding .destroy()
  2. remove the element
  3. Disable the delete button
import {Component, DefineMap, DefineList, fixture, realtimeRestModel} from "//unpkg.com/can@5/core.mjs"; //👀

const Todo = DefineMap.extend("Todo",{
  id: { type: "number", identity: true },
  complete: { type: "boolean", default: false },
  dueDate: {type: "date", Default: Date},
  name: "string"
});

Todo.List = DefineList.extend("TodoList",{
  "#": Todo
});

realtimeRestModel({ //👀
  Map: Todo,
  List: Todo.List,
  url: "/api/todos/{id}"
});

mockService();

Component.extend({
  tag: "todo-list",
  view: `
    <form class="form-inline mb-3">
      Results:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="count">
        <option value="">All</option>
        <option value="5">5</option>
        <option value="20">20</option>
      </select>

      Sort By:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="sort">
        <option value="">none</option>
        <option value="name">name</option>
        <option value="dueDate">dueDate</option>
      </select>

      Show:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="completeFilter">
        <option value="">All</option>
        <option value="complete">Complete</option>
        <option value="incomplete">Incomplete</option>
      </select>

      Due:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="dueFilter">
        <option value="">Anytime</option>
        <option value="today">Today</option>
        <option value="week">This Week</option>
      </select>

    </form>

    <table class="table">
      <tbody>
        {{# if(todosPromise.isResolved) }}
          {{# for(todo of todosPromise.value) }}
            <tr>
              <td>{{ todo.complete }}</td>
              <td>{{ todo.name }}</td>
              <td>{{ todo.dueDate.toLocaleDateString() }}</td>
              <td>
                <button
                  class="btn btn-danger btn-sm"
                  on:click="todo.destroy()"
                  disabled:from="todo.isDestroying()">
                    {{# if(todo.isDestroying()) }}
                      deleting
                    {{else}}
                      delete
                    {{/ if}}
                </button>
              </td>
            </tr>
          {{ else }}
            <tr><td class='table-info'>No todos</td></tr>
          {{/ for }}
        {{/ if }}
        {{# if(todosPromise.isPending) }}
          <tr><td class='table-active'>Loading</td></tr>
        {{/ if }}
      </tbody>
    </table>
  `,
  ViewModel: {
    // Stateful properties
    count: {type:"string", default: "5"},
    sort: "string",
    completeFilter: "string",
    dueFilter: "string",

    // Derived properties
    get todosPromise() {
      var query = {filter: { }};
      if(this.count) {
        query.page = {
          start: 0,
          end: (+this.count)-1
        };
      }
      if(this.sort) {
        query.sort =  this.sort;
      }
      if(this.completeFilter) {
        query.filter.complete = this.completeFilter === "complete";
      }
      if(this.dueFilter) {
        var day = 24*60*60*1000;
        var now = new Date();
        var today = new Date(now.getFullYear(), now.getMonth(), now.getDate() );
        if(this.dueFilter === "today") {

          query.filter.dueDate = {
            $gte: today.toString(),
            $lt: new Date(today.getTime() + day).toString()
          }
        }
        if(this.dueFilter === "week") {
          var start = today.getTime() - (today.getDay() * day);
          query.filter.dueDate = {
            $gte: new Date(start).toString(),
            $lt: new Date(start + 7*day).toString()
          };
        }
      }
      return Todo.getList(query);
    }
  }
});


function mockService(){

  var terms = ["can you","please","","","",""],
    verbs = ["clean","walk","do","vacuum","organize","fold",
          "wash","dust","pay","cook","get","take out"],
    subjects = ["dog","laundry","diapers","clothes","car",
          "windows","carpet","taxes","food","gas","trash"];

  var dayInMS = 24*60*60*1000;
  var lastWeek = new Date() - (7*dayInMS);
  var fourWeeks = new Date().getTime() + (4*7*dayInMS);

  var todoStore = fixture.store(100, function(){
    return {
      complete: fixture.rand([true, false],1)[0],
      dueDate: new Date( fixture.rand(lastWeek, fourWeeks) ).toString(),
      name: (fixture.rand(terms,1)[0]+" "+
        fixture.rand(verbs,1)[0]+" "+
        fixture.rand(subjects,1)[0]).trim()
    }
  }, Todo);

  fixture("/api/todos/{id}", todoStore);
  fixture.delay = 1000;
}

Update a todo's complete property

  • Toggle a todo's complete
  • Should be added or removed from the list.
import {Component, DefineMap, DefineList, fixture, realtimeRestModel} from "//unpkg.com/can@5/core.mjs";

const Todo = DefineMap.extend("Todo",{
  id: { type: "number", identity: true },
  complete: { type: "boolean", default: false },
  dueDate: {type: "date", Default: Date},
  name: "string",
  preventSave(){
    return !this.name || this.isSaving() || this.isDestroying();
  }
});

Todo.List = DefineList.extend("TodoList",{
  "#": Todo
});

realtimeRestModel({
  Map: Todo,
  List: Todo.List,
  url: "/api/todos/{id}"
});

mockService();

Component.extend({
  tag: "todo-list",
  view: `
    <form class="form-inline mb-3">
      Results:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="count">
        <option value="">All</option>
        <option value="5">5</option>
        <option value="20">20</option>
      </select>

      Sort By:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="sort">
        <option value="">none</option>
        <option value="name">name</option>
        <option value="dueDate">dueDate</option>
      </select>

      Show:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="completeFilter">
        <option value="">All</option>
        <option value="complete">Complete</option>
        <option value="incomplete">Incomplete</option>
      </select>

      Due:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="dueFilter">
        <option value="">Anytime</option>
        <option value="today">Today</option>
        <option value="week">This Week</option>
      </select>

    </form>

    <table class="table">
      <tbody>
        {{# if(todosPromise.isResolved) }}
          {{# for(todo of todosPromise.value) }}
            <tr>
              <td>
                <input type='checkbox'
                  checked:bind='todo.complete'
                  on:change="todo.save()"
                  disabled:from="todo.preventSave()"/>
              </td>
              <td>{{ todo.name }}</td>
              <td>{{ todo.dueDate.toLocaleDateString() }}</td>
              <td>
                <button
                  class="btn btn-danger btn-sm"
                  on:click="todo.destroy()"
                  disabled:from="todo.isDestroying()">
                    {{# if(todo.isDestroying()) }}
                      deleting
                    {{else}}
                      delete
                    {{/ if}}
                </button>
              </td>
            </tr>
          {{ else }}
            <tr><td class='table-info'>No todos</td></tr>
          {{/ for }}
        {{/ if }}
        {{# if(todosPromise.isPending) }}
          <tr><td class='table-active'>Loading</td></tr>
        {{/ if }}
      </tbody>
    </table>
  `,
  ViewModel: {
    // Stateful properties
    count: {type:"string", default: "5"},
    sort: "string",
    completeFilter: "string",
    dueFilter: "string",

    // Derived properties
    get todosPromise() {
      var query = {filter: { }};
      if(this.count) {
        query.page = {
          start: 0,
          end: (+this.count)-1
        };
      }
      if(this.sort) {
        query.sort =  this.sort;
      }
      if(this.completeFilter) {
        query.filter.complete = this.completeFilter === "complete";
      }
      if(this.dueFilter) {
        var day = 24*60*60*1000;
        var now = new Date();
        var today = new Date(now.getFullYear(), now.getMonth(), now.getDate() );
        if(this.dueFilter === "today") {

          query.filter.dueDate = {
            $gte: today.toString(),
            $lt: new Date(today.getTime() + day).toString()
          }
        }
        if(this.dueFilter === "week") {
          var start = today.getTime() - (today.getDay() * day);
          query.filter.dueDate = {
            $gte: new Date(start).toString(),
            $lt: new Date(start + 7*day).toString()
          };
        }
      }
      return Todo.getList(query);
    }
  }
});


function mockService(){

  var terms = ["can you","please","","","",""],
    verbs = ["clean","walk","do","vacuum","organize","fold",
          "wash","dust","pay","cook","get","take out"],
    subjects = ["dog","laundry","diapers","clothes","car",
          "windows","carpet","taxes","food","gas","trash"];

  var dayInMS = 24*60*60*1000;
  var lastWeek = new Date() - (7*dayInMS);
  var fourWeeks = new Date().getTime() + (4*7*dayInMS);

  var todoStore = fixture.store(100, function(){
    return {
      complete: fixture.rand([true, false],1)[0],
      dueDate: new Date( fixture.rand(lastWeek, fourWeeks) ).toString(),
      name: (fixture.rand(terms,1)[0]+" "+
        fixture.rand(verbs,1)[0]+" "+
        fixture.rand(subjects,1)[0]).trim()
    }
  }, Todo);

  fixture("/api/todos/{id}", todoStore);
  fixture.delay = 1000;
}

Update a todo's dueDate property

import {Component, DefineMap, DefineList, fixture, realtimeRestModel} from "//unpkg.com/can@5/core.mjs";

const Todo = DefineMap.extend("Todo",{
  id: { type: "number", identity: true },
  complete: { type: "boolean", default: false },
  dueDate: {type: "date", Default: Date},
  name: "string",
  preventSave(){
    return !this.name || this.isSaving() || this.isDestroying();
  }
});

Todo.List = DefineList.extend("TodoList",{
  "#": Todo
});

realtimeRestModel({
  Map: Todo,
  List: Todo.List,
  url: "/api/todos/{id}"
});

mockService();

Component.extend({
  tag: "todo-list",
  view: `
    <form class="form-inline mb-3">
      Results:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="count">
        <option value="">All</option>
        <option value="5">5</option>
        <option value="20">20</option>
      </select>

      Sort By:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="sort">
        <option value="">none</option>
        <option value="name">name</option>
        <option value="dueDate">dueDate</option>
      </select>

      Show:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="completeFilter">
        <option value="">All</option>
        <option value="complete">Complete</option>
        <option value="incomplete">Incomplete</option>
      </select>

      Due:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="dueFilter">
        <option value="">Anytime</option>
        <option value="today">Today</option>
        <option value="week">This Week</option>
      </select>

    </form>

    <table class="table">
      <tbody>
        {{# if(todosPromise.isResolved) }}
          {{# for(todo of todosPromise.value) }}
            <tr>
              <td>
                <input type='checkbox'
                  checked:bind='todo.complete'
                  on:change="todo.save()"
                  disabled:from="todo.preventSave()"/>
              </td>
              <td>{{ todo.name }}</td>
              <td>
                <input type='date' class='form-control'
                  valueAsDate:bind='todo.dueDate'
                  on:change="todo.save()"
                  disabled:from="todo.preventSave()"/>
              </td>
              <td>
                <button
                  class="btn btn-danger btn-sm"
                  on:click="todo.destroy()"
                  disabled:from="todo.isDestroying()">
                    {{# if(todo.isDestroying()) }}
                      deleting
                    {{else}}
                      delete
                    {{/ if}}
                </button>
              </td>
            </tr>
          {{ else }}
            <tr><td class='table-info'>No todos</td></tr>
          {{/ for }}
        {{/ if }}
        {{# if(todosPromise.isPending) }}
          <tr><td class='table-active'>Loading</td></tr>
        {{/ if }}
      </tbody>
    </table>
  `,
  ViewModel: {
    // Stateful properties
    count: {type:"string", default: "5"},
    sort: "string",
    completeFilter: "string",
    dueFilter: "string",

    // Derived properties
    get todosPromise() {
      var query = {filter: { }};
      if(this.count) {
        query.page = {
          start: 0,
          end: (+this.count)-1
        };
      }
      if(this.sort) {
        query.sort =  this.sort;
      }
      if(this.completeFilter) {
        query.filter.complete = this.completeFilter === "complete";
      }
      if(this.dueFilter) {
        var day = 24*60*60*1000;
        var now = new Date();
        var today = new Date(now.getFullYear(), now.getMonth(), now.getDate() );
        if(this.dueFilter === "today") {

          query.filter.dueDate = {
            $gte: today.toString(),
            $lt: new Date(today.getTime() + day).toString()
          }
        }
        if(this.dueFilter === "week") {
          var start = today.getTime() - (today.getDay() * day);
          query.filter.dueDate = {
            $gte: new Date(start).toString(),
            $lt: new Date(start + 7*day).toString()
          };
        }
      }
      return Todo.getList(query);
    }
  }
});


function mockService(){

  var terms = ["can you","please","","","",""],
    verbs = ["clean","walk","do","vacuum","organize","fold",
          "wash","dust","pay","cook","get","take out"],
    subjects = ["dog","laundry","diapers","clothes","car",
          "windows","carpet","taxes","food","gas","trash"];

  var dayInMS = 24*60*60*1000;
  var lastWeek = new Date() - (7*dayInMS);
  var fourWeeks = new Date().getTime() + (4*7*dayInMS);

  var todoStore = fixture.store(100, function(){
    return {
      complete: fixture.rand([true, false],1)[0],
      dueDate: new Date( fixture.rand(lastWeek, fourWeeks) ).toString(),
      name: (fixture.rand(terms,1)[0]+" "+
        fixture.rand(verbs,1)[0]+" "+
        fixture.rand(subjects,1)[0]).trim()
    }
  }, Todo);

  fixture("/api/todos/{id}", todoStore);
  fixture.delay = 1000;
}

Create Todos

HTML:

<div class="container-fluid">
  <h1>Todos</h1>
  <todo-list></todo-list>
  <h2>Create Todos</h2>
  <todo-create></todo-create>
</div>
import {realtimeRestModel, DefineMap, DefineList, Component, fixture} from "//unpkg.com/can@5/core.mjs";

const Todo = DefineMap.extend("Todo",{
  id: { type: "number", identity: true },
  complete: { type: "boolean", default: false },
  dueDate: {type: "date", Default: Date},
  name: "string",
  preventSave(){
    return !this.name || this.isSaving() || this.isDestroying();
  }
});

Todo.List = DefineList.extend("TodoList",{
  "#": Todo
});

realtimeRestModel({
  Map: Todo,
  List: Todo.List,
  url: "/api/todos/{id}"
});

mockService();

Component.extend({
  tag: "todo-list",
  view: `
    <form class="form-inline mb-3">
      Results:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="count">
        <option value="">All</option>
        <option value="5">5</option>
        <option value="20">20</option>
      </select>

      Sort By:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="sort">
        <option value="">none</option>
        <option value="name">name</option>
        <option value="dueDate">dueDate</option>
      </select>

      Show:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="completeFilter">
        <option value="">All</option>
        <option value="complete">Complete</option>
        <option value="incomplete">Incomplete</option>
      </select>

      Due:
      <select class='form-control mr-3 ml-1 btn-sm'
        value:bind="dueFilter">
        <option value="">Anytime</option>
        <option value="today">Today</option>
        <option value="week">This Week</option>
      </select>

    </form>
    <table class="table">
      <tbody>
        {{# if( this.todosPromise.isResolved ) }}
          {{# for( todo of this.todosPromise.value ) }}
            <tr>
              <td>
                <input type='checkbox'
                  checked:bind='todo.complete'
                  on:change="todo.save()"
                  disabled:from="todo.preventSave()"/>
              </td>
              <td>{{ todo.name }}</td>
              <td>
                <input type='date' class='form-control'
                  valueAsDate:bind='todo.dueDate'
                  on:change="todo.save()"
                  disabled:from="todo.preventSave()"/>
              </td>
              <td>
                <button
                  class="btn btn-danger btn-sm"
                  on:click="todo.destroy()"
                  disabled:from="todo.isDestroying()">
                    {{# if(todo.isDestroying()) }}
                      deleting
                    {{else}}
                      delete
                    {{/ if}}
                </button>
              </td>
            </tr>
          {{ else }}
            <tr><td class='table-info'>No todos</td></tr>
          {{/ for }}
        {{/ if }}
        {{# if( this.todosPromise.isPending ) }}
          <tr><td class='table-active'>Loading</td></tr>
        {{/ if }}
      </tbody>
    </table>
  `,
  ViewModel: {
    sort: "string",
    completeFilter: "string",
    dueFilter: "string",
    count: {type:"string", default: "5"},
    get todosPromise() {
      var query = {filter: { }};
      if(this.sort) {
        query.sort =  this.sort;
      }
      if(this.completeFilter) {
        query.filter.complete = this.completeFilter === "complete";
      }
      if(this.dueFilter) {
        var day = 24*60*60*1000;
        var now = new Date();
        var today = new Date(now.getFullYear(), now.getMonth(), now.getDate() );
        if(this.dueFilter === "today") {

          query.filter.dueDate = {
            $gte: today.toString(),
            $lt: new Date(today.getTime() + day).toString()
          }
        }
        if(this.dueFilter === "week") {
          var start = today.getTime() - (today.getDay() * day);
          query.filter.dueDate = {
            $gte: new Date(start).toString(),
            $lt: new Date(start + 7*day).toString()
          };
        }
      }
      if(this.count) {
        query.page = {
          start: 0,
          end: (+this.count)-1
        };
      }
      return Todo.getList(query);
    }
  }
});

Component.extend({
  tag: "todo-create",
  view: `
    <form on:submit="createTodo(scope.event)">
      <div class="form-group">
        <label>Name</label>
        <input class="form-control"
          on:input:value:bind='this.todo.name'/>
      </div>
      <div class="form-group form-check">
        <input type='checkbox' class="form-check-input"
          checked:bind='this.todo.complete'/>
        <label class="form-check-label">Complete</label>
      </div>
      <div class="form-group">
        <label>Date</label>
        <input type='date'  class="form-control"
            valueAsDate:bind='this.todo.dueDate'/>
      </div>
      <button disabled:from="todo.preventSave()"
        class="btn btn-primary">
        {{# if(this.todo.isSaving()) }}
          Creating ....
        {{ else }}
          Create Todo
        {{/if}}
      </button>

    </form>
  `,
  ViewModel: {
    todo: {
      Default: Todo
    },
    createTodo(event) {
      event.preventDefault();
      this.todo.save().then((createdTodo) => {
        this.todo = new Todo();
      })
    }
  }
});

function mockService(){
  var terms = ["can you","please","","","",""],
    verbs = ["clean","walk","do","vacuum","organize","fold",
          "wash","dust","pay","cook","get","take out"],
    subjects = ["dog","laundry","diapers","clothes","car",
          "windows","carpet","taxes","food","gas","trash"];

  var dayInMS = 24*60*60*1000;
  var lastWeek = new Date() - (7*dayInMS);
  var fourWeeks = new Date().getTime() + (4*7*dayInMS);

  var todoStore = fixture.store(100, function(){
    return {
      complete: fixture.rand([true, false],1)[0],
      dueDate: new Date( fixture.rand(lastWeek, fourWeeks) ).toString(),
      name: (fixture.rand(terms,1)[0]+" "+
        fixture.rand(verbs,1)[0]+" "+
        fixture.rand(subjects,1)[0]).trim()
    }
  }, Todo);

  fixture("/api/todos/{id}", todoStore);
  fixture.delay = 1000;
}

Conclusion

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment