Formotor Todo MVC

Todo application with formotor.js

by Felix Yang

HTML

<!DOCTYPE html>
<html lang="en" data-framework="jquery">
  <head>
    <meta charset="utf-8" />
    <title>Formotor • TodoMVC</title>
  </head>
  <body>
    <section class="todoapp" fm-app>
      <header class="header" fm-component="todo-header">
        <h1>todos</h1>
        <input
          class="new-todo"
          placeholder="What needs to be done?"
          autofocus
        />
      </header>
      <section class="main" fm-component="todo-content">
        <input id="toggle-all" class="toggle-all" type="checkbox" />
        <label for="toggle-all">Mark all as complete</label>
        <ul class="todo-list"></ul>
      </section>
      <footer class="footer" fm-component="todo-footer"></footer>
    </section>
    <footer class="info">
      <p>Double-click to edit a todo</p>
      <p>Created by <a href="https://felixpy.com">Felix Yang</a></p>
      <p>Part of <a href="https://felixpy.github.io/formotor">Formotor</a></p>
    </footer>
    <script id="todo-template" type="text/x-handlebars-template">
      	{{#this}}
      	<li {{#if completed}}class="completed"{{/if}} data-id="{{id}}">
      		<div class="view">
      			<input class="toggle" type="checkbox" {{#if completed}}checked{{/if}}>
      			<label class="title">{{title}}</label>
      			<button class="destroy"></button>
      		</div>
      		<input class="edit" value="{{title}}">
      	</li>
      {{/this}}
    </script>
    <script id="footer-template" type="text/x-handlebars-template">
      <span class="todo-count"><strong>{{activeTodoCount}}</strong> {{activeTodoWord}} left</span>
      <ul class="filters">
      	<li>
      		<a {{#eq filter 'all'}}class="selected"{{/eq}} href="#/all">All</a>
      	</li>
      	<li>
      		<a {{#eq filter 'active'}}class="selected"{{/eq}}href="#/active">Active</a>
      	</li>
      	<li>
      		<a {{#eq filter 'completed'}}class="selected"{{/eq}}href="#/completed">Completed</a>
      	</li>
      </ul>
      {{#if completedTodos}}<button...

CSS

hr {
  margin: 20px 0;
  border: 0;
  border-top: 1px dashed #c5c5c5;
  border-bottom: 1px dashed #f7f7f7;
}

.learn a {
  font-weight: normal;
  text-decoration: none;
  color: #b83f45;
}

.learn a:hover {
  text-decoration: underline;
  color: #787e7e;
}

.learn h3,
.learn h4,
.learn h5 {
  margin: 10px 0;
  font-weight: 500;
  line-height: 1.2;
  color: #000;
}

.learn h3 {
  font-size: 24px;
}

.learn h4 {
  font-size: 18px;
}

.learn h5 {
  margin-bottom: 0;
  font-size: 14px;
}

.learn ul {
  padding: 0;
  margin: 0 0 30px 25px;
}

.learn li {
  line-height: 20px;
}

.learn p {
  font-size: 15px;
  font-weight: 300;
  line-height: 1.3;
  margin-top: 0;
  margin-bottom: 0;
}

#issue-count {
  display: none;
}

.quote {
  border: none;
  margin: 20px 0 60px 0;
}

.quote p {
  font-style: italic;
}

.quote p:before {
  content: '“';
  font-size: 50px;
  opacity: 0.15;
  position: absolute;
  top: -20px;
  left: 3px;
}

.quote p:after {
  content: '”';
  font-size: 50px;
  opacity: 0.15;
  position: absolute;
  bottom: -42px;
  right: 3px;
}

.quote footer {
  position: absolute;
  bottom: -40px;
  right: 0;
}

.quote footer img {
  border-radius: 3px;
}

.quote footer a {
  margin-left: 5px;
  vertical-align: middle;
}

.speech-bubble {
  position: relative;
  padding: 10px;
  background: rgba(0, 0, 0, 0.04);
  border-radius: 5px;
}

.speech-bubble:after {
  content: '';
  position: absolute;
  top: 100%;
  right: 30px;
  border: 13px solid transparent;
  border-top-color: rgba(0, 0, 0, 0.04);
}

.learn-bar > .learn {
  position: absolute;
  width: 272px;
  top: 8px;
  left: -300px;
  padding: 10px;
  border-radius: 5px;
  background-color: rgba(255, 255, 255, 0.6);
  transition-property: left;
  transition-duration: 500ms;
}

@media (min-width: 899px) {
  .learn-bar {
    width: auto;
    padding-left: 300px;
  }

  .learn-bar > .learn {
    left: 8px;
  }
}

html,
body {
  margin: 0;
  padding: 0;
}

button {
  margin: 0;
  padding: 0;
  border: 0;
 ...

JavaScript

/**
 * @file Todo application with formotor.js.
 * @author Felix Yang
 */

/* eslint-disable-next-line */
jQuery(function($) {
  'use strict'

  var Global = window || {}
  var Router = Global.Router || {}
  var Handlebars = Global.Handlebars || {}
  var Formotor = Global.Formotor || {}

  var KEYBOARD = {
    ENTER_KEY: 13,
    ESCAPE_KEY: 27
  }

  /**
   * Create application instance with tiny MVC.
   * @param {object} options
   * @returns {object}
   */
  var createMvcApp = (function () {
    var App = {
      version: '1.0.0'
    }
    var util = {
      pluralize: function (count, word) {
        return count === 1 ? word : word + 's'
      },

      compile: function (selector) {
        return Handlebars.compile($(selector).html())
      }
    }

    /**
     * Formotor component Config
     * @property {object}  todo-header     - todo input box.
     * @property {object}  todo-content    - todo list.
     * @property {object}  todo-footer     - todo status bar.
     */
    App.components = {
      'todo-header': {
        ready: function () {
          this.build()
        },
        methods: {
          build: function () {
            this.bindEvents()
          },

          bindEvents: function () {
            this.$el.on('keyup', '.new-todo', this.create)

            this.$listen('tree:render', this.render)
          },

          render: function () {
            this.$find('.new-todo').focus()
          },

          create: function (e) {
            var self = this
            var $element = $(e.target)
            var todoTask = $element.val().trim()

            if (e.which !== KEYBOARD.ENTER_KEY || !todoTask) {
              return
            }

            self.model.addTodo({
              title: todoTask,
              completed: false
            })

            $element.val('')

            self.$broadcast('tree:render')
          }
        }
      },

      'todo-content': {
        ready: function () {
          this.build()
      ...