Created
September 4, 2014 11:19
-
-
Save seliopou/3fc7b7fde5b160dbf86c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- This is an Elm implementation of TodoMVC using elm-d3 to construct views. | |
-- For information about the TodoMVC project and the functionality that's | |
-- implemented below, go here: | |
-- | |
-- http://todomvc.com | |
-- | |
-- To build this app, from the root directory of the elm-d3 project, compile it | |
-- using the following commands: | |
-- | |
-- make | |
-- elm --make --src-dir=src `./scripts/build-flags` `./scripts/include-css examples/todo.css` examples/Todo.elm | |
-- | |
-- On OS X, you can then open the file in the browser using the following command: | |
-- | |
-- open build/examples/Todo.html | |
-- | |
module Todo where | |
-- Import D3 and dump all its names into the current scope. Also import event | |
-- handlers. | |
-- | |
import D3(..) | |
import D3.Event(..) | |
-- Import Json to set DOM element properties in the view code. Import String to | |
-- use Elm's fast string library. Is this still necessary? I have no idea. | |
-- | |
import Json | |
import String | |
-- There are some cases in the code below that should never happen and in fact | |
-- cannot ever happen. In other languages, you'd use an `asset false` | |
-- expression, or `undefined` to indicate that the case is impossible. In Elm, | |
-- you can call `Native.Error.raise`. | |
-- | |
import Native.Error | |
------------------------------------------------------------------------------- | |
-- The Model | |
-- | |
-- This section contains data type definitions for storing the state of the | |
-- application. It also contains several operations defined over those data | |
-- types that will come in handy later in the application. | |
-- | |
type Item = { text : String, completed : Bool } | |
-- The model for the Todo application. The entire state of the application is | |
-- contained in a record of this type. elm-d3 prohibits and statically enforces | |
-- the inability to read arbitrary information from the DOM. The only way to | |
-- read information from the DOM is through events. As a result, this will | |
-- serve not just as a record, but as an authoritative record of the | |
-- application state. | |
-- | |
type Model = { | |
input : String, -- * The input for a new task description. Note | |
-- that this will be kept up-to-date with the | |
-- content of the <input> element the user | |
-- interacts with, but this is the authoritative | |
-- record of that input. | |
items : [Item] -- * The items in the Todo list. Each item | |
-- includes a description and a 'status bit' | |
-- indicating whether the item has been | |
-- completed (True) or not (False). | |
} | |
-- Sets that status bit of the item at index `i` to the value `b`. | |
-- | |
setStatus : Model -> Int -> Bool -> Model | |
setStatus m i b = | |
{ m | items <- imap (\x j -> if j == i then { x | completed <- b } else x) m.items } | |
-- Sets the description of the item at index `i` to the value `d`. | |
-- | |
setDescription : Model -> Int -> String -> Model | |
setDescription m i d = | |
{ m | items <- imap (\x j -> if j == i then { x | text <- d} else x) m.items } | |
------------------------------------------------------------------------------- | |
-- The Events | |
-- | |
-- This section contains a data type definition high-level application events. | |
-- These events are semantically meaningful in the application's domain and | |
-- make no reference to how the user triggered the events. For example, the | |
-- `ChangeInput` event below was caused by a user keypress, but that's not | |
-- relevant to the "business logic" of the application. The view code to follow | |
-- takes care of mapping low-level DOM events to these high-level application | |
-- events, allowing the event handling code, i.e., the `transform` function, to | |
-- be written in the language of the application, rather than the langauge of | |
-- the DOM. | |
-- | |
-- These are the high-level events that the Todo application must handle. In | |
-- the view code below, you'll map DOM events to this data type. | |
-- | |
data Event | |
= AddInput -- * Create a new item out of the current input. | |
| ChangeInput String -- * Update the current input to have the given value. | |
| Delete Int -- * Remove the item from the given index. | |
| Check Int -- * Mark the item with the given index as completed. | |
| Uncheck Int -- * Mark the item with the given index as not completed. | |
| Noop -- * NOOP (do nothing). | |
-- This is an event handler. It specifies how the model should be modified | |
-- based on a high-level Event. Note that becuase events are represented as a | |
-- variant of an algebraic data type, the type system requires you to put | |
-- event-handling code all in one place. This will serve as the body of the | |
-- application's event loop. | |
-- | |
transform : Event -> Model -> Model | |
transform e m = | |
case e of | |
AddInput -> { m | input <- "" | |
, items <- { text = m.input, completed = False }::m.items } | |
ChangeInput i -> { m | input <- i } | |
Delete index -> { m | items <- ifilter (\_ j -> j /= index) m.items } | |
Check index -> setStatus m index True | |
Uncheck index -> setStatus m index False | |
Noop -> m | |
-- This is the event stream that your DOM event handlers will push Events into | |
-- in your view code. Later, you'll loop over this stream using the transform | |
-- function above to produce a time-varying model. | |
-- | |
events : Stream Event | |
events = stream () | |
------------------------------------------------------------------------------- | |
-- Views | |
-- | |
-- NOTE: By far, the majority of the code in this example is view code. Not all | |
-- of it will be commented as thoroughly as above, but enough will be in order | |
-- to get the point across. That is the hope, at least. | |
-- | |
-- A D3 Selection that, when rendered, will create an HTML fragment to display | |
-- and interact with an Item. The type `Selection Item` indicates that a list | |
-- of `Item`s is bound to current document subtree. This Selection will | |
-- therefor have access to the `Item` that is bound to its parent element, or | |
-- "context", as well as its index. | |
-- | |
-- * When this Selection is rendered, it will create a document fragment that | |
-- looks like this, with event handlers and properties omitted: | |
-- | |
-- <div class="view"> | |
-- <input class="toggle" type="checkbox" /> | |
-- <label>{{item.text}}</label> | |
-- <button class="destroy" /> | |
-- </div> | |
-- | |
-- * When the <button /> is clicked it should create a Delete event and insert | |
-- it into the event stream. | |
-- | |
-- * When the <input /> is clicked, it should create a Check or Uncheck event | |
-- depending on the item's state and insert it into the event stream. | |
-- | |
-- * Any time that the state of the item changes, it should be refected in | |
-- the structure and content of this HTML fragment. That means ensuring that | |
-- the text is properly displayed, and the "checked" property of the <input /> | |
-- element is properly set. | |
-- | |
item_view : Selection Item | |
item_view = -- Native.Error.raise "NYI: item_view" | |
-- A D3 Widget that will bind a new set of data to its subtree based on the | |
-- data bound to the parent Selection. Its type indicates that its context | |
-- will have a Model bound to it. For each item in the model, it should | |
-- produce an HTML fragment that looks like this: | |
-- | |
-- <li class="{{if item.completed then 'completed' else '' }}> | |
-- <item_view /> | |
-- </li> | |
-- | |
items : Widget Model Item | |
items = -- Native.Error.raise "NYI: items" | |
-- A D3 selection that will render an input element. This is the input element | |
-- where users will type their todo items. When rendered, it should produce an | |
-- HTML subtree that looks like this. | |
-- | |
-- <input id="new-todo" autofocus="" placeholder="What needs to be done?" /> | |
-- | |
-- It should also only render this input element once! | |
-- | |
-- * When the value of the field changes, it should create a ChangeInput event | |
-- and send it to the event stream. | |
-- | |
-- * When the input field in the Model changes, that should be reflected in the | |
-- input field. | |
-- | |
-- * When the user presses the Enter key in the input field, it should create | |
-- an AddInput event and send it to the event stream. | |
-- | |
input_box : Selection Model | |
input_box = -- Native.Error.raise "NYI: input_box" | |
-- The remainder of the view code uses concepts previously described, so the | |
-- explanations will be omitted. Continue down to "The Application" section to | |
-- see how everything fits together. | |
-- | |
content : Selection Model | |
content = | |
let header = | |
let h1 = static "h1" <.> text (\_ _ -> "todo") in | |
static "header" | |
|. (h1 `sequence` input_box) | |
in | |
let main = | |
static "section" <.> str attr "id" "main" | |
|^ static "input" | |
|. str attr "id" "toggle-all" | |
|. str attr "type" "checkbox" | |
|^ static "ul" | |
|. str attr "id" "todo-list" | |
|. embed items | |
in | |
sequence header main | |
footer : Selection Model | |
footer = | |
static "footer" <.> str attr "id" "footer" | |
|. static "span" | |
|. str attr "id" "todo-count" | |
|. html (\m _ -> | |
let undone_count = length (filter (\x -> not x.completed) m.items) in | |
"<strong>" ++ (show undone_count) ++ "</strong> items left") | |
------------------------------------------------------------------------------- | |
-- The Application | |
-- | |
-- The view is the composition of two Selections defined above, each rendered | |
-- as children of the root element provided by the Elm runtime. | |
-- | |
view : Selection Model | |
view = | |
static "section" <.> str attr "id" "todoapp" | |
|. content `sequence` footer | |
-- A controller mediates user interactions with model updates. In other words, | |
-- it can take a Stream Event and transform that into a Signal Model. It does | |
-- this by using the folde function, which given an initial Model and a | |
-- function that can transform that Model given an Event, will produce a Signal | |
-- Model. The transform function is the one defined above, and the initial | |
-- Model contains no items and an empty input box. | |
-- | |
controller : Stream Event -> Signal Model | |
controller = | |
let initial = { input="", items=[] } in | |
folde transform initial | |
-- This is the 'entry-point' of the application. It's also where the | |
-- controller's wired up to the view. The view pushes its Events to the events | |
-- stream, which is fed into the controller to produce a Signal of Models. | |
-- | |
-- The render function serves two purposes. First, it performs the initial data | |
-- binding to the view, which provides data bindings in the model an input from | |
-- which to derive new data. Second, it passes the view representation to the | |
-- runtime to render it as a document fragment. | |
-- | |
-- In the use of render below, it is lifted to a Signal of Models. This will | |
-- cause the view to be rerendered every time the Model changes. | |
-- | |
main : Signal Element | |
main = render 800 600 view <~ (controller events) | |
------------------------------------------------------------------------------- | |
-- Helper functions (undocumented) | |
-- | |
imap f l = | |
let loop i l = | |
case l of | |
[] -> [] | |
x::xs -> (f x i)::(loop (i + 1) xs) | |
in | |
loop 0 l | |
ifilter f l = | |
let loop i l = | |
case l of | |
[] -> [] | |
x::xs -> | |
if f x i | |
then x::(loop (i + 1) xs) | |
else loop (i + 1) xs | |
in | |
loop 0 l | |
ifilterMap f l = | |
let loop i l = | |
case l of | |
[] -> [] | |
x::xs -> | |
case f x i of | |
Nothing -> loop (i + 1) xs | |
Just x' -> x'::(loop (i + 1) xs) | |
in | |
loop 0 l |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment