Skip to content

Instantly share code, notes, and snippets.

@seliopou
Created Sep 4, 2014
Embed
What would you like to do?
-- 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