Commit 63b63f46 authored by TasteBot's avatar TasteBot

update the build files for gh-pages [ci skip]

parent 26747e55
module Task where
import Html (..)
import Html.Attributes (..)
import Html.Events (..)
import Json.Decode as Json
import LocalChannel as LC
import Maybe
import Signal
import String
-- MODEL
type alias Model =
{ description : String
, completed : Bool
, edits : Maybe String
, id : Int
}
init : String -> Int -> Model
init desc id =
{ description = desc
, completed = False
, edits = Nothing
, id = id
}
-- UPDATE
type Action
= Focus
| Edit String
| Cancel
| Commit
| Completed Bool
| Delete
update : Action -> Model -> Maybe Model
update update task =
case update of
Focus ->
Just { task | edits <- Just task.description }
Edit description ->
Just { task | edits <- Just description }
Cancel ->
Just { task | edits <- Nothing }
Commit ->
case task.edits of
Nothing ->
Just task
Just rawDescription ->
let description = String.trim rawDescription in
if String.isEmpty description then Nothing else
Just
{ task |
edits <- Nothing,
description <- description
}
Completed bool ->
Just { task | completed <- bool }
Delete ->
Nothing
-- VIEW
view : LC.LocalChannel (Int, Action) -> Model -> Html
view channel task =
let className =
(if task.completed then "completed " else "") ++
case task.edits of
Just _ -> "editing"
Nothing -> ""
description =
Maybe.withDefault task.description task.edits
in
li
[ class className ]
[ div
[ class "view" ]
[ input
[ class "toggle"
, type' "checkbox"
, checked task.completed
, onClick (LC.send channel (task.id, Completed (not task.completed)))
]
[]
, label
[ onDoubleClick (LC.send channel (task.id, Focus)) ]
[ text description ]
, button
[ class "destroy"
, onClick (LC.send channel (task.id, Delete))
]
[]
]
, input
[ class "edit"
, value description
, name "title"
, id ("todo-" ++ toString task.id)
, on "input" targetValue (\desc -> LC.send channel (task.id, Edit desc))
, onBlur (LC.send channel (task.id, Commit))
, onFinish
(LC.send channel (task.id, Commit))
(LC.send channel (task.id, Cancel))
]
[]
]
onFinish : Signal.Message -> Signal.Message -> Attribute
onFinish enterMessage escapeMessage =
let select key =
case key of
13 -> Ok enterMessage
27 -> Ok escapeMessage
_ -> Err "Not a 'finish' key, such as ENTER or ESCAPE"
in
on "keydown" (Json.customDecoder keyCode select) identity
module Todo where
port module Todo exposing (..)
{-| TodoMVC implemented in Elm, using plain HTML and CSS for rendering.
This application is broken up into four distinct parts:
......@@ -6,40 +7,41 @@ This application is broken up into four distinct parts:
1. Model - a full description of the application as data
2. Update - a way to update the model based on user actions
3. View - a way to visualize our model with HTML
4. Inputs - the signals necessary to manage events
This clean division of concerns is a core part of Elm. You can read more about
this in the Pong tutorial: http://elm-lang.org/blog/Pong.elm
This program is not particularly large, so definitely see the following
document for notes on structuring more complex GUIs with Elm:
http://elm-lang.org/learn/Architecture.elm
http://guide.elm-lang.org/architecture/
-}
import Html (..)
import Html.Attributes (..)
import Html.Events (..)
import Html.Lazy (lazy, lazy2)
import List
import LocalChannel as LC
import Maybe
import Signal
import String
import Dom
import Task
import Window
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Html.Lazy exposing (lazy, lazy2)
import Html.App
import Navigation exposing (Parser)
import String
import String.Extra
import Todo.Task
-- MODEL
-- The full application state of our todo app.
type alias Model =
{ tasks : List Task.Model
, field : String
, uid : Int
{ tasks : List Todo.Task.Model
, field : String
, uid : Int
, visibility : String
}
type alias Flags =
Maybe Model
emptyModel : Model
emptyModel =
{ tasks = []
......@@ -49,221 +51,340 @@ emptyModel =
}
-- UPDATE
-- UPDATE
-- A description of the kinds of actions that can be performed on the model of
-- our application. See the following post for more info on this pattern and
-- some alternatives: http://elm-lang.org/learn/Architecture.elm
type Action
-- some alternatives: http://guide.elm-lang.org/architecture/
type Msg
= NoOp
| UpdateField String
| Add
| UpdateTask (Int, Task.Action)
| UpdateTask ( Int, Todo.Task.Msg )
| DeleteComplete
| CheckAll Bool
| ChangeVisibility String
-- How we update our Model on any given Action
update : Action -> Model -> Model
update action model =
case action of
NoOp -> model
UpdateField str ->
{ model | field <- str }
Add ->
let description = String.trim model.field in
if String.isEmpty description then model else
{ model |
uid <- model.uid + 1,
field <- "",
tasks <- model.tasks ++ [Task.init description model.uid]
}
UpdateTask (id, taskAction) ->
let updateTask t =
if t.id == id then Task.update taskAction t else Just t
in
{ model | tasks <- List.filterMap updateTask model.tasks }
-- How we update our Model on any given Message
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case Debug.log "MESSAGE: " msg of
NoOp ->
( model, Cmd.none )
UpdateField str ->
let
newModel =
{ model | field = str }
in
( newModel, save model )
Add ->
let
description =
String.trim model.field
newModel =
if String.isEmpty description then
model
else
{ model
| uid = model.uid + 1
, field = ""
, tasks = model.tasks ++ [ Todo.Task.init description model.uid ]
}
in
( newModel, save newModel )
UpdateTask ( id, taskMsg ) ->
let
updateTask t =
if t.id == id then
Todo.Task.update taskMsg t
else
Just t
newModel =
{ model | tasks = List.filterMap updateTask model.tasks }
in
case taskMsg of
Todo.Task.Focus elementId ->
newModel ! [ save newModel, focusTask elementId ]
_ ->
( newModel, save newModel )
DeleteComplete ->
let
newModel =
{ model
| tasks = List.filter (not << .completed) model.tasks
}
in
( newModel, save newModel )
CheckAll bool ->
let
updateTask t =
{ t | completed = bool }
newModel =
{ model | tasks = List.map updateTask model.tasks }
in
( newModel, save newModel )
ChangeVisibility visibility ->
let
newModel =
{ model | visibility = visibility }
in
( newModel, save model )
focusTask : String -> Cmd Msg
focusTask elementId =
Task.perform (\_ -> NoOp) (\_ -> NoOp) (Dom.focus elementId)
DeleteComplete ->
{ model | tasks <- List.filter (not << .completed) model.tasks }
CheckAll bool ->
let updateTask t = { t | completed <- bool }
in { model | tasks <- List.map updateTask model.tasks }
ChangeVisibility visibility ->
{ model | visibility <- visibility }
-- VIEW
view : Model -> Html
view : Model -> Html Msg
view model =
div
[ class "todomvc-wrapper"
, style [ ("visibility", "hidden") ]
]
[ section
[ id "todoapp" ]
[ lazy taskEntry model.field
, lazy2 taskList model.visibility model.tasks
, lazy2 controls model.visibility model.tasks
]
, infoFooter
]
taskEntry : String -> Html
[ class "todomvc-wrapper"
, style [ ( "visibility", "hidden" ) ]
]
[ section
[ class "todoapp" ]
[ lazy taskEntry model.field
, lazy2 taskList model.visibility model.tasks
, lazy2 controls model.visibility model.tasks
]
, infoFooter
]
taskEntry : String -> Html Msg
taskEntry task =
header
[ id "header" ]
[ h1 [] [ text "todos" ]
, input
[ id "new-todo"
, placeholder "What needs to be done?"
, autofocus True
, value task
, name "newTodo"
, on "input" targetValue (Signal.send actions << UpdateField)
, Task.onFinish (Signal.send actions Add) (Signal.send actions NoOp)
]
[]
]
taskList : String -> List Task.Model -> Html
[ class "header" ]
[ h1 [] [ text "todos" ]
, input
[ class "new-todo"
, placeholder "What needs to be done?"
, autofocus True
, value task
, name "newTodo"
, onInput UpdateField
, Todo.Task.onFinish Add NoOp
]
[]
]
taskList : String -> List Todo.Task.Model -> Html Msg
taskList visibility tasks =
let isVisible todo =
let
isVisible todo =
case visibility of
"Completed" -> todo.completed
"Active" -> not todo.completed
"All" -> True
"Completed" ->
todo.completed
"Active" ->
not todo.completed
-- "All"
_ ->
True
allCompleted = List.all .completed tasks
allCompleted =
List.all .completed tasks
cssVisibility = if List.isEmpty tasks then "hidden" else "visible"
cssVisibility =
if List.isEmpty tasks then
"hidden"
else
"visible"
in
section
[ id "main"
, style [ ("visibility", cssVisibility) ]
]
[ input
[ id "toggle-all"
, type' "checkbox"
, name "toggle"
, checked allCompleted
, onClick (Signal.send actions (CheckAll (not allCompleted)))
]
[]
, label
[ for "toggle-all" ]
[ text "Mark all as complete" ]
, ul
[ id "todo-list" ]
(List.map (Task.view taskActions) (List.filter isVisible tasks))
]
controls : String -> List Task.Model -> Html
section
[ class "main"
, style [ ( "visibility", cssVisibility ) ]
]
[ input
[ class "toggle-all"
, type' "checkbox"
, name "toggle"
, checked allCompleted
, onClick (CheckAll (not allCompleted))
]
[]
, label
[ for "toggle-all" ]
[ text "Mark all as complete" ]
, ul
[ class "todo-list" ]
(List.map
(\task ->
let
id =
task.id
taskView =
Todo.Task.view task
in
Html.App.map (\msg -> UpdateTask ( id, msg )) taskView
)
(List.filter isVisible tasks)
)
]
controls : String -> List Todo.Task.Model -> Html Msg
controls visibility tasks =
let tasksCompleted = List.length (List.filter .completed tasks)
tasksLeft = List.length tasks - tasksCompleted
item_ = if tasksLeft == 1 then " item" else " items"
let
tasksCompleted =
List.length (List.filter .completed tasks)
tasksLeft =
List.length tasks - tasksCompleted
item_ =
if tasksLeft == 1 then
" item"
else
" items"
in
footer
[ id "footer"
, hidden (List.isEmpty tasks)
]
[ span
[ id "todo-count" ]
[ strong [] [ text (toString tasksLeft) ]
, text (item_ ++ " left")
]
, ul
[ id "filters" ]
[ visibilitySwap "#/" "All" visibility
, text " "
, visibilitySwap "#/active" "Active" visibility
, text " "
, visibilitySwap "#/completed" "Completed" visibility
]
, button
[ class "clear-completed"
, id "clear-completed"
, hidden (tasksCompleted == 0)
, onClick (Signal.send actions DeleteComplete)
]
[ text ("Clear completed (" ++ toString tasksCompleted ++ ")") ]
]
visibilitySwap : String -> String -> String -> Html
footer
[ class "footer"
, hidden (List.isEmpty tasks)
]
[ span
[ class "todo-count" ]
[ strong [] [ text (toString tasksLeft) ]
, text (item_ ++ " left")
]
, ul
[ class "filters" ]
[ visibilitySwap "#/" "All" visibility
, text " "
, visibilitySwap "#/active" "Active" visibility
, text " "
, visibilitySwap "#/completed" "Completed" visibility
]
, button
[ class "clear-completed"
, hidden (tasksCompleted == 0)
, onClick DeleteComplete
]
[ text ("Clear completed (" ++ toString tasksCompleted ++ ")") ]
]
visibilitySwap : String -> String -> String -> Html Msg
visibilitySwap uri visibility actualVisibility =
let className = if visibility == actualVisibility then "selected" else "" in
li
[ onClick (Signal.send actions (ChangeVisibility visibility)) ]
[ a [ class className, href uri ] [ text visibility ] ]
let
className =
if visibility == actualVisibility then
"selected"
else
""
in
li
[ onClick (ChangeVisibility visibility) ]
[ a [ class className, href uri ] [ text visibility ] ]
infoFooter : Html
infoFooter : Html msg
infoFooter =
footer [ id "info" ]
[ p [] [ text "Double-click to edit a todo" ]
, p [] [ text "Written by "
, a [ href "https://github.com/evancz" ] [ text "Evan Czaplicki" ]
]
, p [] [ text "Part of "
, a [ href "http://todomvc.com" ] [ text "TodoMVC" ]
]
]
footer
[ class "info" ]
[ p [] [ text "Double-click to edit a todo" ]
, p []
[ text "Written by "
, a [ href "https://github.com/evancz" ] [ text "Evan Czaplicki" ]
]
, p []
[ text "Part of "
, a [ href "http://todomvc.com" ] [ text "TodoMVC" ]
]
]
-- SIGNALS
-- wire the entire application together
main : Signal Html
main : Program Flags
main =
Signal.map view model
Navigation.programWithFlags urlParser
{ urlUpdate = urlUpdate
, view = view
, init = init
, update = update
, subscriptions = subscriptions
}
-- manage the model of our application over time
model : Signal Model
model =
Signal.foldp update initialModel allActions
-- URL PARSERS - check out evancz/url-parser for fancier URL parsing
initialModel : Model
initialModel =
Maybe.withDefault emptyModel savedModel
toUrl : String -> String
toUrl visibility =
"#/" ++ String.toLower visibility
allActions : Signal Action
allActions =
Signal.merge
(Signal.subscribe actions)
(Signal.map ChangeVisibility route)
fromUrl : String -> Maybe String
fromUrl hash =
let
cleanHash =
String.dropLeft 2 hash
in
if (List.member cleanHash [ "all", "active", "completed" ]) == True then
Just cleanHash
else
Nothing
-- interactions with localStorage
port savedModel : Maybe Model
port save : Signal Model
port save = model
urlParser : Parser (Maybe String)
urlParser =
Navigation.makeParser (fromUrl << .hash)
-- routing
port route : Signal String
-- actions from user input
actions : Signal.Channel Action
{-| The URL is turned into a Maybe value. If the URL is valid, we just update
our model with the new visibility settings. If it is not a valid URL,
we set the visibility filter to show all tasks.
-}
urlUpdate : Maybe String -> Model -> ( Model, Cmd Msg )
urlUpdate result model =
case result of
Just visibility ->
update (ChangeVisibility (String.Extra.toSentenceCase visibility)) model
taskActions : LC.LocalChannel (Int, Task.Action)
Nothing ->
update (ChangeVisibility "All") model
port focus : Signal (Maybe Int)
port focus =
let toSelector action =
case action of
UpdateTask (id, Task.Focus) -> Just id
_ -> Nothing
in
init : Flags -> Maybe String -> ( Model, Cmd Msg )
init flags url =
urlUpdate url (Maybe.withDefault emptyModel flags)
-- interactions with localStorage
port save : Model -> Cmd msg
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
module Todo.Task exposing (..)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Json.Decode
import String
-- MODEL
type alias Model =
{ description : String
, completed : Bool
, edits : Maybe String
, id : Int
}
init : String -> Int -> Model
init desc id =
{ description = desc
, completed = False
, edits = Nothing
, id = id
}
-- UPDATE
type Msg
= Focus String
| Edit String
| Cancel
| Commit
| Completed Bool
| Delete
update : Msg -> Model -> Maybe Model
update msg model =
case msg of
Focus elementId ->
Just { model | edits = Just model.description }
Edit description ->
Just { model | edits = Just description }
Cancel ->
Just { model | edits = Nothing }
Commit ->
case model.edits of
Nothing ->
Just model
Just rawDescription ->
let
description =
String.trim rawDescription
in
if String.isEmpty description then
Nothing
else
Just
{ model
| edits = Nothing
, description = description
}
Completed bool ->
Just { model | completed = bool }
Delete ->
Nothing
-- VIEW
view : Model -> Html Msg
view model =
let
className =
(if model.completed then
"completed "
else
""
)
++ case model.edits of
Just _ ->
"editing"
Nothing ->
""
description =
Maybe.withDefault model.description model.edits
elementId =
"todo-" ++ toString model.id
in
li
[ class className ]
[ div
[ class "view" ]
[ input
[ class "toggle"
, type' "checkbox"
, checked model.completed
, onClick (Completed (not model.completed))
]
[]
, label
[ onDoubleClick (Focus elementId) ]
[ text description ]
, button
[ class "destroy"
, onClick Delete
]
[]
]
, input
[ class "edit"
, value description
, name "title"
, id (elementId)
, onInput Edit
, onBlur Commit
, onFinish Commit Cancel
]
[]
]
onFinish : msg -> msg -> Attribute msg
onFinish enterMessage escapeMessage =
let
select key =
case key of
13 ->
enterMessage
_ ->
-- Not a 'finish' key, such as ENTER or ESCAPE
escapeMessage
in
on "keydown" (Json.Decode.map select keyCode)
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -8,8 +8,11 @@
],
"exposed-modules": [],
"dependencies": {
"elm-lang/core": "1.0.0 <= v < 2.0.0",
"evancz/elm-html": "1.0.0 <= v < 2.0.0",
"evancz/local-channel": "1.0.0 <= v < 2.0.0"
}
}
\ No newline at end of file
"elm-community/string-extra": "1.0.2 <= v < 2.0.0",
"elm-lang/core": "4.0.5 <= v < 5.0.0",
"elm-lang/dom": "1.1.0 <= v < 2.0.0",
"elm-lang/html": "1.1.0 <= v < 2.0.0",
"elm-lang/navigation": "1.0.0 <= v < 2.0.0"
},
"elm-version": "0.17.1 <= v < 0.18.0"
}
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -16,47 +16,17 @@
</head>
<body>
<script src="elm.js"></script>
<script src="build/elm.js"></script>
<script>
(function () {
var result = localStorage.getItem('elm-todo-model');
var savedModel = result ? JSON.parse(result) : null;
var todomvc = Elm.fullscreen(Elm.Todo, {
savedModel: savedModel,
route: getRoute()
});
var todomvc = Elm.Todo.fullscreen(savedModel);
todomvc.ports.save.subscribe(function (model) {
localStorage.setItem('elm-todo-model', JSON.stringify(model));
});
// Routing
window.addEventListener('popstate', function () {
todomvc.ports.route.send(getRoute());
}, false);
function getRoute() {
var hash = location.href.split('#')[1] || '';
var route = hash.replace('/', '');
if (['all', 'active', 'completed'].indexOf(route) >= 0) {
return route[0].toUpperCase() + route.substr(1);
}
return 'All';
}
// Setting focus manually
todomvc.ports.focus.subscribe(function (id) {
setTimeout(function () {
if (id === null) {
return;
}
var node = document.getElementById('todo-' + id);
if (document.activeElement !== node) {
node.focus();
}
}, 50);
});
}());
</script>
<script async src="node_modules/todomvc-common/base.js"></script>
......
......@@ -15,12 +15,9 @@ button {
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
-ms-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
......@@ -32,22 +29,19 @@ body {
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
button,
input[type="checkbox"] {
outline: none;
:focus {
outline: 0;
}
.hidden {
display: none;
}
#todoapp {
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
......@@ -55,25 +49,25 @@ input[type="checkbox"] {
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
#todoapp input::-webkit-input-placeholder {
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
#todoapp input::-moz-placeholder {
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
#todoapp input::input-placeholder {
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
#todoapp h1 {
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
......@@ -83,11 +77,10 @@ input[type="checkbox"] {
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
#new-todo,
.new-todo,
.edit {
position: relative;
margin: 0;
......@@ -97,27 +90,23 @@ input[type="checkbox"] {
font-weight: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-ms-box-sizing: border-box;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#new-todo {
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
#main {
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
......@@ -127,7 +116,7 @@ label[for='toggle-all'] {
display: none;
}
#toggle-all {
.toggle-all {
position: absolute;
top: -55px;
left: -12px;
......@@ -137,50 +126,50 @@ label[for='toggle-all'] {
border: none; /* Mobile Safari */
}
#toggle-all:before {
.toggle-all:before {
content: '❯';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
#toggle-all:checked:before {
.toggle-all:checked:before {
color: #737373;
}
#todo-list {
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
#todo-list li {
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
#todo-list li:last-child {
.todo-list li:last-child {
border-bottom: none;
}
#todo-list li.editing {
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
#todo-list li.editing .edit {
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
padding: 12px 16px;
margin: 0 0 0 43px;
}
#todo-list li.editing .view {
.todo-list li.editing .view {
display: none;
}
#todo-list li .toggle {
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
......@@ -191,20 +180,18 @@ label[for='toggle-all'] {
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
-ms-appearance: none;
appearance: none;
}
#todo-list li .toggle:after {
.todo-list li .toggle:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
#todo-list li .toggle:checked:after {
.todo-list li .toggle:checked:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
#todo-list li label {
white-space: pre-line;
.todo-list li label {
word-break: break-all;
padding: 15px 60px 15px 15px;
margin-left: 45px;
......@@ -213,12 +200,12 @@ label[for='toggle-all'] {
transition: color 0.4s;
}
#todo-list li.completed label {
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
#todo-list li .destroy {
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
......@@ -233,27 +220,27 @@ label[for='toggle-all'] {
transition: color 0.2s ease-out;
}
#todo-list li .destroy:hover {
.todo-list li .destroy:hover {
color: #af5b5e;
}
#todo-list li .destroy:after {
.todo-list li .destroy:after {
content: '×';
}
#todo-list li:hover .destroy {
.todo-list li:hover .destroy {
display: block;
}
#todo-list li .edit {
.todo-list li .edit {
display: none;
}
#todo-list li.editing:last-child {
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
#footer {
.footer {
color: #777;
padding: 10px 15px;
height: 20px;
......@@ -261,7 +248,7 @@ label[for='toggle-all'] {
border-top: 1px solid #e6e6e6;
}
#footer:before {
.footer:before {
content: '';
position: absolute;
right: 0;
......@@ -276,16 +263,16 @@ label[for='toggle-all'] {
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
#todo-count {
.todo-count {
float: left;
text-align: left;
}
#todo-count strong {
.todo-count strong {
font-weight: 300;
}
#filters {
.filters {
margin: 0;
padding: 0;
list-style: none;
......@@ -294,11 +281,11 @@ label[for='toggle-all'] {
left: 0;
}
#filters li {
.filters li {
display: inline;
}
#filters li a {
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
......@@ -307,39 +294,28 @@ label[for='toggle-all'] {
border-radius: 3px;
}
#filters li a.selected,
#filters li a:hover {
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
#filters li a.selected {
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
#clear-completed,
html #clear-completed:active {
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
visibility: hidden;
position: relative;
}
#clear-completed::after {
visibility: visible;
content: 'Clear completed';
position: absolute;
right: 0;
white-space: nowrap;
}
#clear-completed:hover::after {
.clear-completed:hover {
text-decoration: underline;
}
#info {
.info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
......@@ -347,17 +323,17 @@ html #clear-completed:active {
text-align: center;
}
#info p {
.info p {
line-height: 1;
}
#info a {
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
#info a:hover {
.info a:hover {
text-decoration: underline;
}
......@@ -366,16 +342,16 @@ html #clear-completed:active {
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all,
#todo-list li .toggle {
.toggle-all,
.todo-list li .toggle {
background: none;
}
#todo-list li .toggle {
.todo-list li .toggle {
height: 40px;
}
#toggle-all {
.toggle-all {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
......@@ -384,11 +360,11 @@ html #clear-completed:active {
}
@media (max-width: 430px) {
#footer {
.footer {
height: 50px;
}
#filters {
.filters {
bottom: 10px;
}
}
......@@ -114,7 +114,12 @@
})({});
if (location.hostname === 'todomvc.com') {
window._gaq = [['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script'));
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-31081062-1', 'auto');
ga('send', 'pageview');
}
/* jshint ignore:end */
......@@ -228,7 +233,7 @@
xhr.onload = function (e) {
var parsedResponse = JSON.parse(e.target.responseText);
if (parsedResponse instanceof Array) {
var count = parsedResponse.length
var count = parsedResponse.length;
if (count !== 0) {
issueLink.innerHTML = 'This app has ' + count + ' open issues';
document.getElementById('issue-count').style.display = 'inline';
......
{
"private": true,
"dependencies": {
"todomvc-app-css": "^1.0.0",
"todomvc-common": "^1.0.1"
"todomvc-app-css": "^2.0.6",
"todomvc-common": "^1.0.2"
}
}
# Elm TodoMVC Example
> A functional reactive language for interactive applications
> A functional language for interactive applications
> _[Elm](http://elm-lang.org/)_
......@@ -14,7 +14,7 @@ Here are some links you may find helpful:
* [Try Elm](http://elm-lang.org/try)
* [Learn Elm](http://elm-lang.org/Learn.elm)
* [Elm Snippets](http://www.share-elm.com/)
* [An Introduction to Elm](http://guide.elm-lang.org/)
Get help from other Elm users:
......@@ -28,25 +28,25 @@ _If you have other helpful links to share, or find any of the links above no lon
## Project Structure
All of the Elm code lives in `Todo.elm` and `Task.elm` and relies
on the [elm-html][] library.
All of the Elm code lives in `Todo.elm` and `Todo/Task.elm` and relies
on the [elm-html][] and [elm-navigation][] packages.
[elm-html]: http://library.elm-lang.org/catalog/evancz-elm-html/latest
[elm-html]: http://package.elm-lang.org/packages/elm-lang/html/latest/
[elm-navigation]: http://package.elm-lang.org/packages/elm-lang/navigation/latest/
There also is a port handler set up in `index.html` to set the focus on
particular text fields when necessary.
## Build Instructions
You need to install
[elm](https://github.com/elm-lang/elm-platform/blob/master/README.md#elm-platform)
You need to install [elm](http://elm-lang.org/install)
on your machine first.
Run the following commands from the root of this project:
```bash
elm-package install
elm-make Todo.elm --output build/Todo.js
elm-package install -y
elm-make Todo.elm --output build/elm.js
```
Then open `index.html` in your browser!
......
# Vanilla ES6 (ES2015) • [TodoMVC](http://todomvc.com)
> An exact port of the [Vanilla JS Example](http://todomvc.com/examples/vanillajs/), but translated into ES6, also known as ES2015.
> A port of the [Vanilla JS Example](http://todomvc.com/examples/vanillajs/), but translated into ES6, also known as ES2015.
## Learning ES6
......@@ -34,9 +34,10 @@ npm run compile
## Implementation
Uses [Babel JS](https://babeljs.io/) to compile ES6 code to ES5, which is then readable by all browsers.
Uses [Google Closure Compiler](https://developers.google.com/closure/compiler/) to compile ES6 code to ES5, which is then readable by all browsers.
## Credit
Created by [Luke Edwards](http://www.lukeed.com)
Refactored by [Aaron Muir Hamilton](https://github.com/xorgy)
'use strict';
var _controller = require('./controller');
var _controller2 = _interopRequireDefault(_controller);
var _helpers = require('./helpers');
var helpers = _interopRequireWildcard(_helpers);
var _template = require('./template');
var _template2 = _interopRequireDefault(_template);
var _store = require('./store');
var _store2 = _interopRequireDefault(_store);
var _model = require('./model');
var _model2 = _interopRequireDefault(_model);
var _view = require('./view');
var _view2 = _interopRequireDefault(_view);
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var $on = helpers.$on;
var setView = function setView() {
return todo.controller.setView(document.location.hash);
};
var Todo =
/**
* Init new Todo List
* @param {string} The name of your list
*/
function Todo(name) {
_classCallCheck(this, Todo);
this.storage = new _store2.default(name);
this.model = new _model2.default(this.storage);
this.template = new _template2.default();
this.view = new _view2.default(this.template);
this.controller = new _controller2.default(this.model, this.view);
};
var todo = new Todo('todos-vanillajs');
$on(window, 'load', setView);
$on(window, 'hashchange', setView);
\ No newline at end of file
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
'use strict';
var _controller = require('./controller');
var _controller2 = _interopRequireDefault(_controller);
var _helpers = require('./helpers');
var helpers = _interopRequireWildcard(_helpers);
var _template = require('./template');
var _template2 = _interopRequireDefault(_template);
var _store = require('./store');
var _store2 = _interopRequireDefault(_store);
var _model = require('./model');
var _model2 = _interopRequireDefault(_model);
var _view = require('./view');
var _view2 = _interopRequireDefault(_view);
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var $on = helpers.$on;
var setView = function setView() {
return todo.controller.setView(document.location.hash);
};
var Todo =
/**
* Init new Todo List
* @param {string} The name of your list
*/
function Todo(name) {
_classCallCheck(this, Todo);
this.storage = new _store2.default(name);
this.model = new _model2.default(this.storage);
this.template = new _template2.default();
this.view = new _view2.default(this.template);
this.controller = new _controller2.default(this.model, this.view);
};
var todo = new Todo('todos-vanillajs');
$on(window, 'load', setView);
$on(window, 'hashchange', setView);
},{"./controller":2,"./helpers":3,"./model":4,"./store":5,"./template":6,"./view":7}],2:[function(require,module,exports){
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Controller = function () {
/**
* Take a model & view, then act as controller between them
* @param {object} model The model instance
* @param {object} view The view instance
*/
function Controller(model, view) {
var _this = this;
_classCallCheck(this, Controller);
this.model = model;
this.view = view;
this.view.bind('newTodo', function (title) {
return _this.addItem(title);
});
this.view.bind('itemEdit', function (item) {
return _this.editItem(item.id);
});
this.view.bind('itemEditDone', function (item) {
return _this.editItemSave(item.id, item.title);
});
this.view.bind('itemEditCancel', function (item) {
return _this.editItemCancel(item.id);
});
this.view.bind('itemRemove', function (item) {
return _this.removeItem(item.id);
});
this.view.bind('itemToggle', function (item) {
return _this.toggleComplete(item.id, item.completed);
});
this.view.bind('removeCompleted', function () {
return _this.removeCompletedItems();
});
this.view.bind('toggleAll', function (status) {
return _this.toggleAll(status.completed);
});
}
/**
* Load & Initialize the view
* @param {string} '' | 'active' | 'completed'
*/
_createClass(Controller, [{
key: 'setView',
value: function setView(hash) {
var route = hash.split('/')[1];
var page = route || '';
this._updateFilter(page);
}
/**
* Event fires on load. Gets all items & displays them
*/
}, {
key: 'showAll',
value: function showAll() {
var _this2 = this;
this.model.read(function (data) {
return _this2.view.render('showEntries', data);
});
}
/**
* Renders all active tasks
*/
}, {
key: 'showActive',
value: function showActive() {
var _this3 = this;
this.model.read({ completed: false }, function (data) {
return _this3.view.render('showEntries', data);
});
}
/**
* Renders all completed tasks
*/
}, {
key: 'showCompleted',
value: function showCompleted() {
var _this4 = this;
this.model.read({ completed: true }, function (data) {
return _this4.view.render('showEntries', data);
});
}
/**
* An event to fire whenever you want to add an item. Simply pass in the event
* object and it'll handle the DOM insertion and saving of the new item.
*/
}, {
key: 'addItem',
value: function addItem(title) {
var _this5 = this;
if (title.trim() === '') {
return;
}
this.model.create(title, function () {
_this5.view.render('clearNewTodo');
_this5._filter(true);
});
}
/*
* Triggers the item editing mode.
*/
}, {
key: 'editItem',
value: function editItem(id) {
var _this6 = this;
this.model.read(id, function (data) {
var title = data[0].title;
_this6.view.render('editItem', { id: id, title: title });
});
}
/*
* Finishes the item editing mode successfully.
*/
}, {
key: 'editItemSave',
value: function editItemSave(id, title) {
var _this7 = this;
title = title.trim();
if (title.length !== 0) {
this.model.update(id, { title: title }, function () {
_this7.view.render('editItemDone', { id: id, title: title });
});
} else {
this.removeItem(id);
}
}
/*
* Cancels the item editing mode.
*/
}, {
key: 'editItemCancel',
value: function editItemCancel(id) {
var _this8 = this;
this.model.read(id, function (data) {
var title = data[0].title;
_this8.view.render('editItemDone', { id: id, title: title });
});
}
/**
* Find the DOM element with given ID,
* Then remove it from DOM & Storage
*/
}, {
key: 'removeItem',
value: function removeItem(id) {
var _this9 = this;
this.model.remove(id, function () {
return _this9.view.render('removeItem', id);
});
this._filter();
}
/**
* Will remove all completed items from the DOM and storage.
*/
}, {
key: 'removeCompletedItems',
value: function removeCompletedItems() {
var _this10 = this;
this.model.read({ completed: true }, function (data) {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = data[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var item = _step.value;
_this10.removeItem(item.id);
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
});
this._filter();
}
/**
* Give it an ID of a model and a checkbox and it will update the item
* in storage based on the checkbox's state.
*
* @param {number} id The ID of the element to complete or uncomplete
* @param {object} checkbox The checkbox to check the state of complete
* or not
* @param {boolean|undefined} silent Prevent re-filtering the todo items
*/
}, {
key: 'toggleComplete',
value: function toggleComplete(id, completed, silent) {
var _this11 = this;
this.model.update(id, { completed: completed }, function () {
_this11.view.render('elementComplete', { id: id, completed: completed });
});
if (!silent) {
this._filter();
}
}
/**
* Will toggle ALL checkboxes' on/off state and completeness of models.
* Just pass in the event object.
*/
}, {
key: 'toggleAll',
value: function toggleAll(completed) {
var _this12 = this;
this.model.read({ completed: !completed }, function (data) {
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = data[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var item = _step2.value;
_this12.toggleComplete(item.id, completed, true);
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
});
this._filter();
}
/**
* Updates the pieces of the page which change depending on the remaining
* number of todos.
*/
}, {
key: '_updateCount',
value: function _updateCount() {
var _this13 = this;
this.model.getCount(function (todos) {
var completed = todos.completed;
var visible = completed > 0;
var checked = completed === todos.total;
_this13.view.render('updateElementCount', todos.active);
_this13.view.render('clearCompletedButton', { completed: completed, visible: visible });
_this13.view.render('toggleAll', { checked: checked });
_this13.view.render('contentBlockVisibility', { visible: todos.total > 0 });
});
}
/**
* Re-filters the todo items, based on the active route.
* @param {boolean|undefined} force forces a re-painting of todo items.
*/
}, {
key: '_filter',
value: function _filter(force) {
var active = this._activeRoute;
var activeRoute = active.charAt(0).toUpperCase() + active.substr(1);
// Update the elements on the page, which change with each completed todo
this._updateCount();
// If the last active route isn't "All", or we're switching routes, we
// re-create the todo item elements, calling:
// this.show[All|Active|Completed]()
if (force || this._lastActiveRoute !== 'All' || this._lastActiveRoute !== activeRoute) {
this['show' + activeRoute]();
}
this._lastActiveRoute = activeRoute;
}
/**
* Simply updates the filter nav's selected states
*/
}, {
key: '_updateFilter',
value: function _updateFilter(currentPage) {
// Store a reference to the active route, allowing us to re-filter todo
// items as they are marked complete or incomplete.
this._activeRoute = currentPage;
if (currentPage === '') {
this._activeRoute = 'All';
}
this._filter();
this.view.render('setFilter', currentPage);
}
}]);
return Controller;
}();
exports.default = Controller;
},{}],3:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.qs = qs;
exports.qsa = qsa;
exports.$on = $on;
exports.$delegate = $delegate;
exports.$parent = $parent;
// Allow for looping on nodes by chaining:
// qsa('.foo').forEach(function () {})
NodeList.prototype.forEach = Array.prototype.forEach;
// Get element(s) by CSS selector:
function qs(selector, scope) {
return (scope || document).querySelector(selector);
}
function qsa(selector, scope) {
return (scope || document).querySelectorAll(selector);
}
// addEventListener wrapper:
function $on(target, type, callback, useCapture) {
target.addEventListener(type, callback, !!useCapture);
}
// Attach a handler to event for all elements that match the selector,
// now or in the future, based on a root element
function $delegate(target, selector, type, handler) {
var dispatchEvent = function dispatchEvent(event) {
var targetElement = event.target;
var potentialElements = qsa(selector, target);
var hasMatch = Array.from(potentialElements).includes(targetElement);
if (hasMatch) {
handler.call(targetElement, event);
}
};
// https://developer.mozilla.org/en-US/docs/Web/Events/blur
var useCapture = type === 'blur' || type === 'focus';
$on(target, type, dispatchEvent, useCapture);
}
// Find the element's parent with the given tag name:
// $parent(qs('a'), 'div')
function $parent(element, tagName) {
if (!element.parentNode) {
return;
}
if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) {
return element.parentNode;
}
return $parent(element.parentNode, tagName);
}
},{}],4:[function(require,module,exports){
'use strict';
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/**
* Creates a new Model instance and hooks up the storage.
* @constructor
* @param {object} storage A reference to the client side storage class
*/
var Model = function () {
function Model(storage) {
_classCallCheck(this, Model);
this.storage = storage;
}
/**
* Creates a new todo model
*
* @param {string} [title] The title of the task
* @param {function} [callback] The callback to fire after the model is created
*/
_createClass(Model, [{
key: 'create',
value: function create(title, callback) {
title = title || '';
var newItem = {
title: title.trim(),
completed: false
};
this.storage.save(newItem, callback);
}
/**
* Finds and returns a model in storage. If no query is given it'll simply
* return everything. If you pass in a string or number it'll look that up as
* the ID of the model to find. Lastly, you can pass it an object to match
* against.
*
* @param {string|number|object} [query] A query to match models against
* @param {function} [callback] The callback to fire after the model is found
*
* @example
* model.read(1, func) // Will find the model with an ID of 1
* model.read('1') // Same as above
* //Below will find a model with foo equalling bar and hello equalling world.
* model.read({ foo: 'bar', hello: 'world' })
*/
}, {
key: 'read',
value: function read(query, callback) {
var queryType = typeof query === 'undefined' ? 'undefined' : _typeof(query);
if (queryType === 'function') {
this.storage.findAll(query);
} else if (queryType === 'string' || queryType === 'number') {
query = parseInt(query, 10);
this.storage.find({ id: query }, callback);
} else {
this.storage.find(query, callback);
}
}
/**
* Updates a model by giving it an ID, data to update, and a callback to fire when
* the update is complete.
*
* @param {number} id The id of the model to update
* @param {object} data The properties to update and their new value
* @param {function} callback The callback to fire when the update is complete.
*/
}, {
key: 'update',
value: function update(id, data, callback) {
this.storage.save(data, callback, id);
}
/**
* Removes a model from storage
*
* @param {number} id The ID of the model to remove
* @param {function} callback The callback to fire when the removal is complete.
*/
}, {
key: 'remove',
value: function remove(id, callback) {
this.storage.remove(id, callback);
}
/**
* WARNING: Will remove ALL data from storage.
*
* @param {function} callback The callback to fire when the storage is wiped.
*/
}, {
key: 'removeAll',
value: function removeAll(callback) {
this.storage.drop(callback);
}
/**
* Returns a count of all todos
*/
}, {
key: 'getCount',
value: function getCount(callback) {
var todos = {
active: 0,
completed: 0,
total: 0
};
this.storage.findAll(function (data) {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = data[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var todo = _step.value;
if (todo.completed) {
todos.completed++;
} else {
todos.active++;
}
todos.total++;
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
callback(todos);
});
}
}]);
return Model;
}();
exports.default = Model;
},{}],5:[function(require,module,exports){
"use strict";
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/*jshint eqeqeq:false */
/**
* Creates a new client side storage object and will create an empty
* collection if no collection already exists.
*
* @param {string} name The name of our DB we want to use
* @param {function} callback Our fake DB uses callbacks because in
* real life you probably would be making AJAX calls
*/
var Store = function () {
function Store(name, callback) {
_classCallCheck(this, Store);
this._dbName = name;
if (!localStorage[name]) {
var data = {
todos: []
};
localStorage[name] = JSON.stringify(data);
}
if (callback) {
callback.call(this, JSON.parse(localStorage[name]));
}
}
/**
* Finds items based on a query given as a JS object
*
* @param {object} query The query to match against (i.e. {foo: 'bar'})
* @param {function} callback The callback to fire when the query has
* completed running
*
* @example
* db.find({foo: 'bar', hello: 'world'}, function (data) {
* // data will return any items that have foo: bar and
* // hello: world in their properties
* })
*/
_createClass(Store, [{
key: "find",
value: function find(query, callback) {
var todos = JSON.parse(localStorage[this._dbName]).todos;
callback.call(this, todos.filter(function (todo) {
for (var q in query) {
if (query[q] !== todo[q]) {
return false;
}
}
return true;
}));
}
/**
* Will retrieve all data from the collection
*
* @param {function} callback The callback to fire upon retrieving data
*/
}, {
key: "findAll",
value: function findAll(callback) {
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
}
/**
* Will save the given data to the DB. If no item exists it will create a new
* item, otherwise it'll simply update an existing item's properties
*
* @param {object} updateData The data to save back into the DB
* @param {function} callback The callback to fire after saving
* @param {number} id An optional param to enter an ID of an item to update
*/
}, {
key: "save",
value: function save(updateData, callback, id) {
var data = JSON.parse(localStorage[this._dbName]);
var todos = data.todos;
var len = todos.length;
// If an ID was actually given, find the item and update each property
if (id) {
for (var i = 0; i < len; i++) {
if (todos[i].id === id) {
for (var key in updateData) {
todos[i][key] = updateData[key];
}
break;
}
}
localStorage[this._dbName] = JSON.stringify(data);
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
} else {
// Generate an ID
updateData.id = new Date().getTime();
todos.push(updateData);
localStorage[this._dbName] = JSON.stringify(data);
if (callback) {
callback.call(this, [updateData]);
}
}
}
/**
* Will remove an item from the Store based on its ID
*
* @param {number} id The ID of the item you want to remove
* @param {function} callback The callback to fire after saving
*/
}, {
key: "remove",
value: function remove(id, callback) {
var data = JSON.parse(localStorage[this._dbName]);
var todos = data.todos;
var len = todos.length;
for (var i = 0; i < todos.length; i++) {
if (todos[i].id == id) {
todos.splice(i, 1);
break;
}
}
localStorage[this._dbName] = JSON.stringify(data);
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
}
/**
* Will drop all storage and start fresh
*
* @param {function} callback The callback to fire after dropping the data
*/
}, {
key: "drop",
value: function drop(callback) {
localStorage[this._dbName] = JSON.stringify({ todos: [] });
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
}
}]);
return Store;
}();
exports.default = Store;
},{}],6:[function(require,module,exports){
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var htmlEscapes = {
'&': '&amp',
'<': '&lt',
'>': '&gt',
'"': '&quot',
'\'': '&#x27',
'`': '&#x60'
};
var reUnescapedHtml = /[&<>"'`]/g;
var reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
var escape = function escape(str) {
return str && reHasUnescapedHtml.test(str) ? str.replace(reUnescapedHtml, escapeHtmlChar) : str;
};
var escapeHtmlChar = function escapeHtmlChar(chr) {
return htmlEscapes[chr];
};
var Template = function () {
function Template() {
_classCallCheck(this, Template);
this.defaultTemplate = '\n\t\t\t<li data-id="{{id}}" class="{{completed}}">\n\t\t\t\t<div class="view">\n\t\t\t\t\t<input class="toggle" type="checkbox" {{checked}}>\n\t\t\t\t\t<label>{{title}}</label>\n\t\t\t\t\t<button class="destroy"></button>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t';
}
/**
* Creates an <li> HTML string and returns it for placement in your app.
*
* NOTE: In real life you should be using a templating engine such as Mustache
* or Handlebars, however, this is a vanilla JS example.
*
* @param {object} data The object containing keys you want to find in the
* template to replace.
* @returns {string} HTML String of an <li> element
*
* @example
* view.show({
* id: 1,
* title: "Hello World",
* completed: 0,
* })
*/
_createClass(Template, [{
key: 'show',
value: function show(data) {
var _this = this;
var view = data.map(function (d) {
var template = _this.defaultTemplate;
var completed = d.completed ? 'completed' : '';
var checked = d.completed ? 'checked' : '';
return _this.defaultTemplate.replace('{{id}}', d.id).replace('{{title}}', escape(d.title)).replace('{{completed}}', completed).replace('{{checked}}', checked);
});
return view.join('');
}
/**
* Displays a counter of how many to dos are left to complete
*
* @param {number} activeTodos The number of active todos.
* @returns {string} String containing the count
*/
}, {
key: 'itemCounter',
value: function itemCounter(activeTodos) {
var plural = activeTodos === 1 ? '' : 's';
return '<strong>' + activeTodos + '</strong> item' + plural + ' left';
}
/**
* Updates the text within the "Clear completed" button
*
* @param {[type]} completedTodos The number of completed todos.
* @returns {string} String containing the count
*/
}, {
key: 'clearCompletedButton',
value: function clearCompletedButton(completedTodos) {
return completedTodos > 0 ? 'Clear completed' : '';
}
}]);
return Template;
}();
exports.default = Template;
},{}],7:[function(require,module,exports){
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
var _helpers = require('./helpers');
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var _itemId = function _itemId(element) {
return parseInt((0, _helpers.$parent)(element, 'li').dataset.id, 10);
};
var _setFilter = function _setFilter(currentPage) {
(0, _helpers.qs)('.filters .selected').className = '';
(0, _helpers.qs)('.filters [href="#/' + currentPage + '"]').className = 'selected';
};
var _elementComplete = function _elementComplete(id, completed) {
var listItem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (!listItem) {
return;
}
listItem.className = completed ? 'completed' : '';
// In case it was toggled from an event and not by clicking the checkbox
(0, _helpers.qs)('input', listItem).checked = completed;
};
var _editItem = function _editItem(id, title) {
var listItem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (!listItem) {
return;
}
listItem.className += ' editing';
var input = document.createElement('input');
input.className = 'edit';
listItem.appendChild(input);
input.focus();
input.value = title;
};
/**
* View that abstracts away the browser's DOM completely.
* It has two simple entry points:
*
* - bind(eventName, handler)
* Takes a todo application event and registers the handler
* - render(command, parameterObject)
* Renders the given command with the options
*/
var View = function () {
function View(template) {
var _this = this;
_classCallCheck(this, View);
this.template = template;
this.ENTER_KEY = 13;
this.ESCAPE_KEY = 27;
this.$todoList = (0, _helpers.qs)('.todo-list');
this.$todoItemCounter = (0, _helpers.qs)('.todo-count');
this.$clearCompleted = (0, _helpers.qs)('.clear-completed');
this.$main = (0, _helpers.qs)('.main');
this.$footer = (0, _helpers.qs)('.footer');
this.$toggleAll = (0, _helpers.qs)('.toggle-all');
this.$newTodo = (0, _helpers.qs)('.new-todo');
this.viewCommands = {
showEntries: function showEntries(parameter) {
return _this.$todoList.innerHTML = _this.template.show(parameter);
},
removeItem: function removeItem(parameter) {
return _this._removeItem(parameter);
},
updateElementCount: function updateElementCount(parameter) {
return _this.$todoItemCounter.innerHTML = _this.template.itemCounter(parameter);
},
clearCompletedButton: function clearCompletedButton(parameter) {
return _this._clearCompletedButton(parameter.completed, parameter.visible);
},
contentBlockVisibility: function contentBlockVisibility(parameter) {
return _this.$main.style.display = _this.$footer.style.display = parameter.visible ? 'block' : 'none';
},
toggleAll: function toggleAll(parameter) {
return _this.$toggleAll.checked = parameter.checked;
},
setFilter: function setFilter(parameter) {
return _setFilter(parameter);
},
clearNewTodo: function clearNewTodo(parameter) {
return _this.$newTodo.value = '';
},
elementComplete: function elementComplete(parameter) {
return _elementComplete(parameter.id, parameter.completed);
},
editItem: function editItem(parameter) {
return _editItem(parameter.id, parameter.title);
},
editItemDone: function editItemDone(parameter) {
return _this._editItemDone(parameter.id, parameter.title);
}
};
}
_createClass(View, [{
key: '_removeItem',
value: function _removeItem(id) {
var elem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (elem) {
this.$todoList.removeChild(elem);
}
}
}, {
key: '_clearCompletedButton',
value: function _clearCompletedButton(completedCount, visible) {
this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount);
this.$clearCompleted.style.display = visible ? 'block' : 'none';
}
}, {
key: '_editItemDone',
value: function _editItemDone(id, title) {
var listItem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (!listItem) {
return;
}
var input = (0, _helpers.qs)('input.edit', listItem);
listItem.removeChild(input);
listItem.className = listItem.className.replace(' editing', '');
(0, _helpers.qsa)('label', listItem).forEach(function (label) {
return label.textContent = title;
});
}
}, {
key: 'render',
value: function render(viewCmd, parameter) {
this.viewCommands[viewCmd](parameter);
}
}, {
key: '_bindItemEditDone',
value: function _bindItemEditDone(handler) {
var self = this;
(0, _helpers.$delegate)(self.$todoList, 'li .edit', 'blur', function () {
if (!this.dataset.iscanceled) {
handler({
id: _itemId(this),
title: this.value
});
}
});
// Remove the cursor from the input when you hit enter just like if it were a real form
(0, _helpers.$delegate)(self.$todoList, 'li .edit', 'keypress', function (event) {
if (event.keyCode === self.ENTER_KEY) {
this.blur();
}
});
}
}, {
key: '_bindItemEditCancel',
value: function _bindItemEditCancel(handler) {
var self = this;
(0, _helpers.$delegate)(self.$todoList, 'li .edit', 'keyup', function (event) {
if (event.keyCode === self.ESCAPE_KEY) {
var id = _itemId(this);
this.dataset.iscanceled = true;
this.blur();
handler({ id: id });
}
});
}
}, {
key: 'bind',
value: function bind(event, handler) {
var _this2 = this;
switch (event) {
case 'newTodo':
(0, _helpers.$on)(this.$newTodo, 'change', function () {
return handler(_this2.$newTodo.value);
});
break;
case 'removeCompleted':
(0, _helpers.$on)(this.$clearCompleted, 'click', handler);
break;
case 'toggleAll':
(0, _helpers.$on)(this.$toggleAll, 'click', function () {
handler({ completed: this.checked });
});
break;
case 'itemEdit':
(0, _helpers.$delegate)(this.$todoList, 'li label', 'dblclick', function () {
handler({ id: _itemId(this) });
});
break;
case 'itemRemove':
(0, _helpers.$delegate)(this.$todoList, '.destroy', 'click', function () {
handler({ id: _itemId(this) });
});
break;
case 'itemToggle':
(0, _helpers.$delegate)(this.$todoList, '.toggle', 'click', function () {
handler({
id: _itemId(this),
completed: this.checked
});
});
break;
case 'itemEditDone':
this._bindItemEditDone(handler);
break;
case 'itemEditCancel':
this._bindItemEditCancel(handler);
break;
}
}
}]);
return View;
}();
exports.default = View;
},{"./helpers":3}]},{},[1]);
var e,h="undefined"!=typeof window&&window===this?this:"undefined"!=typeof global?global:this;function k(){h.Symbol||(h.Symbol=l);k=function(){}}var m=0;function l(a){return"jscomp_symbol_"+a+m++}function n(){k();h.Symbol.iterator||(h.Symbol.iterator=h.Symbol("iterator"));n=function(){}}function q(a){n();if(a[h.Symbol.iterator])return a[h.Symbol.iterator]();var b=0;return{next:function(){return b==a.length?{done:!0}:{done:!1,value:a[b++]}}}}function r(a,b){return(b||document).querySelector(a)}
function t(a,b,c,d){a.addEventListener(b,c,!!d)}function w(a,b,c,d,f){t(a,c,function(c){for(var f=c.target,u=a.querySelectorAll(b),v=u.length;v--;)if(u[v]===f){d.call(f,c);break}},!!f)}function x(a){return a.replace(/[&<]/g,function(a){return"&"===a?"&amp;":"&lt;"})};var y={};function z(a){return a.reduce(function(a,c){return a+('\n<li data-id="'+c.id+'"'+(c.c?' class="completed"':"")+'>\n\t<input class="toggle" type="checkbox" '+(c.c?"checked":"")+">\n\t<label>"+x(c.title)+'</label>\n\t<button class="destroy"></button>\n</li>')},"")};function A(a){return parseInt(a.parentNode.dataset.id,10)}function B(){this.a=r(".todo-list");this.s=r(".todo-count");this.b=r(".clear-completed");this.o=r(".main");this.h=r(".toggle-all");this.g=r(".new-todo");w(this.a,"li label","dblclick",function(a){a=a.target;var b=a.parentElement;b.classList.add("editing");var c=document.createElement("input");c.className="edit";c.value=a.innerText;b.appendChild(c);c.focus()})}B.prototype.u=function(a){this.a.innerHTML=z(a)};
function C(a,b){var c=r('[data-id="'+a+'"]');c.removeChild(r("input.edit",c));c.classList.remove("editing");r("label",c).textContent=b}function D(a,b){t(a.g,"change",function(a){(a=a.target.value.trim())&&b(a)})}function E(a,b){t(a.h,"click",function(a){b(a.target.checked)})}function F(a,b){w(a.a,".destroy","click",function(a){b(A(a.target))})}function G(a,b){w(a.a,".toggle","click",function(a){a=a.target;b(A(a),a.checked)})}
function H(a,b){w(a.a,"li .edit","blur",function(a){a=a.target;a.dataset.v||b(A(a),a.value.trim())},!0);w(a.a,"li .edit","keypress",function(a){var b=a.target;13===a.keyCode&&b.blur()})}function I(a,b){w(a.a,"li .edit","keyup",function(a){var d=a.target;27===a.keyCode&&(d.dataset.v=!0,d.blur(),b(A(d)))})};function J(a,b){var c=window.localStorage,d;this.a=function(){return d||JSON.parse(c.getItem(a)||"[]")};this.b=function(b){c.setItem(a,JSON.stringify(d=b))};b&&b()}J.prototype.find=function(a,b){var c=this.a(),d;b(c.filter(function(b){for(d in a)if(a[d]!==b[d])return!1;return!0}))};function K(a,b,c){for(var d=b.id,f=a.a(),g=f.length,p;g--;)if(f[g].id===d){for(p in b)f[g][p]=b[p];break}a.b(f);c&&c()}function L(a,b,c){var d=a.a();d.push(b);a.b(d);c&&c()}
J.prototype.remove=function(a,b){var c,d=this.a().filter(function(b){for(c in a)if(a[c]!==b[c])return!0;return!1});this.b(d);b&&b(d)};function M(a,b){a.find(y,function(a){for(var d=a.length,f=d,g=0;f--;)g+=a[f].c;b(d,d-g,g)})};function N(a,b){var c=this;this.a=a;this.b=b;D(b,this.j.bind(this));H(b,this.m.bind(this));I(b,this.l.bind(this));F(b,this.i.bind(this));G(b,function(a,b){O(c,a,b);c.f()});t(b.b,"click",this.w.bind(this));E(b,this.A.bind(this));this.h="";this.g=null}e=N.prototype;e.j=function(a){var b=this;L(this.a,{id:Date.now(),title:a,c:!1},function(){b.b.g.value="";b.f(!0)})};e.m=function(a,b){b.length?K(this.a,{id:a,title:b},function(){C(a,b)}):this.i(a)};e.l=function(a){this.a.find({id:a},function(b){C(a,b[0].title)})};
e.i=function(a){var b=this;this.a.remove({id:a},function(){b.f();var c=r('[data-id="'+a+'"]');c&&b.b.a.removeChild(c)})};e.w=function(){this.a.remove({c:!0},this.f.bind(this))};function O(a,b,c){K(a.a,{id:b,c:c},function(){var a=r('[data-id="'+b+'"]');a&&(a.className=c?"completed":"",r("input",a).checked=c)})}e.A=function(a){var b=this;this.a.find({c:!a},function(c){c=q(c);for(var d=c.next();!d.done;d=c.next())O(b,d.value.id,a)});this.f()};
e.f=function(a){var b=this,c=this.h;(a||""!==this.g||this.g!==c)&&this.a.find({"":y,active:{c:!1},completed:{c:!0}}[c],this.b.u.bind(this.b));M(this.a,function(a,c,g){b.b.s.innerHTML=c+" item"+(1!==c?"s":"")+" left";b.b.b.style.display=g?"block":"none";b.b.h.checked=g===a;b.b.o.style.display=a?"block":"none"});this.g=c};var P=new J("todos-vanilla-es6"),Q=new B,R=new N(P,Q);function S(){var a=document.location.hash.replace(/^#\//,"");R.h=a;R.f();r(".filters>.selected").className="";r('.filters>[href="#/'+a+'"]').className="selected"}t(window,"load",S);t(window,"hashchange",S);
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Controller = function () {
/**
* Take a model & view, then act as controller between them
* @param {object} model The model instance
* @param {object} view The view instance
*/
function Controller(model, view) {
var _this = this;
_classCallCheck(this, Controller);
this.model = model;
this.view = view;
this.view.bind('newTodo', function (title) {
return _this.addItem(title);
});
this.view.bind('itemEdit', function (item) {
return _this.editItem(item.id);
});
this.view.bind('itemEditDone', function (item) {
return _this.editItemSave(item.id, item.title);
});
this.view.bind('itemEditCancel', function (item) {
return _this.editItemCancel(item.id);
});
this.view.bind('itemRemove', function (item) {
return _this.removeItem(item.id);
});
this.view.bind('itemToggle', function (item) {
return _this.toggleComplete(item.id, item.completed);
});
this.view.bind('removeCompleted', function () {
return _this.removeCompletedItems();
});
this.view.bind('toggleAll', function (status) {
return _this.toggleAll(status.completed);
});
}
/**
* Load & Initialize the view
* @param {string} '' | 'active' | 'completed'
*/
_createClass(Controller, [{
key: 'setView',
value: function setView(hash) {
var route = hash.split('/')[1];
var page = route || '';
this._updateFilter(page);
}
/**
* Event fires on load. Gets all items & displays them
*/
}, {
key: 'showAll',
value: function showAll() {
var _this2 = this;
this.model.read(function (data) {
return _this2.view.render('showEntries', data);
});
}
/**
* Renders all active tasks
*/
}, {
key: 'showActive',
value: function showActive() {
var _this3 = this;
this.model.read({ completed: false }, function (data) {
return _this3.view.render('showEntries', data);
});
}
/**
* Renders all completed tasks
*/
}, {
key: 'showCompleted',
value: function showCompleted() {
var _this4 = this;
this.model.read({ completed: true }, function (data) {
return _this4.view.render('showEntries', data);
});
}
/**
* An event to fire whenever you want to add an item. Simply pass in the event
* object and it'll handle the DOM insertion and saving of the new item.
*/
}, {
key: 'addItem',
value: function addItem(title) {
var _this5 = this;
if (title.trim() === '') {
return;
}
this.model.create(title, function () {
_this5.view.render('clearNewTodo');
_this5._filter(true);
});
}
/*
* Triggers the item editing mode.
*/
}, {
key: 'editItem',
value: function editItem(id) {
var _this6 = this;
this.model.read(id, function (data) {
var title = data[0].title;
_this6.view.render('editItem', { id: id, title: title });
});
}
/*
* Finishes the item editing mode successfully.
*/
}, {
key: 'editItemSave',
value: function editItemSave(id, title) {
var _this7 = this;
title = title.trim();
if (title.length !== 0) {
this.model.update(id, { title: title }, function () {
_this7.view.render('editItemDone', { id: id, title: title });
});
} else {
this.removeItem(id);
}
}
/*
* Cancels the item editing mode.
*/
}, {
key: 'editItemCancel',
value: function editItemCancel(id) {
var _this8 = this;
this.model.read(id, function (data) {
var title = data[0].title;
_this8.view.render('editItemDone', { id: id, title: title });
});
}
/**
* Find the DOM element with given ID,
* Then remove it from DOM & Storage
*/
}, {
key: 'removeItem',
value: function removeItem(id) {
var _this9 = this;
this.model.remove(id, function () {
return _this9.view.render('removeItem', id);
});
this._filter();
}
/**
* Will remove all completed items from the DOM and storage.
*/
}, {
key: 'removeCompletedItems',
value: function removeCompletedItems() {
var _this10 = this;
this.model.read({ completed: true }, function (data) {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = data[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var item = _step.value;
_this10.removeItem(item.id);
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
});
this._filter();
}
/**
* Give it an ID of a model and a checkbox and it will update the item
* in storage based on the checkbox's state.
*
* @param {number} id The ID of the element to complete or uncomplete
* @param {object} checkbox The checkbox to check the state of complete
* or not
* @param {boolean|undefined} silent Prevent re-filtering the todo items
*/
}, {
key: 'toggleComplete',
value: function toggleComplete(id, completed, silent) {
var _this11 = this;
this.model.update(id, { completed: completed }, function () {
_this11.view.render('elementComplete', { id: id, completed: completed });
});
if (!silent) {
this._filter();
}
}
/**
* Will toggle ALL checkboxes' on/off state and completeness of models.
* Just pass in the event object.
*/
}, {
key: 'toggleAll',
value: function toggleAll(completed) {
var _this12 = this;
this.model.read({ completed: !completed }, function (data) {
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = data[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var item = _step2.value;
_this12.toggleComplete(item.id, completed, true);
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
});
this._filter();
}
/**
* Updates the pieces of the page which change depending on the remaining
* number of todos.
*/
}, {
key: '_updateCount',
value: function _updateCount() {
var _this13 = this;
this.model.getCount(function (todos) {
var completed = todos.completed;
var visible = completed > 0;
var checked = completed === todos.total;
_this13.view.render('updateElementCount', todos.active);
_this13.view.render('clearCompletedButton', { completed: completed, visible: visible });
_this13.view.render('toggleAll', { checked: checked });
_this13.view.render('contentBlockVisibility', { visible: todos.total > 0 });
});
}
/**
* Re-filters the todo items, based on the active route.
* @param {boolean|undefined} force forces a re-painting of todo items.
*/
}, {
key: '_filter',
value: function _filter(force) {
var active = this._activeRoute;
var activeRoute = active.charAt(0).toUpperCase() + active.substr(1);
// Update the elements on the page, which change with each completed todo
this._updateCount();
// If the last active route isn't "All", or we're switching routes, we
// re-create the todo item elements, calling:
// this.show[All|Active|Completed]()
if (force || this._lastActiveRoute !== 'All' || this._lastActiveRoute !== activeRoute) {
this['show' + activeRoute]();
}
this._lastActiveRoute = activeRoute;
}
/**
* Simply updates the filter nav's selected states
*/
}, {
key: '_updateFilter',
value: function _updateFilter(currentPage) {
// Store a reference to the active route, allowing us to re-filter todo
// items as they are marked complete or incomplete.
this._activeRoute = currentPage;
if (currentPage === '') {
this._activeRoute = 'All';
}
this._filter();
this.view.render('setFilter', currentPage);
}
}]);
return Controller;
}();
exports.default = Controller;
\ No newline at end of file
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.qs = qs;
exports.qsa = qsa;
exports.$on = $on;
exports.$delegate = $delegate;
exports.$parent = $parent;
// Allow for looping on nodes by chaining:
// qsa('.foo').forEach(function () {})
NodeList.prototype.forEach = Array.prototype.forEach;
// Get element(s) by CSS selector:
function qs(selector, scope) {
return (scope || document).querySelector(selector);
}
function qsa(selector, scope) {
return (scope || document).querySelectorAll(selector);
}
// addEventListener wrapper:
function $on(target, type, callback, useCapture) {
target.addEventListener(type, callback, !!useCapture);
}
// Attach a handler to event for all elements that match the selector,
// now or in the future, based on a root element
function $delegate(target, selector, type, handler) {
var dispatchEvent = function dispatchEvent(event) {
var targetElement = event.target;
var potentialElements = qsa(selector, target);
var hasMatch = Array.from(potentialElements).includes(targetElement);
if (hasMatch) {
handler.call(targetElement, event);
}
};
// https://developer.mozilla.org/en-US/docs/Web/Events/blur
var useCapture = type === 'blur' || type === 'focus';
$on(target, type, dispatchEvent, useCapture);
}
// Find the element's parent with the given tag name:
// $parent(qs('a'), 'div')
function $parent(element, tagName) {
if (!element.parentNode) {
return;
}
if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) {
return element.parentNode;
}
return $parent(element.parentNode, tagName);
}
\ No newline at end of file
'use strict';
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/**
* Creates a new Model instance and hooks up the storage.
* @constructor
* @param {object} storage A reference to the client side storage class
*/
var Model = function () {
function Model(storage) {
_classCallCheck(this, Model);
this.storage = storage;
}
/**
* Creates a new todo model
*
* @param {string} [title] The title of the task
* @param {function} [callback] The callback to fire after the model is created
*/
_createClass(Model, [{
key: 'create',
value: function create(title, callback) {
title = title || '';
var newItem = {
title: title.trim(),
completed: false
};
this.storage.save(newItem, callback);
}
/**
* Finds and returns a model in storage. If no query is given it'll simply
* return everything. If you pass in a string or number it'll look that up as
* the ID of the model to find. Lastly, you can pass it an object to match
* against.
*
* @param {string|number|object} [query] A query to match models against
* @param {function} [callback] The callback to fire after the model is found
*
* @example
* model.read(1, func) // Will find the model with an ID of 1
* model.read('1') // Same as above
* //Below will find a model with foo equalling bar and hello equalling world.
* model.read({ foo: 'bar', hello: 'world' })
*/
}, {
key: 'read',
value: function read(query, callback) {
var queryType = typeof query === 'undefined' ? 'undefined' : _typeof(query);
if (queryType === 'function') {
this.storage.findAll(query);
} else if (queryType === 'string' || queryType === 'number') {
query = parseInt(query, 10);
this.storage.find({ id: query }, callback);
} else {
this.storage.find(query, callback);
}
}
/**
* Updates a model by giving it an ID, data to update, and a callback to fire when
* the update is complete.
*
* @param {number} id The id of the model to update
* @param {object} data The properties to update and their new value
* @param {function} callback The callback to fire when the update is complete.
*/
}, {
key: 'update',
value: function update(id, data, callback) {
this.storage.save(data, callback, id);
}
/**
* Removes a model from storage
*
* @param {number} id The ID of the model to remove
* @param {function} callback The callback to fire when the removal is complete.
*/
}, {
key: 'remove',
value: function remove(id, callback) {
this.storage.remove(id, callback);
}
/**
* WARNING: Will remove ALL data from storage.
*
* @param {function} callback The callback to fire when the storage is wiped.
*/
}, {
key: 'removeAll',
value: function removeAll(callback) {
this.storage.drop(callback);
}
/**
* Returns a count of all todos
*/
}, {
key: 'getCount',
value: function getCount(callback) {
var todos = {
active: 0,
completed: 0,
total: 0
};
this.storage.findAll(function (data) {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = data[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var todo = _step.value;
if (todo.completed) {
todos.completed++;
} else {
todos.active++;
}
todos.total++;
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
callback(todos);
});
}
}]);
return Model;
}();
exports.default = Model;
\ No newline at end of file
"use strict";
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/*jshint eqeqeq:false */
/**
* Creates a new client side storage object and will create an empty
* collection if no collection already exists.
*
* @param {string} name The name of our DB we want to use
* @param {function} callback Our fake DB uses callbacks because in
* real life you probably would be making AJAX calls
*/
var Store = function () {
function Store(name, callback) {
_classCallCheck(this, Store);
this._dbName = name;
if (!localStorage[name]) {
var data = {
todos: []
};
localStorage[name] = JSON.stringify(data);
}
if (callback) {
callback.call(this, JSON.parse(localStorage[name]));
}
}
/**
* Finds items based on a query given as a JS object
*
* @param {object} query The query to match against (i.e. {foo: 'bar'})
* @param {function} callback The callback to fire when the query has
* completed running
*
* @example
* db.find({foo: 'bar', hello: 'world'}, function (data) {
* // data will return any items that have foo: bar and
* // hello: world in their properties
* })
*/
_createClass(Store, [{
key: "find",
value: function find(query, callback) {
var todos = JSON.parse(localStorage[this._dbName]).todos;
callback.call(this, todos.filter(function (todo) {
for (var q in query) {
if (query[q] !== todo[q]) {
return false;
}
}
return true;
}));
}
/**
* Will retrieve all data from the collection
*
* @param {function} callback The callback to fire upon retrieving data
*/
}, {
key: "findAll",
value: function findAll(callback) {
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
}
/**
* Will save the given data to the DB. If no item exists it will create a new
* item, otherwise it'll simply update an existing item's properties
*
* @param {object} updateData The data to save back into the DB
* @param {function} callback The callback to fire after saving
* @param {number} id An optional param to enter an ID of an item to update
*/
}, {
key: "save",
value: function save(updateData, callback, id) {
var data = JSON.parse(localStorage[this._dbName]);
var todos = data.todos;
var len = todos.length;
// If an ID was actually given, find the item and update each property
if (id) {
for (var i = 0; i < len; i++) {
if (todos[i].id === id) {
for (var key in updateData) {
todos[i][key] = updateData[key];
}
break;
}
}
localStorage[this._dbName] = JSON.stringify(data);
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
} else {
// Generate an ID
updateData.id = new Date().getTime();
todos.push(updateData);
localStorage[this._dbName] = JSON.stringify(data);
if (callback) {
callback.call(this, [updateData]);
}
}
}
/**
* Will remove an item from the Store based on its ID
*
* @param {number} id The ID of the item you want to remove
* @param {function} callback The callback to fire after saving
*/
}, {
key: "remove",
value: function remove(id, callback) {
var data = JSON.parse(localStorage[this._dbName]);
var todos = data.todos;
var len = todos.length;
for (var i = 0; i < todos.length; i++) {
if (todos[i].id == id) {
todos.splice(i, 1);
break;
}
}
localStorage[this._dbName] = JSON.stringify(data);
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
}
/**
* Will drop all storage and start fresh
*
* @param {function} callback The callback to fire after dropping the data
*/
}, {
key: "drop",
value: function drop(callback) {
localStorage[this._dbName] = JSON.stringify({ todos: [] });
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
}
}]);
return Store;
}();
exports.default = Store;
\ No newline at end of file
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var htmlEscapes = {
'&': '&amp',
'<': '&lt',
'>': '&gt',
'"': '&quot',
'\'': '&#x27',
'`': '&#x60'
};
var reUnescapedHtml = /[&<>"'`]/g;
var reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
var escape = function escape(str) {
return str && reHasUnescapedHtml.test(str) ? str.replace(reUnescapedHtml, escapeHtmlChar) : str;
};
var escapeHtmlChar = function escapeHtmlChar(chr) {
return htmlEscapes[chr];
};
var Template = function () {
function Template() {
_classCallCheck(this, Template);
this.defaultTemplate = '\n\t\t\t<li data-id="{{id}}" class="{{completed}}">\n\t\t\t\t<div class="view">\n\t\t\t\t\t<input class="toggle" type="checkbox" {{checked}}>\n\t\t\t\t\t<label>{{title}}</label>\n\t\t\t\t\t<button class="destroy"></button>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t';
}
/**
* Creates an <li> HTML string and returns it for placement in your app.
*
* NOTE: In real life you should be using a templating engine such as Mustache
* or Handlebars, however, this is a vanilla JS example.
*
* @param {object} data The object containing keys you want to find in the
* template to replace.
* @returns {string} HTML String of an <li> element
*
* @example
* view.show({
* id: 1,
* title: "Hello World",
* completed: 0,
* })
*/
_createClass(Template, [{
key: 'show',
value: function show(data) {
var _this = this;
var view = data.map(function (d) {
var template = _this.defaultTemplate;
var completed = d.completed ? 'completed' : '';
var checked = d.completed ? 'checked' : '';
return _this.defaultTemplate.replace('{{id}}', d.id).replace('{{title}}', escape(d.title)).replace('{{completed}}', completed).replace('{{checked}}', checked);
});
return view.join('');
}
/**
* Displays a counter of how many to dos are left to complete
*
* @param {number} activeTodos The number of active todos.
* @returns {string} String containing the count
*/
}, {
key: 'itemCounter',
value: function itemCounter(activeTodos) {
var plural = activeTodos === 1 ? '' : 's';
return '<strong>' + activeTodos + '</strong> item' + plural + ' left';
}
/**
* Updates the text within the "Clear completed" button
*
* @param {[type]} completedTodos The number of completed todos.
* @returns {string} String containing the count
*/
}, {
key: 'clearCompletedButton',
value: function clearCompletedButton(completedTodos) {
return completedTodos > 0 ? 'Clear completed' : '';
}
}]);
return Template;
}();
exports.default = Template;
\ No newline at end of file
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
var _helpers = require('./helpers');
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var _itemId = function _itemId(element) {
return parseInt((0, _helpers.$parent)(element, 'li').dataset.id, 10);
};
var _setFilter = function _setFilter(currentPage) {
(0, _helpers.qs)('.filters .selected').className = '';
(0, _helpers.qs)('.filters [href="#/' + currentPage + '"]').className = 'selected';
};
var _elementComplete = function _elementComplete(id, completed) {
var listItem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (!listItem) {
return;
}
listItem.className = completed ? 'completed' : '';
// In case it was toggled from an event and not by clicking the checkbox
(0, _helpers.qs)('input', listItem).checked = completed;
};
var _editItem = function _editItem(id, title) {
var listItem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (!listItem) {
return;
}
listItem.className += ' editing';
var input = document.createElement('input');
input.className = 'edit';
listItem.appendChild(input);
input.focus();
input.value = title;
};
/**
* View that abstracts away the browser's DOM completely.
* It has two simple entry points:
*
* - bind(eventName, handler)
* Takes a todo application event and registers the handler
* - render(command, parameterObject)
* Renders the given command with the options
*/
var View = function () {
function View(template) {
var _this = this;
_classCallCheck(this, View);
this.template = template;
this.ENTER_KEY = 13;
this.ESCAPE_KEY = 27;
this.$todoList = (0, _helpers.qs)('.todo-list');
this.$todoItemCounter = (0, _helpers.qs)('.todo-count');
this.$clearCompleted = (0, _helpers.qs)('.clear-completed');
this.$main = (0, _helpers.qs)('.main');
this.$footer = (0, _helpers.qs)('.footer');
this.$toggleAll = (0, _helpers.qs)('.toggle-all');
this.$newTodo = (0, _helpers.qs)('.new-todo');
this.viewCommands = {
showEntries: function showEntries(parameter) {
return _this.$todoList.innerHTML = _this.template.show(parameter);
},
removeItem: function removeItem(parameter) {
return _this._removeItem(parameter);
},
updateElementCount: function updateElementCount(parameter) {
return _this.$todoItemCounter.innerHTML = _this.template.itemCounter(parameter);
},
clearCompletedButton: function clearCompletedButton(parameter) {
return _this._clearCompletedButton(parameter.completed, parameter.visible);
},
contentBlockVisibility: function contentBlockVisibility(parameter) {
return _this.$main.style.display = _this.$footer.style.display = parameter.visible ? 'block' : 'none';
},
toggleAll: function toggleAll(parameter) {
return _this.$toggleAll.checked = parameter.checked;
},
setFilter: function setFilter(parameter) {
return _setFilter(parameter);
},
clearNewTodo: function clearNewTodo(parameter) {
return _this.$newTodo.value = '';
},
elementComplete: function elementComplete(parameter) {
return _elementComplete(parameter.id, parameter.completed);
},
editItem: function editItem(parameter) {
return _editItem(parameter.id, parameter.title);
},
editItemDone: function editItemDone(parameter) {
return _this._editItemDone(parameter.id, parameter.title);
}
};
}
_createClass(View, [{
key: '_removeItem',
value: function _removeItem(id) {
var elem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (elem) {
this.$todoList.removeChild(elem);
}
}
}, {
key: '_clearCompletedButton',
value: function _clearCompletedButton(completedCount, visible) {
this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount);
this.$clearCompleted.style.display = visible ? 'block' : 'none';
}
}, {
key: '_editItemDone',
value: function _editItemDone(id, title) {
var listItem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (!listItem) {
return;
}
var input = (0, _helpers.qs)('input.edit', listItem);
listItem.removeChild(input);
listItem.className = listItem.className.replace(' editing', '');
(0, _helpers.qsa)('label', listItem).forEach(function (label) {
return label.textContent = title;
});
}
}, {
key: 'render',
value: function render(viewCmd, parameter) {
this.viewCommands[viewCmd](parameter);
}
}, {
key: '_bindItemEditDone',
value: function _bindItemEditDone(handler) {
var self = this;
(0, _helpers.$delegate)(self.$todoList, 'li .edit', 'blur', function () {
if (!this.dataset.iscanceled) {
handler({
id: _itemId(this),
title: this.value
});
}
});
// Remove the cursor from the input when you hit enter just like if it were a real form
(0, _helpers.$delegate)(self.$todoList, 'li .edit', 'keypress', function (event) {
if (event.keyCode === self.ENTER_KEY) {
this.blur();
}
});
}
}, {
key: '_bindItemEditCancel',
value: function _bindItemEditCancel(handler) {
var self = this;
(0, _helpers.$delegate)(self.$todoList, 'li .edit', 'keyup', function (event) {
if (event.keyCode === self.ESCAPE_KEY) {
var id = _itemId(this);
this.dataset.iscanceled = true;
this.blur();
handler({ id: id });
}
});
}
}, {
key: 'bind',
value: function bind(event, handler) {
var _this2 = this;
switch (event) {
case 'newTodo':
(0, _helpers.$on)(this.$newTodo, 'change', function () {
return handler(_this2.$newTodo.value);
});
break;
case 'removeCompleted':
(0, _helpers.$on)(this.$clearCompleted, 'click', handler);
break;
case 'toggleAll':
(0, _helpers.$on)(this.$toggleAll, 'click', function () {
handler({ completed: this.checked });
});
break;
case 'itemEdit':
(0, _helpers.$delegate)(this.$todoList, 'li label', 'dblclick', function () {
handler({ id: _itemId(this) });
});
break;
case 'itemRemove':
(0, _helpers.$delegate)(this.$todoList, '.destroy', 'click', function () {
handler({ id: _itemId(this) });
});
break;
case 'itemToggle':
(0, _helpers.$delegate)(this.$todoList, '.toggle', 'click', function () {
handler({
id: _itemId(this),
completed: this.checked
});
});
break;
case 'itemEditDone':
this._bindItemEditDone(handler);
break;
case 'itemEditCancel':
this._bindItemEditCancel(handler);
break;
}
}
}]);
return View;
}();
exports.default = View;
\ No newline at end of file
<!DOCTYPE html>
<html>
<!doctype html>
<html lang="en" data-framework="es6">
<head>
<meta charset="utf-8">
<title>ES6 • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<title>Vanilla ES6 • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
</head>
<body>
......@@ -12,31 +11,29 @@
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus>
</header>
<section class="main">
<section style="display:none" class="main">
<input class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list"></ul>
<footer class="footer">
<span class="todo-count"></span>
<div class="filters">
<a href="#/" class="selected">All</a>
<a href="#/active">Active</a>
<a href="#/completed">Completed</a>
</div>
<button class="clear-completed">Clear completed</button>
</footer>
</section>
<footer class="footer">
<span class="todo-count"></span>
<ul class="filters">
<li><a href="#/" class="selected">All</a></li>
<li><a href="#/active">Active</a></li>
<li><a href="#/completed">Completed</a></li>
</ul>
<button class="clear-completed">Clear completed</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<br>
<p>Written by <a href="http://twitter.com/lukeed05">Luke Edwards</a></p>
<p>Refactored by <a href="https://github.com/xorgy">Aaron Muir Hamilton</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script src="dist/bundle.js"></script>
<script src="node_modules/todomvc-common/base.js"></script>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
</body>
</html>
......@@ -17,8 +17,7 @@ button {
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
......@@ -30,8 +29,7 @@ body {
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
......@@ -100,8 +98,7 @@ input[type="checkbox"] {
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.new-todo {
......@@ -163,17 +160,19 @@ label[for='toggle-all'] {
padding: 0;
}
.todo-list li.editing button,
.todo-list li.editing label,
.todo-list li.editing .toggle{
display: none;
}
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
padding: 12px 16px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
......@@ -287,11 +286,7 @@ label[for='toggle-all'] {
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
.filters a {
color: inherit;
margin: 3px;
padding: 3px 7px;
......@@ -300,12 +295,12 @@ label[for='toggle-all'] {
border-radius: 3px;
}
.filters li a.selected,
.filters li a:hover {
.filters a.selected,
.filters a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
.filters a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
......
/* global _ */
(function () {
'use strict';
/* jshint ignore:start */
// Underscore's Template Module
// Courtesy of underscorejs.org
var _ = (function (_) {
_.defaults = function (object) {
if (!object) {
return object;
}
for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {
var iterable = arguments[argsIndex];
if (iterable) {
for (var key in iterable) {
if (object[key] == null) {
object[key] = iterable[key];
}
}
}
}
return object;
}
// By default, Underscore uses ERB-style template delimiters, change the
// following template settings to use alternative delimiters.
_.templateSettings = {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g,
escape : /<%-([\s\S]+?)%>/g
};
// When customizing `templateSettings`, if you don't want to define an
// interpolation, evaluation or escaping regex, we need one that is
// guaranteed not to match.
var noMatch = /(.)^/;
// Certain characters need to be escaped so that they can be put into a
// string literal.
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\t': 't',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
_.template = function(text, data, settings) {
var render;
settings = _.defaults({}, settings, _.templateSettings);
// Combine delimiters into one regular expression via alternation.
var matcher = new RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
// Compile the template source, escaping string literals appropriately.
var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset)
.replace(escaper, function(match) { return '\\' + escapes[match]; });
if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
}
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
}
if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
index = offset + match.length;
return match;
});
source += "';\n";
// If a variable is not specified, place data values in local scope.
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + "return __p;\n";
try {
render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}
if (data) return render(data, _);
var template = function(data) {
return render.call(this, data, _);
};
// Provide the compiled function source as a convenience for precompilation.
template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
return template;
};
return _;
})({});
if (location.hostname === 'todomvc.com') {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-31081062-1', 'auto');
ga('send', 'pageview');
}
/* jshint ignore:end */
function redirect() {
if (location.hostname === 'tastejs.github.io') {
location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com');
}
}
function findRoot() {
var base = location.href.indexOf('examples/');
return location.href.substr(0, base);
}
function getFile(file, callback) {
if (!location.host) {
return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.');
}
var xhr = new XMLHttpRequest();
xhr.open('GET', findRoot() + file, true);
xhr.send();
xhr.onload = function () {
if (xhr.status === 200 && callback) {
callback(xhr.responseText);
}
};
}
function Learn(learnJSON, config) {
if (!(this instanceof Learn)) {
return new Learn(learnJSON, config);
}
var template, framework;
if (typeof learnJSON !== 'object') {
try {
learnJSON = JSON.parse(learnJSON);
} catch (e) {
return;
}
}
if (config) {
template = config.template;
framework = config.framework;
}
if (!template && learnJSON.templates) {
template = learnJSON.templates.todomvc;
}
if (!framework && document.querySelector('[data-framework]')) {
framework = document.querySelector('[data-framework]').dataset.framework;
}
this.template = template;
if (learnJSON.backend) {
this.frameworkJSON = learnJSON.backend;
this.frameworkJSON.issueLabel = framework;
this.append({
backend: true
});
} else if (learnJSON[framework]) {
this.frameworkJSON = learnJSON[framework];
this.frameworkJSON.issueLabel = framework;
this.append();
}
this.fetchIssueCount();
}
Learn.prototype.append = function (opts) {
var aside = document.createElement('aside');
aside.innerHTML = _.template(this.template, this.frameworkJSON);
aside.className = 'learn';
if (opts && opts.backend) {
// Remove demo link
var sourceLinks = aside.querySelector('.source-links');
var heading = sourceLinks.firstElementChild;
var sourceLink = sourceLinks.lastElementChild;
// Correct link path
var href = sourceLink.getAttribute('href');
sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http')));
sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML;
} else {
// Localize demo links
var demoLinks = aside.querySelectorAll('.demo-link');
Array.prototype.forEach.call(demoLinks, function (demoLink) {
if (demoLink.getAttribute('href').substr(0, 4) !== 'http') {
demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href'));
}
});
}
document.body.className = (document.body.className + ' learn-bar').trim();
document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
};
Learn.prototype.fetchIssueCount = function () {
var issueLink = document.getElementById('issue-count-link');
if (issueLink) {
var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos');
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function (e) {
var parsedResponse = JSON.parse(e.target.responseText);
if (parsedResponse instanceof Array) {
var count = parsedResponse.length;
if (count !== 0) {
issueLink.innerHTML = 'This app has ' + count + ' open issues';
document.getElementById('issue-count').style.display = 'inline';
}
}
};
xhr.send();
}
};
redirect();
getFile('learn.json', Learn);
})();
{
"private": true,
"scripts": {
"compile": "babel src --presets es2015 --out-dir=dist && browserify dist/app.js > dist/bundle.js",
"compile": "java -jar node_modules/google-closure-compiler/compiler.jar -O ADVANCED --language_in=ES6_STRICT --new_type_inf --js_output_file='dist/bundle.js' 'src/**.js' -W VERBOSE",
"prepublish": "npm run compile"
},
"dependencies": {
......@@ -9,8 +9,6 @@
"todomvc-common": "^1.0.2"
},
"devDependencies": {
"babel-core": "^6.1.0",
"babel-preset-es2015": "^6.1.18",
"browserify": "^12.0.1"
"google-closure-compiler": "^20160315.2.0"
}
}
import Controller from './controller';
import * as helpers from './helpers';
import {$on} from './helpers';
import Template from './template';
import Store from './store';
import Model from './model';
import View from './view';
const $on = helpers.$on;
const setView = () => todo.controller.setView(document.location.hash);
const store = new Store('todos-vanilla-es6');
class Todo {
/**
* Init new Todo List
* @param {string} The name of your list
*/
constructor(name) {
this.storage = new Store(name);
this.model = new Model(this.storage);
const template = new Template();
const view = new View(template);
this.template = new Template();
this.view = new View(this.template);
this.controller = new Controller(this.model, this.view);
}
}
const todo = new Todo('todos-vanillajs');
/**
* @type {Controller}
*/
const controller = new Controller(store, view);
const setView = () => controller.setView(document.location.hash);
$on(window, 'load', setView);
$on(window, 'hashchange', setView);
import {emptyItemQuery} from './item';
import Store from './store';
import View from './view';
export default class Controller {
/**
* Take a model & view, then act as controller between them
* @param {object} model The model instance
* @param {object} view The view instance
* @param {!Store} store A Store instance
* @param {!View} view A View instance
*/
constructor(model, view) {
this.model = model;
constructor(store, view) {
this.store = store;
this.view = view;
this.view.bind('newTodo', title => this.addItem(title));
this.view.bind('itemEdit', item => this.editItem(item.id));
this.view.bind('itemEditDone', item => this.editItemSave(item.id, item.title));
this.view.bind('itemEditCancel', item => this.editItemCancel(item.id));
this.view.bind('itemRemove', item => this.removeItem(item.id));
this.view.bind('itemToggle', item => this.toggleComplete(item.id, item.completed));
this.view.bind('removeCompleted', () => this.removeCompletedItems());
this.view.bind('toggleAll', status => this.toggleAll(status.completed));
}
/**
* Load & Initialize the view
* @param {string} '' | 'active' | 'completed'
*/
setView(hash) {
const route = hash.split('/')[1];
const page = route || '';
this._updateFilter(page);
}
/**
* Event fires on load. Gets all items & displays them
*/
showAll() {
this.model.read(data => this.view.render('showEntries', data));
}
view.bindAddItem(this.addItem.bind(this));
view.bindEditItemSave(this.editItemSave.bind(this));
view.bindEditItemCancel(this.editItemCancel.bind(this));
view.bindRemoveItem(this.removeItem.bind(this));
view.bindToggleItem((id, completed) => {
this.toggleCompleted(id, completed);
this._filter();
});
view.bindRemoveCompleted(this.removeCompletedItems.bind(this));
view.bindToggleAll(this.toggleAll.bind(this));
/**
* Renders all active tasks
*/
showActive() {
this.model.read({completed: false}, data => this.view.render('showEntries', data));
this._activeRoute = '';
this._lastActiveRoute = null;
}
/**
* Renders all completed tasks
* Set and render the active route.
*
* @param {string} raw '' | '#/' | '#/active' | '#/completed'
*/
showCompleted() {
this.model.read({completed: true}, data => this.view.render('showEntries', data));
setView(raw) {
const route = raw.replace(/^#\//, '');
this._activeRoute = route;
this._filter();
this.view.updateFilterButtons(route);
}
/**
* An event to fire whenever you want to add an item. Simply pass in the event
* object and it'll handle the DOM insertion and saving of the new item.
* Add an Item to the Store and display it in the list.
*
* @param {!string} title Title of the new item
*/
addItem(title) {
if (title.trim() === '') {
return;
}
this.model.create(title, () => {
this.view.render('clearNewTodo');
this.store.insert({
id: Date.now(),
title,
completed: false
}, () => {
this.view.clearNewTodo();
this._filter(true);
});
}
/*
* Triggers the item editing mode.
*/
editItem(id) {
this.model.read(id, data => {
const title = data[0].title;
this.view.render('editItem', {id, title});
});
}
/*
* Finishes the item editing mode successfully.
/**
* Save an Item in edit.
*
* @param {number} id ID of the Item in edit
* @param {!string} title New title for the Item in edit
*/
editItemSave(id, title) {
title = title.trim();
if (title.length !== 0) {
this.model.update(id, {title}, () => {
this.view.render('editItemDone', {id, title});
if (title.length) {
this.store.update({id, title}, () => {
this.view.editItemDone(id, title);
});
} else {
this.removeItem(id);
}
}
/*
* Cancels the item editing mode.
/**
* Cancel the item editing mode.
*
* @param {!number} id ID of the Item in edit
*/
editItemCancel(id) {
this.model.read(id, data => {
this.store.find({id}, data => {
const title = data[0].title;
this.view.render('editItemDone', {id, title});
this.view.editItemDone(id, title);
});
}
/**
* Find the DOM element with given ID,
* Then remove it from DOM & Storage
* Remove the data and elements related to an Item.
*
* @param {!number} id Item ID of item to remove
*/
removeItem(id) {
this.model.remove(id, () => this.view.render('removeItem', id));
this._filter();
this.store.remove({id}, () => {
this._filter();
this.view.removeItem(id);
});
}
/**
* Will remove all completed items from the DOM and storage.
* Remove all completed items.
*/
removeCompletedItems() {
this.model.read({completed: true}, data => {
for (let item of data) {
this.removeItem(item.id);
}
});
this._filter();
this.store.remove({completed: true}, this._filter.bind(this));
}
/**
* Give it an ID of a model and a checkbox and it will update the item
* in storage based on the checkbox's state.
* Update an Item in storage based on the state of completed.
*
* @param {number} id The ID of the element to complete or uncomplete
* @param {object} checkbox The checkbox to check the state of complete
* or not
* @param {boolean|undefined} silent Prevent re-filtering the todo items
* @param {!number} id ID of the target Item
* @param {!boolean} completed Desired completed state
*/
toggleComplete(id, completed, silent) {
this.model.update(id, {completed}, () => {
this.view.render('elementComplete', {id, completed});
toggleCompleted(id, completed) {
this.store.update({id, completed}, () => {
this.view.setItemComplete(id, completed);
});
if (!silent) {
this._filter();
}
}
/**
* Will toggle ALL checkboxes' on/off state and completeness of models.
* Just pass in the event object.
* Set all items to complete or active.
*
* @param {boolean} completed Desired completed state
*/
toggleAll(completed) {
this.model.read({completed: !completed}, data => {
for (let item of data) {
this.toggleComplete(item.id, completed, true);
this.store.find({completed: !completed}, data => {
for (let {id} of data) {
this.toggleCompleted(id, completed);
}
});
......@@ -155,58 +129,31 @@ export default class Controller {
}
/**
* Updates the pieces of the page which change depending on the remaining
* number of todos.
*/
_updateCount() {
this.model.getCount(todos => {
const completed = todos.completed;
const visible = completed > 0;
const checked = completed === todos.total;
this.view.render('updateElementCount', todos.active);
this.view.render('clearCompletedButton', {completed, visible});
this.view.render('toggleAll', {checked});
this.view.render('contentBlockVisibility', {visible: todos.total > 0});
});
}
/**
* Re-filters the todo items, based on the active route.
* @param {boolean|undefined} force forces a re-painting of todo items.
* Refresh the list based on the current route.
*
* @param {boolean} [force] Force a re-paint of the list
*/
_filter(force) {
const active = this._activeRoute;
const activeRoute = active.charAt(0).toUpperCase() + active.substr(1);
// Update the elements on the page, which change with each completed todo
this._updateCount();
// If the last active route isn't "All", or we're switching routes, we
// re-create the todo item elements, calling:
// this.show[All|Active|Completed]()
if (force || this._lastActiveRoute !== 'All' || this._lastActiveRoute !== activeRoute) {
this['show' + activeRoute]();
const route = this._activeRoute;
if (force || this._lastActiveRoute !== '' || this._lastActiveRoute !== route) {
/* jscs:disable disallowQuotedKeysInObjects */
this.store.find({
'': emptyItemQuery,
'active': {completed: false},
'completed': {completed: true}
}[route], this.view.showItems.bind(this.view));
/* jscs:enable disallowQuotedKeysInObjects */
}
this._lastActiveRoute = activeRoute;
}
/**
* Simply updates the filter nav's selected states
*/
_updateFilter(currentPage) {
// Store a reference to the active route, allowing us to re-filter todo
// items as they are marked complete or incomplete.
this._activeRoute = currentPage;
this.store.count((total, active, completed) => {
this.view.setItemsLeft(active);
this.view.setClearCompletedButtonVisibility(completed);
if (currentPage === '') {
this._activeRoute = 'All';
}
this._filter();
this.view.setCompleteAllCheckbox(completed === total);
this.view.setMainVisibility(total);
});
this.view.render('setFilter', currentPage);
this._lastActiveRoute = route;
}
}
// Allow for looping on nodes by chaining:
// qsa('.foo').forEach(function () {})
NodeList.prototype.forEach = Array.prototype.forEach;
// Get element(s) by CSS selector:
/**
* querySelector wrapper
*
* @param {string} selector Selector to query
* @param {Element} [scope] Optional scope element for the selector
*/
export function qs(selector, scope) {
return (scope || document).querySelector(selector);
}
export function qsa(selector, scope) {
return (scope || document).querySelectorAll(selector);
}
// addEventListener wrapper:
export function $on(target, type, callback, useCapture) {
target.addEventListener(type, callback, !!useCapture);
/**
* addEventListener wrapper
*
* @param {Element|Window} target Target Element
* @param {string} type Event name to bind to
* @param {Function} callback Event callback
* @param {boolean} [capture] Capture the event
*/
export function $on(target, type, callback, capture) {
target.addEventListener(type, callback, !!capture);
}
// Attach a handler to event for all elements that match the selector,
// now or in the future, based on a root element
export function $delegate(target, selector, type, handler) {
/**
* Attach a handler to an event for all elements matching a selector.
*
* @param {Element} target Element which the event must bubble to
* @param {string} selector Selector to match
* @param {string} type Event name
* @param {Function} handler Function called when the event bubbles to target
* from an element matching selector
* @param {boolean} [capture] Capture the event
*/
export function $delegate(target, selector, type, handler, capture) {
const dispatchEvent = event => {
const targetElement = event.target;
const potentialElements = qsa(selector, target);
const hasMatch = Array.from(potentialElements).includes(targetElement);
if (hasMatch) {
handler.call(targetElement, event);
const potentialElements = target.querySelectorAll(selector);
let i = potentialElements.length;
while (i--) {
if (potentialElements[i] === targetElement) {
handler.call(targetElement, event);
break;
}
}
};
// https://developer.mozilla.org/en-US/docs/Web/Events/blur
const useCapture = type === 'blur' || type === 'focus';
$on(target, type, dispatchEvent, useCapture);
$on(target, type, dispatchEvent, !!capture);
}
// Find the element's parent with the given tag name:
// $parent(qs('a'), 'div')
export function $parent(element, tagName) {
if (!element.parentNode) {
return;
}
if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) {
return element.parentNode;
}
return $parent(element.parentNode, tagName);
}
/**
* Encode less-than and ampersand characters with entity codes to make user-
* provided text safe to parse as HTML.
*
* @param {string} s String to escape
*
* @returns {string} String with unsafe characters escaped with entity codes
*/
export const escapeForHTML = s => s.replace(/[&<]/g, c => c === '&' ? '&amp;' : '&lt;');
/**
* @typedef {!{id: number, completed: boolean, title: string}}
*/
export var Item;
/**
* @typedef {!Array<Item>}
*/
export var ItemList;
/**
* Enum containing a known-empty record type, matching only empty records unlike Object.
*
* @enum {Object}
*/
const Empty = {
Record: {}
};
/**
* Empty ItemQuery type, based on the Empty @enum.
*
* @typedef {Empty}
*/
export var EmptyItemQuery;
/**
* Reference to the only EmptyItemQuery instance.
*
* @type {EmptyItemQuery}
*/
export const emptyItemQuery = Empty.Record;
/**
* @typedef {!({id: number}|{completed: boolean}|EmptyItemQuery)}
*/
export var ItemQuery;
/**
* @typedef {!({id: number, title: string}|{id: number, completed: boolean})}
*/
export var ItemUpdate;
/**
* Creates a new Model instance and hooks up the storage.
* @constructor
* @param {object} storage A reference to the client side storage class
*/
export default class Model {
constructor(storage) {
this.storage = storage;
}
/**
* Creates a new todo model
*
* @param {string} [title] The title of the task
* @param {function} [callback] The callback to fire after the model is created
*/
create(title, callback){
title = title || '';
const newItem = {
title: title.trim(),
completed: false
};
this.storage.save(newItem, callback);
}
/**
* Finds and returns a model in storage. If no query is given it'll simply
* return everything. If you pass in a string or number it'll look that up as
* the ID of the model to find. Lastly, you can pass it an object to match
* against.
*
* @param {string|number|object} [query] A query to match models against
* @param {function} [callback] The callback to fire after the model is found
*
* @example
* model.read(1, func) // Will find the model with an ID of 1
* model.read('1') // Same as above
* //Below will find a model with foo equalling bar and hello equalling world.
* model.read({ foo: 'bar', hello: 'world' })
*/
read(query, callback){
const queryType = typeof query;
if (queryType === 'function') {
this.storage.findAll(query);
} else if (queryType === 'string' || queryType === 'number') {
query = parseInt(query, 10);
this.storage.find({id: query}, callback);
} else {
this.storage.find(query, callback);
}
}
/**
* Updates a model by giving it an ID, data to update, and a callback to fire when
* the update is complete.
*
* @param {number} id The id of the model to update
* @param {object} data The properties to update and their new value
* @param {function} callback The callback to fire when the update is complete.
*/
update(id, data, callback){
this.storage.save(data, callback, id);
}
/**
* Removes a model from storage
*
* @param {number} id The ID of the model to remove
* @param {function} callback The callback to fire when the removal is complete.
*/
remove(id, callback){
this.storage.remove(id, callback);
}
/**
* WARNING: Will remove ALL data from storage.
*
* @param {function} callback The callback to fire when the storage is wiped.
*/
removeAll(callback){
this.storage.drop(callback);
}
/**
* Returns a count of all todos
*/
getCount(callback){
const todos = {
active: 0,
completed: 0,
total: 0
};
this.storage.findAll(data => {
for (let todo of data) {
if (todo.completed) {
todos.completed++;
} else {
todos.active++;
}
todos.total++;
}
callback(todos);
});
}
}
/*jshint eqeqeq:false */
/**
* Creates a new client side storage object and will create an empty
* collection if no collection already exists.
*
* @param {string} name The name of our DB we want to use
* @param {function} callback Our fake DB uses callbacks because in
* real life you probably would be making AJAX calls
*/
import {Item, ItemList, ItemQuery, ItemUpdate, emptyItemQuery} from './item';
export default class Store {
/**
* @param {!string} name Database name
* @param {function()} [callback] Called when the Store is ready
*/
constructor(name, callback) {
this._dbName = name;
if (!localStorage[name]) {
const data = {
todos: []
};
localStorage[name] = JSON.stringify(data);
}
/**
* @type {Storage}
*/
const localStorage = window.localStorage;
/**
* @type {ItemList}
*/
let liveTodos;
/**
* Read the local ItemList from localStorage.
*
* @returns {ItemList} Current array of todos
*/
this.getLocalStorage = () => {
return liveTodos || JSON.parse(localStorage.getItem(name) || '[]');
};
/**
* Write the local ItemList to localStorage.
*
* @param {ItemList} todos Array of todos to write
*/
this.setLocalStorage = (todos) => {
localStorage.setItem(name, JSON.stringify(liveTodos = todos));
};
if (callback) {
callback.call(this, JSON.parse(localStorage[name]));
callback();
}
}
/**
* Finds items based on a query given as a JS object
* Find items with properties matching those on query.
*
* @param {object} query The query to match against (i.e. {foo: 'bar'})
* @param {function} callback The callback to fire when the query has
* completed running
* @param {ItemQuery} query Query to match
* @param {function(ItemList)} callback Called when the query is done
*
* @example
* db.find({foo: 'bar', hello: 'world'}, function (data) {
* // data will return any items that have foo: bar and
* // hello: world in their properties
* })
* db.find({completed: true}, data => {
* // data shall contain items whose completed properties are true
* })
*/
find(query, callback){
const todos = JSON.parse(localStorage[this._dbName]).todos;
find(query, callback) {
const todos = this.getLocalStorage();
let k;
callback.call(this, todos.filter(todo => {
for (let q in query) {
if (query[q] !== todo[q]) {
callback(todos.filter(todo => {
for (k in query) {
if (query[k] !== todo[k]) {
return false;
}
}
......@@ -52,93 +65,90 @@ export default class Store {
}
/**
* Will retrieve all data from the collection
* Update an item in the Store.
*
* @param {function} callback The callback to fire upon retrieving data
* @param {ItemUpdate} update Record with an id and a property to update
* @param {function()} [callback] Called when partialRecord is applied
*/
findAll(callback){
update(update, callback) {
const id = update.id;
const todos = this.getLocalStorage();
let i = todos.length;
let k;
while (i--) {
if (todos[i].id === id) {
for (k in update) {
todos[i][k] = update[k];
}
break;
}
}
this.setLocalStorage(todos);
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
callback();
}
}
/**
* Will save the given data to the DB. If no item exists it will create a new
* item, otherwise it'll simply update an existing item's properties
* Insert an item into the Store.
*
* @param {object} updateData The data to save back into the DB
* @param {function} callback The callback to fire after saving
* @param {number} id An optional param to enter an ID of an item to update
* @param {Item} item Item to insert
* @param {function()} [callback] Called when item is inserted
*/
save(updateData, callback, id){
const data = JSON.parse(localStorage[this._dbName]);
const todos = data.todos;
const len = todos.length;
// If an ID was actually given, find the item and update each property
if (id) {
for (let i = 0; i < len; i++) {
if (todos[i].id === id) {
for (let key in updateData) {
todos[i][key] = updateData[key];
}
break;
}
}
insert(item, callback) {
const todos = this.getLocalStorage();
todos.push(item);
this.setLocalStorage(todos);
localStorage[this._dbName] = JSON.stringify(data);
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
} else {
// Generate an ID
updateData.id = new Date().getTime();
todos.push(updateData);
localStorage[this._dbName] = JSON.stringify(data);
if (callback) {
callback.call(this, [updateData]);
}
if (callback) {
callback();
}
}
/**
* Will remove an item from the Store based on its ID
* Remove items from the Store based on a query.
*
* @param {number} id The ID of the item you want to remove
* @param {function} callback The callback to fire after saving
* @param {ItemQuery} query Query matching the items to remove
* @param {function(ItemList)|function()} [callback] Called when records matching query are removed
*/
remove(id, callback){
const data = JSON.parse(localStorage[this._dbName]);
const todos = data.todos;
const len = todos.length;
for (let i = 0; i < todos.length; i++) {
if (todos[i].id == id) {
todos.splice(i, 1);
break;
remove(query, callback) {
let k;
const todos = this.getLocalStorage().filter(todo => {
for (k in query) {
if (query[k] !== todo[k]) {
return true;
}
}
}
return false;
});
localStorage[this._dbName] = JSON.stringify(data);
this.setLocalStorage(todos);
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
callback(todos);
}
}
/**
* Will drop all storage and start fresh
* Count total, active, and completed todos.
*
* @param {function} callback The callback to fire after dropping the data
* @param {function(number, number, number)} callback Called when the count is completed
*/
drop(callback){
localStorage[this._dbName] = JSON.stringify({todos: []});
count(callback) {
this.find(emptyItemQuery, data => {
const total = data.length;
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
let i = total;
let completed = 0;
while (i--) {
completed += data[i].completed;
}
callback(total, total - completed, completed);
});
}
}
const htmlEscapes = {
'&': '&amp',
'<': '&lt',
'>': '&gt',
'"': '&quot',
'\'': '&#x27',
'`': '&#x60'
};
import {ItemList} from './item';
const reUnescapedHtml = /[&<>"'`]/g;
const reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
const escape = str => (str && reHasUnescapedHtml.test(str)) ? str.replace(reUnescapedHtml, escapeHtmlChar) : str;
const escapeHtmlChar = chr => htmlEscapes[chr];
import {escapeForHTML} from './helpers';
export default class Template {
constructor() {
this.defaultTemplate = `
<li data-id="{{id}}" class="{{completed}}">
<div class="view">
<input class="toggle" type="checkbox" {{checked}}>
<label>{{title}}</label>
<button class="destroy"></button>
</div>
</li>
`;
}
/**
* Creates an <li> HTML string and returns it for placement in your app.
* Format the contents of a todo list.
*
* NOTE: In real life you should be using a templating engine such as Mustache
* or Handlebars, however, this is a vanilla JS example.
*
* @param {object} data The object containing keys you want to find in the
* template to replace.
* @returns {string} HTML String of an <li> element
* @param {ItemList} items Object containing keys you want to find in the template to replace.
* @returns {!string} Contents for a todo list
*
* @example
* view.show({
* id: 1,
* title: "Hello World",
* completed: 0,
* })
* id: 1,
* title: "Hello World",
* completed: false,
* })
*/
show(data){
const view = data.map(d => {
const template = this.defaultTemplate;
const completed = d.completed ? 'completed' : '';
const checked = d.completed ? 'checked' : '';
return this.defaultTemplate
.replace('{{id}}', d.id)
.replace('{{title}}', escape(d.title))
.replace('{{completed}}', completed)
.replace('{{checked}}', checked);
});
return view.join('');
itemList(items) {
return items.reduce((a, item) => a + `
<li data-id="${item.id}"${item.completed ? ' class="completed"' : ''}>
<input class="toggle" type="checkbox" ${item.completed ? 'checked' : ''}>
<label>${escapeForHTML(item.title)}</label>
<button class="destroy"></button>
</li>`, '');
}
/**
* Displays a counter of how many to dos are left to complete
* Format the contents of an "items left" indicator.
*
* @param {number} activeTodos The number of active todos.
* @returns {string} String containing the count
*/
itemCounter(activeTodos){
const plural = activeTodos === 1 ? '' : 's';
return `<strong>${activeTodos}</strong> item${plural} left`;
}
/**
* Updates the text within the "Clear completed" button
* @param {number} activeTodos Number of active todos
*
* @param {[type]} completedTodos The number of completed todos.
* @returns {string} String containing the count
* @returns {!string} Contents for an "items left" indicator
*/
clearCompletedButton(completedTodos){
return (completedTodos > 0) ? 'Clear completed' : '';
itemCounter(activeTodos) {
return `${activeTodos} item${activeTodos !== 1 ? 's' : ''} left`;
}
}
import {qs, qsa, $on, $parent, $delegate} from './helpers';
import {ItemList} from './item';
import {qs, $on, $delegate} from './helpers';
import Template from './template';
const _itemId = element => parseInt($parent(element, 'li').dataset.id, 10);
const _itemId = element => parseInt(element.parentNode.dataset.id, 10);
const ENTER_KEY = 13;
const ESCAPE_KEY = 27;
const _setFilter = currentPage => {
qs('.filters .selected').className = '';
qs(`.filters [href="#/${currentPage}"]`).className = 'selected';
};
const _elementComplete = (id, completed) => {
const listItem = qs(`[data-id="${id}"]`);
if (!listItem) {
return;
}
listItem.className = completed ? 'completed' : '';
// In case it was toggled from an event and not by clicking the checkbox
qs('input', listItem).checked = completed;
};
const _editItem = (id, title) => {
const listItem = qs(`[data-id="${id}"]`);
if (!listItem) {
return;
}
listItem.className += ' editing';
const input = document.createElement('input');
input.className = 'edit';
listItem.appendChild(input);
input.focus();
input.value = title;
};
/**
* View that abstracts away the browser's DOM completely.
* It has two simple entry points:
*
* - bind(eventName, handler)
* Takes a todo application event and registers the handler
* - render(command, parameterObject)
* Renders the given command with the options
*/
export default class View {
/**
* @param {!Template} template A Template instance
*/
constructor(template) {
this.template = template;
this.ENTER_KEY = 13;
this.ESCAPE_KEY = 27;
this.$todoList = qs('.todo-list');
this.$todoItemCounter = qs('.todo-count');
this.$clearCompleted = qs('.clear-completed');
this.$main = qs('.main');
this.$footer = qs('.footer');
this.$toggleAll = qs('.toggle-all');
this.$newTodo = qs('.new-todo');
$delegate(this.$todoList, 'li label', 'dblclick', ({target}) => {
this.editItem(target);
});
}
/**
* Put an item into edit mode.
*
* @param {!Element} target Target Item's label Element
*/
editItem(target) {
const listItem = target.parentElement;
this.viewCommands = {
showEntries: parameter => this.$todoList.innerHTML = this.template.show(parameter),
removeItem: parameter => this._removeItem(parameter),
updateElementCount: parameter => this.$todoItemCounter.innerHTML = this.template.itemCounter(parameter),
clearCompletedButton: parameter => this._clearCompletedButton(parameter.completed, parameter.visible),
contentBlockVisibility: parameter => this.$main.style.display = this.$footer.style.display = parameter.visible ? 'block' : 'none',
toggleAll: parameter => this.$toggleAll.checked = parameter.checked,
setFilter: parameter => _setFilter(parameter),
clearNewTodo: parameter => this.$newTodo.value = '',
elementComplete: parameter => _elementComplete(parameter.id, parameter.completed),
editItem: parameter => _editItem(parameter.id, parameter.title),
editItemDone: parameter => this._editItemDone(parameter.id, parameter.title),
};
}
_removeItem(id) {
listItem.classList.add('editing');
const input = document.createElement('input');
input.className = 'edit';
input.value = target.innerText;
listItem.appendChild(input);
input.focus();
}
/**
* Populate the todo list with a list of items.
*
* @param {ItemList} items Array of items to display
*/
showItems(items) {
this.$todoList.innerHTML = this.template.itemList(items);
}
/**
* Remove an item from the view.
*
* @param {number} id Item ID of the item to remove
*/
removeItem(id) {
const elem = qs(`[data-id="${id}"]`);
if (elem) {
......@@ -84,108 +64,170 @@ export default class View {
}
}
_clearCompletedButton(completedCount, visible) {
this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount);
this.$clearCompleted.style.display = visible ? 'block' : 'none';
/**
* Set the number in the 'items left' display.
*
* @param {number} itemsLeft Number of items left
*/
setItemsLeft(itemsLeft) {
this.$todoItemCounter.innerHTML = this.template.itemCounter(itemsLeft);
}
/**
* Set the visibility of the "Clear completed" button.
*
* @param {boolean|number} visible Desired visibility of the button
*/
setClearCompletedButtonVisibility(visible) {
this.$clearCompleted.style.display = !!visible ? 'block' : 'none';
}
/**
* Set the visibility of the main content and footer.
*
* @param {boolean|number} visible Desired visibility
*/
setMainVisibility(visible) {
this.$main.style.display = !!visible ? 'block' : 'none';
}
/**
* Set the checked state of the Complete All checkbox.
*
* @param {boolean|number} checked The desired checked state
*/
setCompleteAllCheckbox(checked) {
this.$toggleAll.checked = !!checked;
}
/**
* Change the appearance of the filter buttons based on the route.
*
* @param {string} route The current route
*/
updateFilterButtons(route) {
qs('.filters>.selected').className = '';
qs(`.filters>[href="#/${route}"]`).className = 'selected';
}
/**
* Clear the new todo input
*/
clearNewTodo() {
this.$newTodo.value = '';
}
_editItemDone(id, title) {
/**
* Render an item as either completed or not.
*
* @param {!number} id Item ID
* @param {!boolean} completed True if the item is completed
*/
setItemComplete(id, completed) {
const listItem = qs(`[data-id="${id}"]`);
if (!listItem) {
return;
}
listItem.className = completed ? 'completed' : '';
// In case it was toggled from an event and not by clicking the checkbox
qs('input', listItem).checked = completed;
}
/**
* Bring an item out of edit mode.
*
* @param {!number} id Item ID of the item in edit
* @param {!string} title New title for the item in edit
*/
editItemDone(id, title) {
const listItem = qs(`[data-id="${id}"]`);
const input = qs('input.edit', listItem);
listItem.removeChild(input);
listItem.className = listItem.className.replace(' editing', '');
listItem.classList.remove('editing');
qsa('label', listItem).forEach(label => label.textContent = title);
qs('label', listItem).textContent = title;
}
render(viewCmd, parameter) {
this.viewCommands[viewCmd](parameter);
/**
* @param {Function} handler Function called on synthetic event.
*/
bindAddItem(handler) {
$on(this.$newTodo, 'change', ({target}) => {
const title = target.value.trim();
if (title) {
handler(title);
}
});
}
_bindItemEditDone(handler) {
const self = this;
/**
* @param {Function} handler Function called on synthetic event.
*/
bindRemoveCompleted(handler) {
$on(this.$clearCompleted, 'click', handler);
}
$delegate(self.$todoList, 'li .edit', 'blur', function () {
if (!this.dataset.iscanceled) {
handler({
id: _itemId(this),
title: this.value
});
}
/**
* @param {Function} handler Function called on synthetic event.
*/
bindToggleAll(handler) {
$on(this.$toggleAll, 'click', ({target}) => {
handler(target.checked);
});
}
// Remove the cursor from the input when you hit enter just like if it were a real form
$delegate(self.$todoList, 'li .edit', 'keypress', function (event) {
if (event.keyCode === self.ENTER_KEY) {
this.blur();
}
/**
* @param {Function} handler Function called on synthetic event.
*/
bindRemoveItem(handler) {
$delegate(this.$todoList, '.destroy', 'click', ({target}) => {
handler(_itemId(target));
});
}
_bindItemEditCancel(handler) {
const self = this;
/**
* @param {Function} handler Function called on synthetic event.
*/
bindToggleItem(handler) {
$delegate(this.$todoList, '.toggle', 'click', ({target}) => {
handler(_itemId(target), target.checked);
});
}
$delegate(self.$todoList, 'li .edit', 'keyup', function (event) {
if (event.keyCode === self.ESCAPE_KEY) {
const id = _itemId(this);
this.dataset.iscanceled = true;
this.blur();
/**
* @param {Function} handler Function called on synthetic event.
*/
bindEditItemSave(handler) {
$delegate(this.$todoList, 'li .edit', 'blur', ({target}) => {
if (!target.dataset.iscanceled) {
handler(_itemId(target), target.value.trim());
}
}, true);
handler({ id });
// Remove the cursor from the input when you hit enter just like if it were a real form
$delegate(this.$todoList, 'li .edit', 'keypress', ({target, keyCode}) => {
if (keyCode === ENTER_KEY) {
target.blur();
}
});
}
bind(event, handler) {
switch (event) {
case 'newTodo':
$on(this.$newTodo, 'change', () => handler(this.$newTodo.value));
break;
case 'removeCompleted':
$on(this.$clearCompleted, 'click', handler);
break;
case 'toggleAll':
$on(this.$toggleAll, 'click', function () {
handler({completed: this.checked});
});
break;
case 'itemEdit':
$delegate(this.$todoList, 'li label', 'dblclick', function () {
handler({id: _itemId(this)});
});
break;
case 'itemRemove':
$delegate(this.$todoList, '.destroy', 'click', function () {
handler({id: _itemId(this)});
});
break;
case 'itemToggle':
$delegate(this.$todoList, '.toggle', 'click', function () {
handler({
id: _itemId(this),
completed: this.checked
});
});
break;
case 'itemEditDone':
this._bindItemEditDone(handler);
break;
case 'itemEditCancel':
this._bindItemEditCancel(handler);
break;
}
/**
* @param {Function} handler Function called on synthetic event.
*/
bindEditItemCancel(handler) {
$delegate(this.$todoList, 'li .edit', 'keyup', ({target, keyCode}) => {
if (keyCode === ESCAPE_KEY) {
target.dataset.iscanceled = true;
target.blur();
handler(_itemId(target));
}
});
}
}
......@@ -11,6 +11,6 @@ __key__:key};this._instances[idx]=inst;return inst},_showHideChildren:function(h
</p><ul class="js-app-list-inner applist js"><li class="routing"><a href="examples/spine" data-source="http://spinejs.com" data-content="Spine is a lightweight framework for building JavaScript web applications. Spine gives you an MVC structure and then gets out of your way, allowing you to concentrate on the fun stuff, building awesome web applications.">Spine</a></li><li class="routing"><a href="examples/vanilladart/build/web" data-source="http://dartlang.org" data-content="Dart firstly targets the development of modern and large scale browser-side web apps. It's an object oriented language with a C-style syntax. It has two run modes : it can be compiled to JS, and will later run in native VM in compliant browsers (just in a dedicated Chromium provided with Dart SDK for the moment).">Dart</a></li><li class="routing"><a href="examples/gwt" data-source="https://developers.google.com/web-toolkit/" data-content="Google Web Toolkit (GWT) is an MVP development toolkit for building and optimizing complex browser-based applications. GWT is used by many products at Google, including Google AdWords.">GWT</a></li><li class="routing"><a href="examples/closure" data-source="http://code.google.com/closure/library/" data-content="The Closure Library is a broad, well-tested, modular, and cross-browser JavaScript library. You can pull just what you need from a large set of reusable UI widgets and controls, and from lower-level utilities for DOM manipulation, server communication, animation, data structures, unit testing, rich-text editing, and more.">Closure</a></li><li class="routing"><a href="examples/elm" data-source="http://elm-lang.org" data-content="A functional reactive language for interactive applications">Elm</a></li><li><a href="examples/angular-dart/web" data-source="https://github.com/angular/angular.dart" data-content="Dart firstly targets the development of modern and large scale browser-side web apps. It's an object oriented language with a C-style syntax. AngularDart is a port of Angular to Dart.">AngularDart</a></li><li><a href="examples/typescript-backbone" data-source="http://typescriptlang.org" data-content="TypeScript is a language for application-scale JavaScript development. It offers classes, modules, interfaces and type-checking at compile time to help you build robust components.">TypeScript <br>+ Backbone.js</a></li><li><a href="examples/typescript-angular" data-source="http://typescriptlang.org" data-content="An AngularJS + TypeScript implementation of TodoMVC. The only significant difference between this and the vanilla Angular app is that dependency injection is done via annotated constructors, which allows minification of JavaScript.">TypeScript <br>+ AngularJS</a></li><li><a href="examples/typescript-react" data-source="http://typescriptlang.org" data-content="An TypeScript + React implementation of TodoMVC. TypeScript is a language for application-scale JavaScript development. It offers classes, modules, interfaces and type-checking at compile time to help you build robust components. This examples showcases how to work with TSX files, which allow us to enjoy native JSX support while working with TypeScript.">TypeScript <br>+ React</a></li><li><a href="examples/serenadejs" data-source="https://github.com/elabs/serenade.js" data-content="Serenade.js is yet another MVC client side JavaScript framework. Why do we indulge in recreating the wheel? We believe that Serenade.js more closely follows the ideas of classical MVC than competing frameworks.">Serenade.js</a></li><li class="routing"><a href="examples/reagent" data-source="https://reagent-project.github.io/" data-content="Reagent provides a minimalistic interface between ClojureScript and React.">Reagent</a></li><li class="routing"><a href="examples/scalajs-react" data-source="https://github.com/japgolly/scalajs-react" data-content="Facebook's React on Scala.js.">Scala.js + React</a></li><li class="routing"><a href="examples/binding-scala" data-source="https://github.com/ThoughtWorksInc/Binding.scala" data-content="Binding.scala is a reactive web framework. This TodoMVC application has the tiniest code size among all the TodoMVC implementations.">Scala.js + Binding.scala</a></li><li class="routing"><a href="examples/js_of_ocaml" data-source="http://ocsigen.org/js_of_ocaml/" data-content="Js_of_ocaml is a compiler of OCaml bytecode to Javascript.">js_of_ocaml</a></li><li class="routing"><a href="examples/humble" data-source="https://github.com/go-humble/humble" data-content="Humble is a collection of loosely-coupled tools designed to build client-side and hybrid web applications using GopherJS, which compiles Go code to JavaScript.">Humble + GopherJS</a></li></ul></div><div class="js-app-list" data-app-list="labs"><p class="applist-intro">
These are examples written in JavaScript that
we are still evaluating.
</p><ul class="js-app-list-inner applist js"><li class="routing"><a href="examples/thorax" data-source="http://thoraxjs.org" data-content="An opinionated, battle tested Backbone + Handlebars framework to build large scale web applications.">Thorax</a></li><li class="routing"><a href="examples/chaplin-brunch/public" data-source="http://chaplinjs.org" data-content="Chaplin is an architecture for JavaScript applications using the Backbone.js library. Chaplin addresses Backbone’s limitations by providing a lightweight and flexible structure that features well-proven design patterns and best practises.">Chaplin + Brunch</a></li><li class="routing"><a href="examples/backbone_require" data-source="http://requirejs.org" data-content="RequireJS is a JavaScript file and module loader. It is optimized for in-browser use, but it can be used in other JavaScript environments, like Rhino and Node. Using a modular script loader like RequireJS will improve the speed and quality of your code.">Backbone.js + RequireJS</a></li><li><a href="examples/knockoutjs_require" data-source="http://knockoutjs.com" data-content="This project is an adaptation of /examples/knockoutjs with require.js.">KnockoutJS + RequireJS</a></li><li class="routing"><a href="examples/angularjs_require" data-source="http://angularjs.org" data-content="What HTML would have been had it been designed for web apps. This is an example of using it with AMD modules.">AngularJS + RequireJS</a></li><li class="routing"><a href="examples/canjs_require" data-source="http://canjs.us" data-content="CanJS is a client-side, JavaScript framework that makes building rich web applications easy. The AMD version lets you use the framework in a fully modular fashion and will only what you actually need.">CanJS + RequireJS</a></li><li class="routing"><a href="examples/thorax_lumbar/public" data-source="http://walmartlabs.github.com/lumbar" data-content="An opinionated, battle tested Backbone + Handlebars framework to build large scale web applications. This implementation uses Lumbar, a route based module loader.">Thorax + Lumbar</a></li><li class="routing"><a href="examples/somajs_require" data-source="http://somajs.github.com/somajs" data-content="soma.js is a framework created to build scalable and maintainable javascript applications.">soma.js + RequireJS</a></li><li class="routing"><a href="examples/durandal" data-source="http://durandaljs.com/" data-content="Single Page Apps Done Right">Durandal</a></li><li class="routing"><a href="examples/lavaca_require" data-source="http://getlavaca.com" data-content="A curated collection of tools for building mobile web applications.">Lavaca + RequireJS</a></li><li><a href="examples/cujo/index.html" data-source="http://cujojs.com" data-content="cujoJS is an architectural framework for building highly modular, scalable, maintainable applications in Javascript. It provides architectural plumbing, such as modules (AMD and CommonJS), declarative application composition, declarative connections, and aspect oriented programming.">cujoJS</a></li><li class="routing"><a href="examples/sammyjs" data-source="http://sammyjs.org" data-content="Sammy.js is a tiny JavaScript framework developed to ease the pain and provide a basic structure for developing JavaScript applications.">Sammy.js</a></li><li><a href="examples/somajs" data-source="http://somajs.github.com/somajs" data-content="soma.js is a framework created to build scalable and maintainable javascript applications.">soma.js</a></li><li><a href="examples/duel/www" data-source="https://bitbucket.org/mckamey/duel/wiki/Home" data-content="DUEL is a dual-side templating engine using HTML for layout and 100% pure JavaScript as the binding language. The same views may be executed both directly in the browser (client-side template) and on the server (server-side template).">DUEL</a></li><li class="routing"><a href="examples/kendo" data-source="http://www.kendoui.com/" data-content="Kendo UI is a comprehensive HTML5, JavaScript framework for modern web and mobile app development">Kendo UI</a></li><li class="routing"><a href="examples/puremvc" data-source="http://puremvc.github.com" data-content="PureMVC is a lightweight framework for creating applications based upon the classic Model-View-Controller design meta-pattern.">PureMVC</a></li><li><a href="examples/olives" data-source="https://github.com/flams/olives" data-content="Olives is a JS MVC framework that helps you create realtime UIs. It includes a set of AMD/CommonJS modules that are easily extensive, a high level of abstraction to reduce boilerplate and is based on socket.io, to provide a powerful means to communicate with node.js.">Olives</a></li><li><a href="examples/dijon" data-source="https://github.com/creynders/dijon-framework" data-content="Dijon is an IOC and DI micro-framework for Javascript. Originally it was meant to be a port of Robotlegs, but deviated to something quite different. It remains however heavily inspired by Robotlegs, and more specifically Swiftsuspenders.">Dijon</a></li><li class="routing"><a href="examples/rappidjs" data-source="http://www.rappidjs.com" data-content="rAppid.js is a declarative JavaScript framework for rapid web application development. It supports dependency loading, Model-View binding, View-Model binding, dependency injection and i18n.">rAppid.js</a></li><li><a href="examples/extjs_deftjs" data-source="http://deftjs.org/" data-content="Essential extensions for enterprise web and mobile application development with Ext JS and Sencha Touch">DeftJS + ExtJS</a></li><li class="routing"><a href="examples/ariatemplates" data-source="http://ariatemplates.com/" data-content="Aria Templates has been designed for web apps that are used 8+ hours a day, and that need to display and process high amount of data with a minimum of bandwidth consumption.">Aria Templates</a></li><li class="routing"><a href="examples/enyo_backbone" data-source="http://enyojs.com/" data-content="Enyo is a simple but powerful encapsulation model, which helps you factor application functionality into self-contained building blocks that are easy to reuse and maintain.">Enyo +<br>Backbone.js</a></li><li class="routing"><a href="examples/angularjs-perf" data-source="http://angularjs.org" data-content="What HTML would have been had it been designed for web apps. A version with several performance optimizations.">AngularJS <br>(optimized)</a></li><li class="routing"><a href="examples/sapui5" data-source="http://scn.sap.com/community/developer-center/front-end" data-content="SAPUI5 is SAP's HTML5-based UI technology that allows you to build rich, interactive Web applications.">SAPUI5</a></li><li class="routing"><a href="examples/exoskeleton" data-source="http://exosjs.com/" data-content="A faster and leaner Backbone for your HTML5 apps.">Exoskeleton</a></li><li class="routing"><a href="examples/atmajs" data-source="http://atmajs.com/" data-content="HMVC and the component-based architecture for building client, server or hybrid applications">Atma.js</a></li><li><a href="examples/ractive" data-source="http://ractivejs.org" data-content="Ractive.js is a next-generation DOM manipulation library, optimised for developer sanity.">Ractive.js</a></li><li><a href="examples/componentjs" data-source="http://componentjs.com" data-content="ComponentJS is a stand-alone MPL-licensed Open Source library for JavaScript, providing a powerful run-time Component System for hierarchically structuring the User-Interface (UI) dialogs of complex HTML5-based Rich Clients (aka Single-Page-Apps) — under maximum applied Separation of Concerns (SoC) architecture principle, through optional Model, View and Controller component roles, with sophisticated hierarchical Event, Service, Hook, Model, Socket and Property mechanisms, and fully independent and agnostic of the particular UI widget toolkit.">ComponentJS</a></li><li class="routing"><a href="examples/react-alt" data-source="http://alt.js.org" data-content="This React example integrates Alt for an example of an application following the Flux architecture.">React + Alt</a></li><li><a href="examples/react-backbone" data-source="http://facebook.github.io/react/" data-content="This React example integrates Backbone for its model and router. It is a showcase of third-party library integration for developers wishing to use React together with a different JavaScript framework.">React + <br>Backbone.js</a></li><li><a href="examples/aurelia" data-source="http://aurelia.io/" data-content="Aurelia is a next generation JavaScript client framework that leverages simple conventions to empower your creativity">Aurelia</a></li><li class="routing"><a href="examples/foam" data-source="http://foam-framework.github.io/foam/" data-content="FOAM is a deeply MVC Javascript metaprogramming framework being developed by a team at Google.">FOAM</a></li><li class="routing"><a href="examples/webrx" data-source="http://webrxjs.org" data-content="WebRx is a Javascript MVVM-Framework that combines functional-reactive programming with declarative Data-Binding, Templating and Client-Side Routing. The framework is built on top of ReactiveX for Javascript (RxJs) which is a powerful set of libraries for processing and querying asynchronous data-streams that can originate from diverse sources such as Http-Requests, Input-Events, Timers and much more.">WebRx</a></li><li><a href="examples/angular2" data-source="http://angular.io" data-content="Angular is a development platform for building mobile and desktop web applications">Angular 2.0</a></li><li class="routing"><a href="examples/riotjs" data-source="http://riotjs.com/" data-content="Riot.js is a React-like user interface micro-library.">Riot</a></li><li class="routing"><a href="examples/jsblocks" data-source="http://jsblocks.com/" data-content="From simple user interfaces to complex single-page applications using faster, server-side rendered and easy to learn framework.">JSBlocks</a></li></ul></div></iron-pages><ul class="legend"><li><span class="label">R</span> = App also demonstrates routing</li></ul><hr><h2>Real-time</h2><ul class="applist"><li class="routing"><a href="http://todomvc-socketstream.herokuapp.com" data-source="http://www.socketstream.org" data-content="SocketStream is a fast, modular Node.js web framework dedicated to building realtime single-page apps">SocketStream</a></li><li class="routing"><a href="examples/firebase-angular" data-source="https://www.firebase.com" data-content="Firebase is a scalable realtime backend that lets you build apps without managing servers. Firebase persists and updates JSON data in realtime and is best used in combination with a JavaScript MV* framework such as AngularJS or Backbone.">Firebase + AngularJS</a></li></ul><hr><h2>Node.js</h2><ul class="applist"><li><a href="http://gcloud-todos.appspot.com" data-source="http://googlecloudplatform.github.io/gcloud-node/" data-content="An Express backend implementation for the AngularJS front end using Google Cloud Client Library for Node.js.">Express + gcloud-node</a></li></ul><hr><h2>Compare these to a non-framework implementation</h2><ul class="applist"><li class="routing"><a href="examples/vanillajs" data-source="https://developer.mozilla.org/en/JavaScript" data-content="You know JavaScript right? :P">Vanilla JS</a></li><li class="routing"><a href="examples/jquery" data-source="http://jquery.com" data-content="jQuery is a fast and concise JavaScript Library that simplifies HTML document traversing, event handling, animating, and Ajax interactions for rapid web development. jQuery is designed to change the way that you write JavaScript.">jQuery</a></li></ul></div></div><hr><div class="row"><div class="col-md-6 quotes"><blockquote class="quote speech-bubble"><p></p><footer><img width="40" height="40" alt=""><a></a></footer></blockquote></div><div class="col-md-6"><img class="screenshot" src="site-assets/screenshot.png" width="558" height="246" alt="Todo app screenshot"></div></div><hr><div class="row"><div class="col-md-4"><h2>New in 1.3</h2><ul class="whats-new"><li>We now have 64 applications.
</p><ul class="js-app-list-inner applist js"><li class="routing"><a href="examples/thorax" data-source="http://thoraxjs.org" data-content="An opinionated, battle tested Backbone + Handlebars framework to build large scale web applications.">Thorax</a></li><li class="routing"><a href="examples/chaplin-brunch/public" data-source="http://chaplinjs.org" data-content="Chaplin is an architecture for JavaScript applications using the Backbone.js library. Chaplin addresses Backbone’s limitations by providing a lightweight and flexible structure that features well-proven design patterns and best practises.">Chaplin + Brunch</a></li><li class="routing"><a href="examples/backbone_require" data-source="http://requirejs.org" data-content="RequireJS is a JavaScript file and module loader. It is optimized for in-browser use, but it can be used in other JavaScript environments, like Rhino and Node. Using a modular script loader like RequireJS will improve the speed and quality of your code.">Backbone.js + RequireJS</a></li><li><a href="examples/knockoutjs_require" data-source="http://knockoutjs.com" data-content="This project is an adaptation of /examples/knockoutjs with require.js.">KnockoutJS + RequireJS</a></li><li class="routing"><a href="examples/angularjs_require" data-source="http://angularjs.org" data-content="What HTML would have been had it been designed for web apps. This is an example of using it with AMD modules.">AngularJS + RequireJS</a></li><li class="routing"><a href="examples/canjs_require" data-source="http://canjs.us" data-content="CanJS is a client-side, JavaScript framework that makes building rich web applications easy. The AMD version lets you use the framework in a fully modular fashion and will only what you actually need.">CanJS + RequireJS</a></li><li class="routing"><a href="examples/thorax_lumbar/public" data-source="http://walmartlabs.github.com/lumbar" data-content="An opinionated, battle tested Backbone + Handlebars framework to build large scale web applications. This implementation uses Lumbar, a route based module loader.">Thorax + Lumbar</a></li><li class="routing"><a href="examples/somajs_require" data-source="http://somajs.github.com/somajs" data-content="soma.js is a framework created to build scalable and maintainable javascript applications.">soma.js + RequireJS</a></li><li class="routing"><a href="examples/durandal" data-source="http://durandaljs.com/" data-content="Single Page Apps Done Right">Durandal</a></li><li class="routing"><a href="examples/lavaca_require" data-source="http://getlavaca.com" data-content="A curated collection of tools for building mobile web applications.">Lavaca + RequireJS</a></li><li><a href="examples/cujo/index.html" data-source="http://cujojs.com" data-content="cujoJS is an architectural framework for building highly modular, scalable, maintainable applications in Javascript. It provides architectural plumbing, such as modules (AMD and CommonJS), declarative application composition, declarative connections, and aspect oriented programming.">cujoJS</a></li><li class="routing"><a href="examples/sammyjs" data-source="http://sammyjs.org" data-content="Sammy.js is a tiny JavaScript framework developed to ease the pain and provide a basic structure for developing JavaScript applications.">Sammy.js</a></li><li><a href="examples/somajs" data-source="http://somajs.github.com/somajs" data-content="soma.js is a framework created to build scalable and maintainable javascript applications.">soma.js</a></li><li><a href="examples/duel/www" data-source="https://bitbucket.org/mckamey/duel/wiki/Home" data-content="DUEL is a dual-side templating engine using HTML for layout and 100% pure JavaScript as the binding language. The same views may be executed both directly in the browser (client-side template) and on the server (server-side template).">DUEL</a></li><li class="routing"><a href="examples/kendo" data-source="http://www.kendoui.com/" data-content="Kendo UI is a comprehensive HTML5, JavaScript framework for modern web and mobile app development">Kendo UI</a></li><li class="routing"><a href="examples/puremvc" data-source="http://puremvc.github.com" data-content="PureMVC is a lightweight framework for creating applications based upon the classic Model-View-Controller design meta-pattern.">PureMVC</a></li><li><a href="examples/olives" data-source="https://github.com/flams/olives" data-content="Olives is a JS MVC framework that helps you create realtime UIs. It includes a set of AMD/CommonJS modules that are easily extensive, a high level of abstraction to reduce boilerplate and is based on socket.io, to provide a powerful means to communicate with node.js.">Olives</a></li><li><a href="examples/dijon" data-source="https://github.com/creynders/dijon-framework" data-content="Dijon is an IOC and DI micro-framework for Javascript. Originally it was meant to be a port of Robotlegs, but deviated to something quite different. It remains however heavily inspired by Robotlegs, and more specifically Swiftsuspenders.">Dijon</a></li><li class="routing"><a href="examples/rappidjs" data-source="http://www.rappidjs.com" data-content="rAppid.js is a declarative JavaScript framework for rapid web application development. It supports dependency loading, Model-View binding, View-Model binding, dependency injection and i18n.">rAppid.js</a></li><li><a href="examples/extjs_deftjs" data-source="http://deftjs.org/" data-content="Essential extensions for enterprise web and mobile application development with Ext JS and Sencha Touch">DeftJS + ExtJS</a></li><li class="routing"><a href="examples/ariatemplates" data-source="http://ariatemplates.com/" data-content="Aria Templates has been designed for web apps that are used 8+ hours a day, and that need to display and process high amount of data with a minimum of bandwidth consumption.">Aria Templates</a></li><li class="routing"><a href="examples/enyo_backbone" data-source="http://enyojs.com/" data-content="Enyo is a simple but powerful encapsulation model, which helps you factor application functionality into self-contained building blocks that are easy to reuse and maintain.">Enyo +<br>Backbone.js</a></li><li class="routing"><a href="examples/angularjs-perf" data-source="http://angularjs.org" data-content="What HTML would have been had it been designed for web apps. A version with several performance optimizations.">AngularJS <br>(optimized)</a></li><li class="routing"><a href="examples/sapui5" data-source="http://scn.sap.com/community/developer-center/front-end" data-content="SAPUI5 is SAP's HTML5-based UI technology that allows you to build rich, interactive Web applications.">SAPUI5</a></li><li class="routing"><a href="examples/exoskeleton" data-source="http://exosjs.com/" data-content="A faster and leaner Backbone for your HTML5 apps.">Exoskeleton</a></li><li class="routing"><a href="examples/atmajs" data-source="http://atmajs.com/" data-content="HMVC and the component-based architecture for building client, server or hybrid applications">Atma.js</a></li><li><a href="examples/ractive" data-source="http://ractivejs.org" data-content="Ractive.js is a next-generation DOM manipulation library, optimised for developer sanity.">Ractive.js</a></li><li><a href="examples/componentjs" data-source="http://componentjs.com" data-content="ComponentJS is a stand-alone MPL-licensed Open Source library for JavaScript, providing a powerful run-time Component System for hierarchically structuring the User-Interface (UI) dialogs of complex HTML5-based Rich Clients (aka Single-Page-Apps) — under maximum applied Separation of Concerns (SoC) architecture principle, through optional Model, View and Controller component roles, with sophisticated hierarchical Event, Service, Hook, Model, Socket and Property mechanisms, and fully independent and agnostic of the particular UI widget toolkit.">ComponentJS</a></li><li class="routing"><a href="examples/react-alt" data-source="http://alt.js.org" data-content="This React example integrates Alt for an example of an application following the Flux architecture.">React + Alt</a></li><li><a href="examples/react-backbone" data-source="http://facebook.github.io/react/" data-content="This React example integrates Backbone for its model and router. It is a showcase of third-party library integration for developers wishing to use React together with a different JavaScript framework.">React + <br>Backbone.js</a></li><li><a href="examples/aurelia" data-source="http://aurelia.io/" data-content="Aurelia is a next generation JavaScript client framework that leverages simple conventions to empower your creativity">Aurelia</a></li><li class="routing"><a href="examples/foam" data-source="http://foam-framework.github.io/foam/" data-content="FOAM is a deeply MVC Javascript metaprogramming framework being developed by a team at Google.">FOAM</a></li><li class="routing"><a href="examples/webrx" data-source="http://webrxjs.org" data-content="WebRx is a Javascript MVVM-Framework that combines functional-reactive programming with declarative Data-Binding, Templating and Client-Side Routing. The framework is built on top of ReactiveX for Javascript (RxJs) which is a powerful set of libraries for processing and querying asynchronous data-streams that can originate from diverse sources such as Http-Requests, Input-Events, Timers and much more.">WebRx</a></li><li><a href="examples/angular2" data-source="http://angular.io" data-content="Angular is a development platform for building mobile and desktop web applications">Angular 2.0</a></li><li class="routing"><a href="examples/riotjs" data-source="http://riotjs.com/" data-content="Riot.js is a React-like user interface micro-library.">Riot</a></li><li class="routing"><a href="examples/jsblocks" data-source="http://jsblocks.com/" data-content="From simple user interfaces to complex single-page applications using faster, server-side rendered and easy to learn framework.">JSBlocks</a></li></ul></div></iron-pages><ul class="legend"><li><span class="label">R</span> = App also demonstrates routing</li></ul><hr><h2>Real-time</h2><ul class="applist"><li class="routing"><a href="http://todomvc-socketstream.herokuapp.com" data-source="http://www.socketstream.org" data-content="SocketStream is a fast, modular Node.js web framework dedicated to building realtime single-page apps">SocketStream</a></li><li class="routing"><a href="examples/firebase-angular" data-source="https://www.firebase.com" data-content="Firebase is a scalable realtime backend that lets you build apps without managing servers. Firebase persists and updates JSON data in realtime and is best used in combination with a JavaScript MV* framework such as AngularJS or Backbone.">Firebase + AngularJS</a></li></ul><hr><h2>Node.js</h2><ul class="applist"><li><a href="http://gcloud-todos.appspot.com" data-source="http://googlecloudplatform.github.io/gcloud-node/" data-content="An Express backend implementation for the AngularJS front end using Google Cloud Client Library for Node.js.">Express + gcloud-node</a></li></ul><hr><h2>Compare these to a non-framework implementation</h2><ul class="applist"><li class="routing"><a href="examples/vanillajs" data-source="https://developer.mozilla.org/en/JavaScript" data-content="You know JavaScript right? :P">Vanilla JS</a></li><li class="routing"><a href="examples/vanilla-es6" data-source="https://developer.mozilla.org/en/JavaScript" data-content="Just ECMAScript 6 and DOM APIs. Compiled with Google Closure Compiler.">Vanilla ES6</a></li><li class="routing"><a href="examples/jquery" data-source="http://jquery.com" data-content="jQuery is a fast and concise JavaScript Library that simplifies HTML document traversing, event handling, animating, and Ajax interactions for rapid web development. jQuery is designed to change the way that you write JavaScript.">jQuery</a></li></ul></div></div><hr><div class="row"><div class="col-md-6 quotes"><blockquote class="quote speech-bubble"><p></p><footer><img width="40" height="40" alt=""><a></a></footer></blockquote></div><div class="col-md-6"><img class="screenshot" src="site-assets/screenshot.png" width="558" height="246" alt="Todo app screenshot"></div></div><hr><div class="row"><div class="col-md-4"><h2>New in 1.3</h2><ul class="whats-new"><li>We now have 64 applications.
<label class="link" for="news-expander">New since 1.3 ▼</label><input type="checkbox" id="news-expander"><ul class="collapsed" id="new-apps"><li><a href="examples/angular-dart/web">AngularDart</a></li><li><a href="examples/durandal">Durandel</a></li><li><a href="examples/atmajs">Atma.js</a></li><li><a href="examples/exoskeleton">Exoskeleton</a></li><li><a href="examples/componentjs">ComponentJS</a></li><li><a href="examples/binding-scala">Binding.scala</a></li></ul></li><li>Updates have been made to apps including Vanilla, Angular, React, Backbone, Ember, jQuery &amp; many more.</li><li>The knockoutjs_classBindingProvider application has been removed.</li></ul></div><div class="col-md-4"><h2>Selecting a Framework</h2><p>Once you've downloaded the latest release and played around with the apps, you'll want to decide on a specific framework to try out.</p><p>Study the syntax required for defining models, views and (where applicable) controllers and classes in the frameworks you're interested in and try your hand at editing the code to see how it feels using it first-hand.</p><p>Please ensure that if you're happy with this, you do spend more time investigating the framework (including reading the official docs, the source and its complete feature list). There's often a lot more to a framework than what we present in our examples.</p></div><div class="col-md-4"><h2>Getting Involved</h2><p>Is there a bug we haven't fixed or an MV* framework you feel would benefit from being included in TodoMVC?</p><p>If so, feel free to fork the repo, read our <a href="https://github.com/tastejs/todomvc/wiki">contribution guidelines</a>, and submit a pull request &mdash; we'll be happy to review it for inclusion.</p><p>Make sure you use the <a href="https://github.com/tastejs/todomvc-app-template">template</a> as a starting point and read the <a href="https://github.com/tastejs/todomvc/blob/master/app-spec.md">app specification</a>.</p><p><a class="zocial small ltgray" href="https://github.com/tastejs/todomvc/wiki">Submit Pull Request &raquo;</a></p></div></div><hr><footer class="credit"><p>Brought to you by
<a href="https://github.com/addyosmani"><img src="http://gravatar.com/avatar/96270e4c3e5e9806cf7245475c00b275?s=80" width="40" height="40" alt="Addy Osmani">Addy</a><a href="https://github.com/sindresorhus"><img src="http://gravatar.com/avatar/d36a92237c75c5337c17b60d90686bf9.png?s=80" width="40" height="40" alt="Sindre Sorhus">Sindre</a><a href="https://github.com/passy"><img src="http://gravatar.com/avatar/be451fcdbf0e5ff07f23ed16cb5c90a3.png?s=80" width="40" height="40" alt="Pascal Hartig">Pascal</a><a href="https://github.com/stephenplusplus"><img src="http://gravatar.com/avatar/098cfe2d360e77c3229f2cd5298354c4?s=80" width="40" height="40" alt="Stephen Sawchuk">Stephen</a><a href="https://github.com/colineberhardt"><img src="http://gravatar.com/avatar/73bba00b41ff1c9ecc3ee29487bace7d?s=80" width="40" height="40" alt="Colin Eberhardt">Colin</a><a href="https://github.com/arthurvr"><img src="https://avatars1.githubusercontent.com/u/6025224?v=3&s=80" width="40" height="40" alt="Arthur Verschaeve">Arthur</a><a href="https://github.com/samccone"><img src="https://avatars0.githubusercontent.com/u/883126?v=3&s=80" width="40" height="40" alt="Sam Saccone">Sam</a></p></footer></div><script src="site-assets/main.min.js"></script><script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.async=true;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs)}}(document,"script","twitter-wjs");</script><script>(function(){var po=document.createElement("script");po.type="text/javascript";po.async=true;po.src="https://apis.google.com/js/plusone.js";var s=document.getElementsByTagName("script")[0];s.parentNode.insertBefore(po,s)})();</script><script>var _gaq=[["_setAccount","UA-31081062-1"],["_trackPageview"]];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src="//www.google-analytics.com/ga.js";s.parentNode.insertBefore(g,s)})(document,"script");</script></body></html>
......@@ -1123,6 +1123,15 @@
"url": "examples/vanillajs"
}]
},
"es6": {
"name": "ECMAScript 6",
"description": "The ECMAScript 6 (ES2015) standard was ratified in 2015 following years of work standardizing improvements to ECMAScript 3. The committee introduced a wide variety of improvements such as arrow functions, const declarations, and native Promises.",
"homepage": "developer.mozilla.org/en-US/docs/JavaScript",
"examples": [{
"name": "Vanilla ES6 Example",
"url": "examples/vanilla-es6"
}]
},
"js_of_ocaml": {
"name": "js_of_ocaml",
"description": "Js_of_ocaml is a compiler of OCaml bytecode to Javascript. It makes it possible to run Ocaml programs in a Web browser.",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment