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. {-| TodoMVC implemented in Elm, using plain HTML and CSS for rendering.
This application is broken up into four distinct parts: This application is broken up into four distinct parts:
...@@ -6,40 +7,41 @@ 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 1. Model - a full description of the application as data
2. Update - a way to update the model based on user actions 2. Update - a way to update the model based on user actions
3. View - a way to visualize our model with HTML 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 This program is not particularly large, so definitely see the following
document for notes on structuring more complex GUIs with Elm: 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 Dom
import Html.Attributes (..)
import Html.Events (..)
import Html.Lazy (lazy, lazy2)
import List
import LocalChannel as LC
import Maybe
import Signal
import String
import Task 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 -- MODEL
-- The full application state of our todo app. -- The full application state of our todo app.
type alias Model = type alias Model =
{ tasks : List Task.Model { tasks : List Todo.Task.Model
, field : String , field : String
, uid : Int , uid : Int
, visibility : String , visibility : String
} }
type alias Flags =
Maybe Model
emptyModel : Model emptyModel : Model
emptyModel = emptyModel =
{ tasks = [] { tasks = []
...@@ -49,66 +51,119 @@ emptyModel = ...@@ -49,66 +51,119 @@ emptyModel =
} }
-- UPDATE
-- UPDATE
-- A description of the kinds of actions that can be performed on the model of -- 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 -- our application. See the following post for more info on this pattern and
-- some alternatives: http://elm-lang.org/learn/Architecture.elm -- some alternatives: http://guide.elm-lang.org/architecture/
type Action
type Msg
= NoOp = NoOp
| UpdateField String | UpdateField String
| Add | Add
| UpdateTask (Int, Task.Action) | UpdateTask ( Int, Todo.Task.Msg )
| DeleteComplete | DeleteComplete
| CheckAll Bool | CheckAll Bool
| ChangeVisibility String | ChangeVisibility String
-- How we update our Model on any given Action
update : Action -> Model -> Model -- How we update our Model on any given Message
update action model =
case action of
NoOp -> model update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case Debug.log "MESSAGE: " msg of
NoOp ->
( model, Cmd.none )
UpdateField str -> UpdateField str ->
{ model | field <- str } let
newModel =
{ model | field = str }
in
( newModel, save model )
Add -> Add ->
let description = String.trim model.field in let
if String.isEmpty description then model else description =
{ model | String.trim model.field
uid <- model.uid + 1,
field <- "", newModel =
tasks <- model.tasks ++ [Task.init description model.uid] if String.isEmpty description then
model
else
{ model
| uid = model.uid + 1
, field = ""
, tasks = model.tasks ++ [ Todo.Task.init description model.uid ]
} }
UpdateTask (id, taskAction) ->
let updateTask t =
if t.id == id then Task.update taskAction t else Just t
in in
{ model | tasks <- List.filterMap updateTask model.tasks } ( 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 -> DeleteComplete ->
{ model | tasks <- List.filter (not << .completed) model.tasks } let
newModel =
{ model
| tasks = List.filter (not << .completed) model.tasks
}
in
( newModel, save newModel )
CheckAll bool -> CheckAll bool ->
let updateTask t = { t | completed <- bool } let
in { model | tasks <- List.map updateTask model.tasks } updateTask t =
{ t | completed = bool }
newModel =
{ model | tasks = List.map updateTask model.tasks }
in
( newModel, save newModel )
ChangeVisibility visibility -> ChangeVisibility visibility ->
{ model | visibility <- visibility } let
newModel =
{ model | visibility = visibility }
in
( newModel, save model )
focusTask : String -> Cmd Msg
focusTask elementId =
Task.perform (\_ -> NoOp) (\_ -> NoOp) (Dom.focus elementId)
-- VIEW -- VIEW
view : Model -> Html
view : Model -> Html Msg
view model = view model =
div div
[ class "todomvc-wrapper" [ class "todomvc-wrapper"
, style [ ("visibility", "hidden") ] , style [ ( "visibility", "hidden" ) ]
] ]
[ section [ section
[ id "todoapp" ] [ class "todoapp" ]
[ lazy taskEntry model.field [ lazy taskEntry model.field
, lazy2 taskList model.visibility model.tasks , lazy2 taskList model.visibility model.tasks
, lazy2 controls model.visibility model.tasks , lazy2 controls model.visibility model.tasks
...@@ -116,72 +171,108 @@ view model = ...@@ -116,72 +171,108 @@ view model =
, infoFooter , infoFooter
] ]
taskEntry : String -> Html
taskEntry : String -> Html Msg
taskEntry task = taskEntry task =
header header
[ id "header" ] [ class "header" ]
[ h1 [] [ text "todos" ] [ h1 [] [ text "todos" ]
, input , input
[ id "new-todo" [ class "new-todo"
, placeholder "What needs to be done?" , placeholder "What needs to be done?"
, autofocus True , autofocus True
, value task , value task
, name "newTodo" , name "newTodo"
, on "input" targetValue (Signal.send actions << UpdateField) , onInput UpdateField
, Task.onFinish (Signal.send actions Add) (Signal.send actions NoOp) , Todo.Task.onFinish Add NoOp
] ]
[] []
] ]
taskList : String -> List Task.Model -> Html
taskList : String -> List Todo.Task.Model -> Html Msg
taskList visibility tasks = taskList visibility tasks =
let isVisible todo = let
isVisible todo =
case visibility of case visibility of
"Completed" -> todo.completed "Completed" ->
"Active" -> not todo.completed todo.completed
"All" -> True
allCompleted = List.all .completed tasks "Active" ->
not todo.completed
cssVisibility = if List.isEmpty tasks then "hidden" else "visible" -- "All"
_ ->
True
allCompleted =
List.all .completed tasks
cssVisibility =
if List.isEmpty tasks then
"hidden"
else
"visible"
in in
section section
[ id "main" [ class "main"
, style [ ("visibility", cssVisibility) ] , style [ ( "visibility", cssVisibility ) ]
] ]
[ input [ input
[ id "toggle-all" [ class "toggle-all"
, type' "checkbox" , type' "checkbox"
, name "toggle" , name "toggle"
, checked allCompleted , checked allCompleted
, onClick (Signal.send actions (CheckAll (not allCompleted))) , onClick (CheckAll (not allCompleted))
] ]
[] []
, label , label
[ for "toggle-all" ] [ for "toggle-all" ]
[ text "Mark all as complete" ] [ text "Mark all as complete" ]
, ul , ul
[ id "todo-list" ] [ class "todo-list" ]
(List.map (Task.view taskActions) (List.filter isVisible tasks)) (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 Task.Model -> Html
controls : String -> List Todo.Task.Model -> Html Msg
controls visibility tasks = controls visibility tasks =
let tasksCompleted = List.length (List.filter .completed tasks) let
tasksLeft = List.length tasks - tasksCompleted tasksCompleted =
item_ = if tasksLeft == 1 then " item" else " items" List.length (List.filter .completed tasks)
tasksLeft =
List.length tasks - tasksCompleted
item_ =
if tasksLeft == 1 then
" item"
else
" items"
in in
footer footer
[ id "footer" [ class "footer"
, hidden (List.isEmpty tasks) , hidden (List.isEmpty tasks)
] ]
[ span [ span
[ id "todo-count" ] [ class "todo-count" ]
[ strong [] [ text (toString tasksLeft) ] [ strong [] [ text (toString tasksLeft) ]
, text (item_ ++ " left") , text (item_ ++ " left")
] ]
, ul , ul
[ id "filters" ] [ class "filters" ]
[ visibilitySwap "#/" "All" visibility [ visibilitySwap "#/" "All" visibility
, text " " , text " "
, visibilitySwap "#/active" "Active" visibility , visibilitySwap "#/active" "Active" visibility
...@@ -190,80 +281,110 @@ controls visibility tasks = ...@@ -190,80 +281,110 @@ controls visibility tasks =
] ]
, button , button
[ class "clear-completed" [ class "clear-completed"
, id "clear-completed"
, hidden (tasksCompleted == 0) , hidden (tasksCompleted == 0)
, onClick (Signal.send actions DeleteComplete) , onClick DeleteComplete
] ]
[ text ("Clear completed (" ++ toString tasksCompleted ++ ")") ] [ text ("Clear completed (" ++ toString tasksCompleted ++ ")") ]
] ]
visibilitySwap : String -> String -> String -> Html
visibilitySwap : String -> String -> String -> Html Msg
visibilitySwap uri visibility actualVisibility = visibilitySwap uri visibility actualVisibility =
let className = if visibility == actualVisibility then "selected" else "" in let
className =
if visibility == actualVisibility then
"selected"
else
""
in
li li
[ onClick (Signal.send actions (ChangeVisibility visibility)) ] [ onClick (ChangeVisibility visibility) ]
[ a [ class className, href uri ] [ text visibility ] ] [ a [ class className, href uri ] [ text visibility ] ]
infoFooter : Html
infoFooter : Html msg
infoFooter = infoFooter =
footer [ id "info" ] footer
[ class "info" ]
[ p [] [ text "Double-click to edit a todo" ] [ p [] [ text "Double-click to edit a todo" ]
, p [] [ text "Written by " , p []
[ text "Written by "
, a [ href "https://github.com/evancz" ] [ text "Evan Czaplicki" ] , a [ href "https://github.com/evancz" ] [ text "Evan Czaplicki" ]
] ]
, p [] [ text "Part of " , p []
[ text "Part of "
, a [ href "http://todomvc.com" ] [ text "TodoMVC" ] , a [ href "http://todomvc.com" ] [ text "TodoMVC" ]
] ]
] ]
-- SIGNALS
-- wire the entire application together -- wire the entire application together
main : Signal Html
main : Program Flags
main = 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 urlParser : Parser (Maybe String)
port save = model urlParser =
Navigation.makeParser (fromUrl << .hash)
-- routing
port route : Signal String
-- actions from user input {-| The URL is turned into a Maybe value. If the URL is valid, we just update
actions : Signal.Channel Action 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
Nothing ->
taskActions : LC.LocalChannel (Int, Task.Action) update (ChangeVisibility "All") model
port focus : Signal (Maybe Int) init : Flags -> Maybe String -> ( Model, Cmd Msg )
port focus = init flags url =
let toSelector action = urlUpdate url (Maybe.withDefault emptyModel flags)
case action of
UpdateTask (id, Task.Focus) -> Just id
_ -> Nothing
in -- 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 @@ ...@@ -8,8 +8,11 @@
], ],
"exposed-modules": [], "exposed-modules": [],
"dependencies": { "dependencies": {
"elm-lang/core": "1.0.0 <= v < 2.0.0", "elm-community/string-extra": "1.0.2 <= v < 2.0.0",
"evancz/elm-html": "1.0.0 <= v < 2.0.0", "elm-lang/core": "4.0.5 <= v < 5.0.0",
"evancz/local-channel": "1.0.0 <= v < 2.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 @@ ...@@ -16,47 +16,17 @@
</head> </head>
<body> <body>
<script src="elm.js"></script> <script src="build/elm.js"></script>
<script> <script>
(function () { (function () {
var result = localStorage.getItem('elm-todo-model'); var result = localStorage.getItem('elm-todo-model');
var savedModel = result ? JSON.parse(result) : null; var savedModel = result ? JSON.parse(result) : null;
var todomvc = Elm.fullscreen(Elm.Todo, { var todomvc = Elm.Todo.fullscreen(savedModel);
savedModel: savedModel,
route: getRoute()
});
todomvc.ports.save.subscribe(function (model) { todomvc.ports.save.subscribe(function (model) {
localStorage.setItem('elm-todo-model', JSON.stringify(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>
<script async src="node_modules/todomvc-common/base.js"></script> <script async src="node_modules/todomvc-common/base.js"></script>
......
...@@ -15,12 +15,9 @@ button { ...@@ -15,12 +15,9 @@ button {
font-weight: inherit; font-weight: inherit;
color: inherit; color: inherit;
-webkit-appearance: none; -webkit-appearance: none;
-ms-appearance: none;
appearance: none; appearance: none;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
-ms-font-smoothing: antialiased;
font-smoothing: antialiased;
} }
body { body {
...@@ -32,22 +29,19 @@ body { ...@@ -32,22 +29,19 @@ body {
max-width: 550px; max-width: 550px;
margin: 0 auto; margin: 0 auto;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
-ms-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300; font-weight: 300;
} }
button, :focus {
input[type="checkbox"] { outline: 0;
outline: none;
} }
.hidden { .hidden {
display: none; display: none;
} }
#todoapp { .todoapp {
background: #fff; background: #fff;
margin: 130px 0 40px 0; margin: 130px 0 40px 0;
position: relative; position: relative;
...@@ -55,25 +49,25 @@ input[type="checkbox"] { ...@@ -55,25 +49,25 @@ input[type="checkbox"] {
0 25px 50px 0 rgba(0, 0, 0, 0.1); 0 25px 50px 0 rgba(0, 0, 0, 0.1);
} }
#todoapp input::-webkit-input-placeholder { .todoapp input::-webkit-input-placeholder {
font-style: italic; font-style: italic;
font-weight: 300; font-weight: 300;
color: #e6e6e6; color: #e6e6e6;
} }
#todoapp input::-moz-placeholder { .todoapp input::-moz-placeholder {
font-style: italic; font-style: italic;
font-weight: 300; font-weight: 300;
color: #e6e6e6; color: #e6e6e6;
} }
#todoapp input::input-placeholder { .todoapp input::input-placeholder {
font-style: italic; font-style: italic;
font-weight: 300; font-weight: 300;
color: #e6e6e6; color: #e6e6e6;
} }
#todoapp h1 { .todoapp h1 {
position: absolute; position: absolute;
top: -155px; top: -155px;
width: 100%; width: 100%;
...@@ -83,11 +77,10 @@ input[type="checkbox"] { ...@@ -83,11 +77,10 @@ input[type="checkbox"] {
color: rgba(175, 47, 47, 0.15); color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility; -webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility; -moz-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
#new-todo, .new-todo,
.edit { .edit {
position: relative; position: relative;
margin: 0; margin: 0;
...@@ -97,27 +90,23 @@ input[type="checkbox"] { ...@@ -97,27 +90,23 @@ input[type="checkbox"] {
font-weight: inherit; font-weight: inherit;
line-height: 1.4em; line-height: 1.4em;
border: 0; border: 0;
outline: none;
color: inherit; color: inherit;
padding: 6px; padding: 6px;
border: 1px solid #999; border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-ms-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
-ms-font-smoothing: antialiased;
font-smoothing: antialiased;
} }
#new-todo { .new-todo {
padding: 16px 16px 16px 60px; padding: 16px 16px 16px 60px;
border: none; border: none;
background: rgba(0, 0, 0, 0.003); background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
} }
#main { .main {
position: relative; position: relative;
z-index: 2; z-index: 2;
border-top: 1px solid #e6e6e6; border-top: 1px solid #e6e6e6;
...@@ -127,7 +116,7 @@ label[for='toggle-all'] { ...@@ -127,7 +116,7 @@ label[for='toggle-all'] {
display: none; display: none;
} }
#toggle-all { .toggle-all {
position: absolute; position: absolute;
top: -55px; top: -55px;
left: -12px; left: -12px;
...@@ -137,50 +126,50 @@ label[for='toggle-all'] { ...@@ -137,50 +126,50 @@ label[for='toggle-all'] {
border: none; /* Mobile Safari */ border: none; /* Mobile Safari */
} }
#toggle-all:before { .toggle-all:before {
content: '❯'; content: '❯';
font-size: 22px; font-size: 22px;
color: #e6e6e6; color: #e6e6e6;
padding: 10px 27px 10px 27px; padding: 10px 27px 10px 27px;
} }
#toggle-all:checked:before { .toggle-all:checked:before {
color: #737373; color: #737373;
} }
#todo-list { .todo-list {
margin: 0; margin: 0;
padding: 0; padding: 0;
list-style: none; list-style: none;
} }
#todo-list li { .todo-list li {
position: relative; position: relative;
font-size: 24px; font-size: 24px;
border-bottom: 1px solid #ededed; border-bottom: 1px solid #ededed;
} }
#todo-list li:last-child { .todo-list li:last-child {
border-bottom: none; border-bottom: none;
} }
#todo-list li.editing { .todo-list li.editing {
border-bottom: none; border-bottom: none;
padding: 0; padding: 0;
} }
#todo-list li.editing .edit { .todo-list li.editing .edit {
display: block; display: block;
width: 506px; width: 506px;
padding: 13px 17px 12px 17px; padding: 12px 16px;
margin: 0 0 0 43px; margin: 0 0 0 43px;
} }
#todo-list li.editing .view { .todo-list li.editing .view {
display: none; display: none;
} }
#todo-list li .toggle { .todo-list li .toggle {
text-align: center; text-align: center;
width: 40px; width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */ /* auto, since non-WebKit browsers doesn't support input styling */
...@@ -191,20 +180,18 @@ label[for='toggle-all'] { ...@@ -191,20 +180,18 @@ label[for='toggle-all'] {
margin: auto 0; margin: auto 0;
border: none; /* Mobile Safari */ border: none; /* Mobile Safari */
-webkit-appearance: none; -webkit-appearance: none;
-ms-appearance: none;
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>'); 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>'); 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 { .todo-list li label {
white-space: pre-line;
word-break: break-all; word-break: break-all;
padding: 15px 60px 15px 15px; padding: 15px 60px 15px 15px;
margin-left: 45px; margin-left: 45px;
...@@ -213,12 +200,12 @@ label[for='toggle-all'] { ...@@ -213,12 +200,12 @@ label[for='toggle-all'] {
transition: color 0.4s; transition: color 0.4s;
} }
#todo-list li.completed label { .todo-list li.completed label {
color: #d9d9d9; color: #d9d9d9;
text-decoration: line-through; text-decoration: line-through;
} }
#todo-list li .destroy { .todo-list li .destroy {
display: none; display: none;
position: absolute; position: absolute;
top: 0; top: 0;
...@@ -233,27 +220,27 @@ label[for='toggle-all'] { ...@@ -233,27 +220,27 @@ label[for='toggle-all'] {
transition: color 0.2s ease-out; transition: color 0.2s ease-out;
} }
#todo-list li .destroy:hover { .todo-list li .destroy:hover {
color: #af5b5e; color: #af5b5e;
} }
#todo-list li .destroy:after { .todo-list li .destroy:after {
content: '×'; content: '×';
} }
#todo-list li:hover .destroy { .todo-list li:hover .destroy {
display: block; display: block;
} }
#todo-list li .edit { .todo-list li .edit {
display: none; display: none;
} }
#todo-list li.editing:last-child { .todo-list li.editing:last-child {
margin-bottom: -1px; margin-bottom: -1px;
} }
#footer { .footer {
color: #777; color: #777;
padding: 10px 15px; padding: 10px 15px;
height: 20px; height: 20px;
...@@ -261,7 +248,7 @@ label[for='toggle-all'] { ...@@ -261,7 +248,7 @@ label[for='toggle-all'] {
border-top: 1px solid #e6e6e6; border-top: 1px solid #e6e6e6;
} }
#footer:before { .footer:before {
content: ''; content: '';
position: absolute; position: absolute;
right: 0; right: 0;
...@@ -276,16 +263,16 @@ label[for='toggle-all'] { ...@@ -276,16 +263,16 @@ label[for='toggle-all'] {
0 17px 2px -6px rgba(0, 0, 0, 0.2); 0 17px 2px -6px rgba(0, 0, 0, 0.2);
} }
#todo-count { .todo-count {
float: left; float: left;
text-align: left; text-align: left;
} }
#todo-count strong { .todo-count strong {
font-weight: 300; font-weight: 300;
} }
#filters { .filters {
margin: 0; margin: 0;
padding: 0; padding: 0;
list-style: none; list-style: none;
...@@ -294,11 +281,11 @@ label[for='toggle-all'] { ...@@ -294,11 +281,11 @@ label[for='toggle-all'] {
left: 0; left: 0;
} }
#filters li { .filters li {
display: inline; display: inline;
} }
#filters li a { .filters li a {
color: inherit; color: inherit;
margin: 3px; margin: 3px;
padding: 3px 7px; padding: 3px 7px;
...@@ -307,39 +294,28 @@ label[for='toggle-all'] { ...@@ -307,39 +294,28 @@ label[for='toggle-all'] {
border-radius: 3px; border-radius: 3px;
} }
#filters li a.selected, .filters li a:hover {
#filters li a:hover {
border-color: rgba(175, 47, 47, 0.1); border-color: rgba(175, 47, 47, 0.1);
} }
#filters li a.selected { .filters li a.selected {
border-color: rgba(175, 47, 47, 0.2); border-color: rgba(175, 47, 47, 0.2);
} }
#clear-completed, .clear-completed,
html #clear-completed:active { html .clear-completed:active {
float: right; float: right;
position: relative; position: relative;
line-height: 20px; line-height: 20px;
text-decoration: none; text-decoration: none;
cursor: pointer; 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; text-decoration: underline;
} }
#info { .info {
margin: 65px auto 0; margin: 65px auto 0;
color: #bfbfbf; color: #bfbfbf;
font-size: 10px; font-size: 10px;
...@@ -347,17 +323,17 @@ html #clear-completed:active { ...@@ -347,17 +323,17 @@ html #clear-completed:active {
text-align: center; text-align: center;
} }
#info p { .info p {
line-height: 1; line-height: 1;
} }
#info a { .info a {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
font-weight: 400; font-weight: 400;
} }
#info a:hover { .info a:hover {
text-decoration: underline; text-decoration: underline;
} }
...@@ -366,16 +342,16 @@ html #clear-completed:active { ...@@ -366,16 +342,16 @@ html #clear-completed:active {
Can't use it globally since it destroys checkboxes in Firefox Can't use it globally since it destroys checkboxes in Firefox
*/ */
@media screen and (-webkit-min-device-pixel-ratio:0) { @media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all, .toggle-all,
#todo-list li .toggle { .todo-list li .toggle {
background: none; background: none;
} }
#todo-list li .toggle { .todo-list li .toggle {
height: 40px; height: 40px;
} }
#toggle-all { .toggle-all {
-webkit-transform: rotate(90deg); -webkit-transform: rotate(90deg);
transform: rotate(90deg); transform: rotate(90deg);
-webkit-appearance: none; -webkit-appearance: none;
...@@ -384,11 +360,11 @@ html #clear-completed:active { ...@@ -384,11 +360,11 @@ html #clear-completed:active {
} }
@media (max-width: 430px) { @media (max-width: 430px) {
#footer { .footer {
height: 50px; height: 50px;
} }
#filters { .filters {
bottom: 10px; bottom: 10px;
} }
} }
...@@ -114,7 +114,12 @@ ...@@ -114,7 +114,12 @@
})({}); })({});
if (location.hostname === 'todomvc.com') { 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 */ /* jshint ignore:end */
...@@ -228,7 +233,7 @@ ...@@ -228,7 +233,7 @@
xhr.onload = function (e) { xhr.onload = function (e) {
var parsedResponse = JSON.parse(e.target.responseText); var parsedResponse = JSON.parse(e.target.responseText);
if (parsedResponse instanceof Array) { if (parsedResponse instanceof Array) {
var count = parsedResponse.length var count = parsedResponse.length;
if (count !== 0) { if (count !== 0) {
issueLink.innerHTML = 'This app has ' + count + ' open issues'; issueLink.innerHTML = 'This app has ' + count + ' open issues';
document.getElementById('issue-count').style.display = 'inline'; document.getElementById('issue-count').style.display = 'inline';
......
{ {
"private": true, "private": true,
"dependencies": { "dependencies": {
"todomvc-app-css": "^1.0.0", "todomvc-app-css": "^2.0.6",
"todomvc-common": "^1.0.1" "todomvc-common": "^1.0.2"
} }
} }
# Elm TodoMVC Example # Elm TodoMVC Example
> A functional reactive language for interactive applications > A functional language for interactive applications
> _[Elm](http://elm-lang.org/)_ > _[Elm](http://elm-lang.org/)_
...@@ -14,7 +14,7 @@ Here are some links you may find helpful: ...@@ -14,7 +14,7 @@ Here are some links you may find helpful:
* [Try Elm](http://elm-lang.org/try) * [Try Elm](http://elm-lang.org/try)
* [Learn Elm](http://elm-lang.org/Learn.elm) * [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: 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 ...@@ -28,25 +28,25 @@ _If you have other helpful links to share, or find any of the links above no lon
## Project Structure ## Project Structure
All of the Elm code lives in `Todo.elm` and `Task.elm` and relies All of the Elm code lives in `Todo.elm` and `Todo/Task.elm` and relies
on the [elm-html][] library. 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 There also is a port handler set up in `index.html` to set the focus on
particular text fields when necessary. particular text fields when necessary.
## Build Instructions ## Build Instructions
You need to install You need to install [elm](http://elm-lang.org/install)
[elm](https://github.com/elm-lang/elm-platform/blob/master/README.md#elm-platform)
on your machine first. on your machine first.
Run the following commands from the root of this project: Run the following commands from the root of this project:
```bash ```bash
elm-package install elm-package install -y
elm-make Todo.elm --output build/Todo.js elm-make Todo.elm --output build/elm.js
``` ```
Then open `index.html` in your browser! Then open `index.html` in your browser!
......
# Vanilla ES6 (ES2015) • [TodoMVC](http://todomvc.com) # 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 ## Learning ES6
...@@ -34,9 +34,10 @@ npm run compile ...@@ -34,9 +34,10 @@ npm run compile
## Implementation ## 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 ## Credit
Created by [Luke Edwards](http://www.lukeed.com) 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){ 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)}
'use strict'; 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)})}
var _controller = require('./controller'); 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)})};
var _controller2 = _interopRequireDefault(_controller); 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);
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]);
'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> <!doctype html>
<html> <html lang="en" data-framework="es6">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>ES6 • TodoMVC</title> <title>Vanilla ES6 • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css"> <link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
</head> </head>
<body> <body>
...@@ -12,31 +11,29 @@ ...@@ -12,31 +11,29 @@
<h1>todos</h1> <h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus> <input class="new-todo" placeholder="What needs to be done?" autofocus>
</header> </header>
<section style="display:none" class="main">
<section class="main">
<input class="toggle-all" type="checkbox"> <input class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label> <label for="toggle-all">Mark all as complete</label>
<ul class="todo-list"></ul> <ul class="todo-list"></ul>
</section>
<footer class="footer"> <footer class="footer">
<span class="todo-count"></span> <span class="todo-count"></span>
<ul class="filters"> <div class="filters">
<li><a href="#/" class="selected">All</a></li> <a href="#/" class="selected">All</a>
<li><a href="#/active">Active</a></li> <a href="#/active">Active</a>
<li><a href="#/completed">Completed</a></li> <a href="#/completed">Completed</a>
</ul> </div>
<button class="clear-completed">Clear completed</button> <button class="clear-completed">Clear completed</button>
</footer> </footer>
</section> </section>
</section>
<footer class="info"> <footer class="info">
<p>Double-click to edit a todo</p> <p>Double-click to edit a todo</p>
<br>
<p>Written by <a href="http://twitter.com/lukeed05">Luke Edwards</a></p> <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> <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer> </footer>
<script src="dist/bundle.js"></script> <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> </body>
</html> </html>
...@@ -17,8 +17,7 @@ button { ...@@ -17,8 +17,7 @@ button {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
font-smoothing: antialiased;
} }
body { body {
...@@ -30,8 +29,7 @@ body { ...@@ -30,8 +29,7 @@ body {
max-width: 550px; max-width: 550px;
margin: 0 auto; margin: 0 auto;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
font-smoothing: antialiased;
font-weight: 300; font-weight: 300;
} }
...@@ -100,8 +98,7 @@ input[type="checkbox"] { ...@@ -100,8 +98,7 @@ input[type="checkbox"] {
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box; box-sizing: border-box;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
font-smoothing: antialiased;
} }
.new-todo { .new-todo {
...@@ -163,17 +160,19 @@ label[for='toggle-all'] { ...@@ -163,17 +160,19 @@ label[for='toggle-all'] {
padding: 0; padding: 0;
} }
.todo-list li.editing button,
.todo-list li.editing label,
.todo-list li.editing .toggle{
display: none;
}
.todo-list li.editing .edit { .todo-list li.editing .edit {
display: block; display: block;
width: 506px; width: 506px;
padding: 13px 17px 12px 17px; padding: 12px 16px;
margin: 0 0 0 43px; margin: 0 0 0 43px;
} }
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle { .todo-list li .toggle {
text-align: center; text-align: center;
width: 40px; width: 40px;
...@@ -287,11 +286,7 @@ label[for='toggle-all'] { ...@@ -287,11 +286,7 @@ label[for='toggle-all'] {
left: 0; left: 0;
} }
.filters li { .filters a {
display: inline;
}
.filters li a {
color: inherit; color: inherit;
margin: 3px; margin: 3px;
padding: 3px 7px; padding: 3px 7px;
...@@ -300,12 +295,12 @@ label[for='toggle-all'] { ...@@ -300,12 +295,12 @@ label[for='toggle-all'] {
border-radius: 3px; border-radius: 3px;
} }
.filters li a.selected, .filters a.selected,
.filters li a:hover { .filters a:hover {
border-color: rgba(175, 47, 47, 0.1); border-color: rgba(175, 47, 47, 0.1);
} }
.filters li a.selected { .filters a.selected {
border-color: rgba(175, 47, 47, 0.2); 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, "private": true,
"scripts": { "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" "prepublish": "npm run compile"
}, },
"dependencies": { "dependencies": {
...@@ -9,8 +9,6 @@ ...@@ -9,8 +9,6 @@
"todomvc-common": "^1.0.2" "todomvc-common": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.1.0", "google-closure-compiler": "^20160315.2.0"
"babel-preset-es2015": "^6.1.18",
"browserify": "^12.0.1"
} }
} }
import Controller from './controller'; import Controller from './controller';
import * as helpers from './helpers'; import {$on} from './helpers';
import Template from './template'; import Template from './template';
import Store from './store'; import Store from './store';
import Model from './model';
import View from './view'; import View from './view';
const $on = helpers.$on; const store = new Store('todos-vanilla-es6');
const setView = () => todo.controller.setView(document.location.hash);
class Todo { const template = new Template();
/** const view = new View(template);
* Init new Todo List
* @param {string} The name of your list
*/
constructor(name) {
this.storage = new Store(name);
this.model = new Model(this.storage);
this.template = new Template();
this.view = new View(this.template);
this.controller = new Controller(this.model, this.view); /**
} * @type {Controller}
} */
const controller = new Controller(store, view);
const todo = new Todo('todos-vanillajs');
const setView = () => controller.setView(document.location.hash);
$on(window, 'load', setView); $on(window, 'load', setView);
$on(window, 'hashchange', setView); $on(window, 'hashchange', setView);
import {emptyItemQuery} from './item';
import Store from './store';
import View from './view';
export default class Controller { export default class Controller {
/** /**
* Take a model & view, then act as controller between them * @param {!Store} store A Store instance
* @param {object} model The model instance * @param {!View} view A View instance
* @param {object} view The view instance
*/ */
constructor(model, view) { constructor(store, view) {
this.model = model; this.store = store;
this.view = view; this.view = view;
this.view.bind('newTodo', title => this.addItem(title)); view.bindAddItem(this.addItem.bind(this));
this.view.bind('itemEdit', item => this.editItem(item.id)); view.bindEditItemSave(this.editItemSave.bind(this));
this.view.bind('itemEditDone', item => this.editItemSave(item.id, item.title)); view.bindEditItemCancel(this.editItemCancel.bind(this));
this.view.bind('itemEditCancel', item => this.editItemCancel(item.id)); view.bindRemoveItem(this.removeItem.bind(this));
this.view.bind('itemRemove', item => this.removeItem(item.id)); view.bindToggleItem((id, completed) => {
this.view.bind('itemToggle', item => this.toggleComplete(item.id, item.completed)); this.toggleCompleted(id, completed);
this.view.bind('removeCompleted', () => this.removeCompletedItems()); this._filter();
this.view.bind('toggleAll', status => this.toggleAll(status.completed)); });
} view.bindRemoveCompleted(this.removeCompletedItems.bind(this));
view.bindToggleAll(this.toggleAll.bind(this));
/**
* 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));
}
/** this._activeRoute = '';
* Renders all active tasks this._lastActiveRoute = null;
*/
showActive() {
this.model.read({completed: false}, data => this.view.render('showEntries', data));
} }
/** /**
* Renders all completed tasks * Set and render the active route.
*
* @param {string} raw '' | '#/' | '#/active' | '#/completed'
*/ */
showCompleted() { setView(raw) {
this.model.read({completed: true}, data => this.view.render('showEntries', data)); 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 * Add an Item to the Store and display it in the list.
* object and it'll handle the DOM insertion and saving of the new item. *
* @param {!string} title Title of the new item
*/ */
addItem(title) { addItem(title) {
if (title.trim() === '') { this.store.insert({
return; id: Date.now(),
} title,
completed: false
this.model.create(title, () => { }, () => {
this.view.render('clearNewTodo'); this.view.clearNewTodo();
this._filter(true); this._filter(true);
}); });
} }
/* /**
* Triggers the item editing mode. * Save an Item in edit.
*/ *
editItem(id) { * @param {number} id ID of the Item in edit
this.model.read(id, data => { * @param {!string} title New title for the Item in edit
const title = data[0].title;
this.view.render('editItem', {id, title});
});
}
/*
* Finishes the item editing mode successfully.
*/ */
editItemSave(id, title) { editItemSave(id, title) {
title = title.trim(); if (title.length) {
this.store.update({id, title}, () => {
if (title.length !== 0) { this.view.editItemDone(id, title);
this.model.update(id, {title}, () => {
this.view.render('editItemDone', {id, title});
}); });
} else { } else {
this.removeItem(id); this.removeItem(id);
} }
} }
/* /**
* Cancels the item editing mode. * Cancel the item editing mode.
*
* @param {!number} id ID of the Item in edit
*/ */
editItemCancel(id) { editItemCancel(id) {
this.model.read(id, data => { this.store.find({id}, data => {
const title = data[0].title; const title = data[0].title;
this.view.render('editItemDone', {id, title}); this.view.editItemDone(id, title);
}); });
} }
/** /**
* Find the DOM element with given ID, * Remove the data and elements related to an Item.
* Then remove it from DOM & Storage *
* @param {!number} id Item ID of item to remove
*/ */
removeItem(id) { removeItem(id) {
this.model.remove(id, () => this.view.render('removeItem', id)); this.store.remove({id}, () => {
this._filter(); this._filter();
this.view.removeItem(id);
});
} }
/** /**
* Will remove all completed items from the DOM and storage. * Remove all completed items.
*/ */
removeCompletedItems() { removeCompletedItems() {
this.model.read({completed: true}, data => { this.store.remove({completed: true}, this._filter.bind(this));
for (let item of data) {
this.removeItem(item.id);
}
});
this._filter();
} }
/** /**
* Give it an ID of a model and a checkbox and it will update the item * Update an Item in storage based on the state of completed.
* in storage based on the checkbox's state.
* *
* @param {number} id The ID of the element to complete or uncomplete * @param {!number} id ID of the target Item
* @param {object} checkbox The checkbox to check the state of complete * @param {!boolean} completed Desired completed state
* or not
* @param {boolean|undefined} silent Prevent re-filtering the todo items
*/ */
toggleComplete(id, completed, silent) { toggleCompleted(id, completed) {
this.model.update(id, {completed}, () => { this.store.update({id, completed}, () => {
this.view.render('elementComplete', {id, completed}); this.view.setItemComplete(id, completed);
}); });
if (!silent) {
this._filter();
}
} }
/** /**
* Will toggle ALL checkboxes' on/off state and completeness of models. * Set all items to complete or active.
* Just pass in the event object. *
* @param {boolean} completed Desired completed state
*/ */
toggleAll(completed) { toggleAll(completed) {
this.model.read({completed: !completed}, data => { this.store.find({completed: !completed}, data => {
for (let item of data) { for (let {id} of data) {
this.toggleComplete(item.id, completed, true); this.toggleCompleted(id, completed);
} }
}); });
...@@ -155,58 +129,31 @@ export default class Controller { ...@@ -155,58 +129,31 @@ export default class Controller {
} }
/** /**
* Updates the pieces of the page which change depending on the remaining * Refresh the list based on the current route.
* number of todos. *
*/ * @param {boolean} [force] Force a re-paint of the list
_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.
*/ */
_filter(force) { _filter(force) {
const active = this._activeRoute; const route = 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]();
}
this._lastActiveRoute = 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.store.count((total, active, completed) => {
* Simply updates the filter nav's selected states this.view.setItemsLeft(active);
*/ this.view.setClearCompletedButtonVisibility(completed);
_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.view.setCompleteAllCheckbox(completed === total);
this._activeRoute = 'All'; this.view.setMainVisibility(total);
} });
this._filter();
this.view.render('setFilter', currentPage); this._lastActiveRoute = route;
} }
} }
// Allow for looping on nodes by chaining: /**
// qsa('.foo').forEach(function () {}) * querySelector wrapper
NodeList.prototype.forEach = Array.prototype.forEach; *
* @param {string} selector Selector to query
// Get element(s) by CSS selector: * @param {Element} [scope] Optional scope element for the selector
*/
export function qs(selector, scope) { export function qs(selector, scope) {
return (scope || document).querySelector(selector); return (scope || document).querySelector(selector);
} }
export function qsa(selector, scope) { /**
return (scope || document).querySelectorAll(selector); * addEventListener wrapper
} *
* @param {Element|Window} target Target Element
// addEventListener wrapper: * @param {string} type Event name to bind to
export function $on(target, type, callback, useCapture) { * @param {Function} callback Event callback
target.addEventListener(type, callback, !!useCapture); * @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 * Attach a handler to an event for all elements matching a selector.
export function $delegate(target, selector, type, handler) { *
* @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 dispatchEvent = event => {
const targetElement = event.target; const targetElement = event.target;
const potentialElements = qsa(selector, target); const potentialElements = target.querySelectorAll(selector);
const hasMatch = Array.from(potentialElements).includes(targetElement); let i = potentialElements.length;
if (hasMatch) { while (i--) {
if (potentialElements[i] === targetElement) {
handler.call(targetElement, event); handler.call(targetElement, event);
break;
}
} }
}; };
// https://developer.mozilla.org/en-US/docs/Web/Events/blur $on(target, type, dispatchEvent, !!capture);
const useCapture = type === 'blur' || type === 'focus';
$on(target, type, dispatchEvent, useCapture);
} }
// Find the element's parent with the given tag name: /**
// $parent(qs('a'), 'div') * Encode less-than and ampersand characters with entity codes to make user-
export function $parent(element, tagName) { * provided text safe to parse as HTML.
if (!element.parentNode) { *
return; * @param {string} s String to escape
} *
* @returns {string} String with unsafe characters escaped with entity codes
if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) { */
return element.parentNode; export const escapeForHTML = s => s.replace(/[&<]/g, c => c === '&' ? '&amp;' : '&lt;');
}
return $parent(element.parentNode, tagName);
}
/**
* @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 */ import {Item, ItemList, ItemQuery, ItemUpdate, emptyItemQuery} from './item';
/**
* 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
*/
export default class Store { export default class Store {
/**
* @param {!string} name Database name
* @param {function()} [callback] Called when the Store is ready
*/
constructor(name, callback) { constructor(name, callback) {
this._dbName = name; /**
* @type {Storage}
*/
const localStorage = window.localStorage;
/**
* @type {ItemList}
*/
let liveTodos;
if (!localStorage[name]) { /**
const data = { * Read the local ItemList from localStorage.
todos: [] *
* @returns {ItemList} Current array of todos
*/
this.getLocalStorage = () => {
return liveTodos || JSON.parse(localStorage.getItem(name) || '[]');
}; };
localStorage[name] = JSON.stringify(data); /**
} * 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) { 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 {ItemQuery} query Query to match
* @param {function} callback The callback to fire when the query has * @param {function(ItemList)} callback Called when the query is done
* completed running
* *
* @example * @example
* db.find({foo: 'bar', hello: 'world'}, function (data) { * db.find({completed: true}, data => {
* // data will return any items that have foo: bar and * // data shall contain items whose completed properties are true
* // hello: world in their properties
* }) * })
*/ */
find(query, callback){ find(query, callback) {
const todos = JSON.parse(localStorage[this._dbName]).todos; const todos = this.getLocalStorage();
let k;
callback.call(this, todos.filter(todo => { callback(todos.filter(todo => {
for (let q in query) { for (k in query) {
if (query[q] !== todo[q]) { if (query[k] !== todo[k]) {
return false; return false;
} }
} }
...@@ -52,93 +65,90 @@ export default class Store { ...@@ -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) {
if (callback) { const id = update.id;
callback.call(this, JSON.parse(localStorage[this._dbName]).todos); const todos = this.getLocalStorage();
} let i = todos.length;
} let k;
/** while (i--) {
* 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
*/
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) { if (todos[i].id === id) {
for (let key in updateData) { for (k in update) {
todos[i][key] = updateData[key]; todos[i][k] = update[k];
} }
break; break;
} }
} }
localStorage[this._dbName] = JSON.stringify(data); this.setLocalStorage(todos);
if (callback) { if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos); callback();
}
} }
} else {
// Generate an ID
updateData.id = new Date().getTime();
todos.push(updateData); /**
localStorage[this._dbName] = JSON.stringify(data); * Insert an item into the Store.
*
* @param {Item} item Item to insert
* @param {function()} [callback] Called when item is inserted
*/
insert(item, callback) {
const todos = this.getLocalStorage();
todos.push(item);
this.setLocalStorage(todos);
if (callback) { if (callback) {
callback.call(this, [updateData]); 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 {ItemQuery} query Query matching the items to remove
* @param {function} callback The callback to fire after saving * @param {function(ItemList)|function()} [callback] Called when records matching query are removed
*/ */
remove(id, callback){ remove(query, callback) {
const data = JSON.parse(localStorage[this._dbName]); let k;
const todos = data.todos;
const len = todos.length; const todos = this.getLocalStorage().filter(todo => {
for (k in query) {
for (let i = 0; i < todos.length; i++) { if (query[k] !== todo[k]) {
if (todos[i].id == id) { return true;
todos.splice(i, 1);
break;
} }
} }
return false;
});
localStorage[this._dbName] = JSON.stringify(data); this.setLocalStorage(todos);
if (callback) { 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){ count(callback) {
localStorage[this._dbName] = JSON.stringify({todos: []}); this.find(emptyItemQuery, data => {
const total = data.length;
if (callback) { let i = total;
callback.call(this, JSON.parse(localStorage[this._dbName]).todos); let completed = 0;
while (i--) {
completed += data[i].completed;
} }
callback(total, total - completed, completed);
});
} }
} }
const htmlEscapes = { import {ItemList} from './item';
'&': '&amp',
'<': '&lt',
'>': '&gt',
'"': '&quot',
'\'': '&#x27',
'`': '&#x60'
};
const reUnescapedHtml = /[&<>"'`]/g; import {escapeForHTML} from './helpers';
const reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
const escape = str => (str && reHasUnescapedHtml.test(str)) ? str.replace(reUnescapedHtml, escapeHtmlChar) : str;
const escapeHtmlChar = chr => htmlEscapes[chr];
export default class Template { 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 * @param {ItemList} items Object containing keys you want to find in the template to replace.
* template to replace. * @returns {!string} Contents for a todo list
* @returns {string} HTML String of an <li> element
* *
* @example * @example
* view.show({ * view.show({
* id: 1, * id: 1,
* title: "Hello World", * title: "Hello World",
* completed: 0, * completed: false,
* }) * })
*/ */
show(data){ itemList(items) {
const view = data.map(d => { return items.reduce((a, item) => a + `
const template = this.defaultTemplate; <li data-id="${item.id}"${item.completed ? ' class="completed"' : ''}>
const completed = d.completed ? 'completed' : ''; <input class="toggle" type="checkbox" ${item.completed ? 'checked' : ''}>
const checked = d.completed ? 'checked' : ''; <label>${escapeForHTML(item.title)}</label>
<button class="destroy"></button>
return this.defaultTemplate </li>`, '');
.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 * Format the contents of an "items left" indicator.
* *
* @param {number} activeTodos The number of active todos. * @param {number} activeTodos 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 {[type]} completedTodos The number of completed todos. * @returns {!string} Contents for an "items left" indicator
* @returns {string} String containing the count
*/ */
clearCompletedButton(completedTodos){ itemCounter(activeTodos) {
return (completedTodos > 0) ? 'Clear completed' : ''; 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 => { export default class View {
qs('.filters .selected').className = ''; /**
qs(`.filters [href="#/${currentPage}"]`).className = 'selected'; * @param {!Template} template A Template instance
}; */
constructor(template) {
const _elementComplete = (id, completed) => { this.template = template;
const listItem = qs(`[data-id="${id}"]`); this.$todoList = qs('.todo-list');
this.$todoItemCounter = qs('.todo-count');
if (!listItem) { this.$clearCompleted = qs('.clear-completed');
return; this.$main = qs('.main');
this.$toggleAll = qs('.toggle-all');
this.$newTodo = qs('.new-todo');
$delegate(this.$todoList, 'li label', 'dblclick', ({target}) => {
this.editItem(target);
});
} }
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}"]`); * Put an item into edit mode.
*
if (!listItem) { * @param {!Element} target Target Item's label Element
return; */
} editItem(target) {
const listItem = target.parentElement;
listItem.className += ' editing'; listItem.classList.add('editing');
const input = document.createElement('input'); const input = document.createElement('input');
input.className = 'edit'; input.className = 'edit';
input.value = target.innerText;
listItem.appendChild(input); listItem.appendChild(input);
input.focus(); input.focus();
input.value = title; }
};
/** /**
* View that abstracts away the browser's DOM completely. * Populate the todo list with a list of items.
* It has two simple entry points:
* *
* - bind(eventName, handler) * @param {ItemList} items Array of items to display
* Takes a todo application event and registers the handler
* - render(command, parameterObject)
* Renders the given command with the options
*/ */
export default class View { showItems(items) {
constructor(template) { this.$todoList.innerHTML = this.template.itemList(items);
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');
this.viewCommands = { /**
showEntries: parameter => this.$todoList.innerHTML = this.template.show(parameter), * Remove an item from the view.
removeItem: parameter => this._removeItem(parameter), *
updateElementCount: parameter => this.$todoItemCounter.innerHTML = this.template.itemCounter(parameter), * @param {number} id Item ID of the item to remove
clearCompletedButton: parameter => this._clearCompletedButton(parameter.completed, parameter.visible), */
contentBlockVisibility: parameter => this.$main.style.display = this.$footer.style.display = parameter.visible ? 'block' : 'none', removeItem(id) {
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) {
const elem = qs(`[data-id="${id}"]`); const elem = qs(`[data-id="${id}"]`);
if (elem) { if (elem) {
...@@ -84,108 +64,170 @@ export default class View { ...@@ -84,108 +64,170 @@ export default class View {
} }
} }
_clearCompletedButton(completedCount, visible) { /**
this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount); * Set the number in the 'items left' display.
this.$clearCompleted.style.display = visible ? 'block' : 'none'; *
* @param {number} itemsLeft Number of items left
*/
setItemsLeft(itemsLeft) {
this.$todoItemCounter.innerHTML = this.template.itemCounter(itemsLeft);
} }
_editItemDone(id, title) { /**
const listItem = qs(`[data-id="${id}"]`); * Set the visibility of the "Clear completed" button.
*
if (!listItem) { * @param {boolean|number} visible Desired visibility of the button
return; */
setClearCompletedButtonVisibility(visible) {
this.$clearCompleted.style.display = !!visible ? 'block' : 'none';
} }
const input = qs('input.edit', listItem); /**
listItem.removeChild(input); * Set the visibility of the main content and footer.
*
* @param {boolean|number} visible Desired visibility
*/
setMainVisibility(visible) {
this.$main.style.display = !!visible ? 'block' : 'none';
}
listItem.className = listItem.className.replace(' editing', ''); /**
* Set the checked state of the Complete All checkbox.
*
* @param {boolean|number} checked The desired checked state
*/
setCompleteAllCheckbox(checked) {
this.$toggleAll.checked = !!checked;
}
qsa('label', listItem).forEach(label => label.textContent = title); /**
* 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';
} }
render(viewCmd, parameter) { /**
this.viewCommands[viewCmd](parameter); * Clear the new todo input
*/
clearNewTodo() {
this.$newTodo.value = '';
} }
_bindItemEditDone(handler) { /**
const self = this; * 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}"]`);
$delegate(self.$todoList, 'li .edit', 'blur', function () { if (!listItem) {
if (!this.dataset.iscanceled) { return;
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 listItem.className = completed ? 'completed' : '';
$delegate(self.$todoList, 'li .edit', 'keypress', function (event) {
if (event.keyCode === self.ENTER_KEY) { // In case it was toggled from an event and not by clicking the checkbox
this.blur(); qs('input', listItem).checked = completed;
}
});
} }
_bindItemEditCancel(handler) { /**
const self = this; * 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);
$delegate(self.$todoList, 'li .edit', 'keyup', function (event) { listItem.classList.remove('editing');
if (event.keyCode === self.ESCAPE_KEY) {
const id = _itemId(this);
this.dataset.iscanceled = true;
this.blur();
handler({ id }); qs('label', listItem).textContent = title;
}
/**
* @param {Function} handler Function called on synthetic event.
*/
bindAddItem(handler) {
$on(this.$newTodo, 'change', ({target}) => {
const title = target.value.trim();
if (title) {
handler(title);
} }
}); });
} }
bind(event, handler) { /**
switch (event) { * @param {Function} handler Function called on synthetic event.
case 'newTodo': */
$on(this.$newTodo, 'change', () => handler(this.$newTodo.value)); bindRemoveCompleted(handler) {
break;
case 'removeCompleted':
$on(this.$clearCompleted, 'click', handler); $on(this.$clearCompleted, 'click', handler);
break; }
case 'toggleAll': /**
$on(this.$toggleAll, 'click', function () { * @param {Function} handler Function called on synthetic event.
handler({completed: this.checked}); */
bindToggleAll(handler) {
$on(this.$toggleAll, 'click', ({target}) => {
handler(target.checked);
}); });
break; }
case 'itemEdit': /**
$delegate(this.$todoList, 'li label', 'dblclick', function () { * @param {Function} handler Function called on synthetic event.
handler({id: _itemId(this)}); */
bindRemoveItem(handler) {
$delegate(this.$todoList, '.destroy', 'click', ({target}) => {
handler(_itemId(target));
}); });
break; }
case 'itemRemove': /**
$delegate(this.$todoList, '.destroy', 'click', function () { * @param {Function} handler Function called on synthetic event.
handler({id: _itemId(this)}); */
bindToggleItem(handler) {
$delegate(this.$todoList, '.toggle', 'click', ({target}) => {
handler(_itemId(target), target.checked);
}); });
break; }
case 'itemToggle': /**
$delegate(this.$todoList, '.toggle', 'click', function () { * @param {Function} handler Function called on synthetic event.
handler({ */
id: _itemId(this), bindEditItemSave(handler) {
completed: this.checked $delegate(this.$todoList, 'li .edit', 'blur', ({target}) => {
}); if (!target.dataset.iscanceled) {
handler(_itemId(target), target.value.trim());
}
}, true);
// 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();
}
}); });
break; }
case 'itemEditDone': /**
this._bindItemEditDone(handler); * @param {Function} handler Function called on synthetic event.
break; */
bindEditItemCancel(handler) {
$delegate(this.$todoList, 'li .edit', 'keyup', ({target, keyCode}) => {
if (keyCode === ESCAPE_KEY) {
target.dataset.iscanceled = true;
target.blur();
case 'itemEditCancel': handler(_itemId(target));
this._bindItemEditCancel(handler);
break;
} }
});
} }
} }
...@@ -11,6 +11,6 @@ __key__:key};this._instances[idx]=inst;return inst},_showHideChildren:function(h ...@@ -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"> </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 These are examples written in JavaScript that
we are still evaluating. 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 <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> <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 @@ ...@@ -1123,6 +1123,15 @@
"url": "examples/vanillajs" "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": { "js_of_ocaml": {
"name": "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.", "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