Commit 7837fd1f authored by Sindre Sorhus's avatar Sindre Sorhus

Merge pull request #1013 from taylorhakes/mithril

Mithril Architecture Example
parents 83bcbfdb 06905bb6
{
"name": "todomvc-mithril",
"version": "0.0.0",
"dependencies": {
"mithril": "0.1.20",
"todomvc-common": "~0.1.4"
}
}
Mithril = m = new function app(window, undefined) {
var type = {}.toString
var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/
function m() {
var args = arguments
var hasAttrs = args[1] !== undefined && type.call(args[1]) == "[object Object]" && !("tag" in args[1]) && !("subtree" in args[1])
var attrs = hasAttrs ? args[1] : {}
var classAttrName = "class" in attrs ? "class" : "className"
var cell = {tag: "div", attrs: {}}
var match, classes = []
while (match = parser.exec(args[0])) {
if (match[1] == "") cell.tag = match[2]
else if (match[1] == "#") cell.attrs.id = match[2]
else if (match[1] == ".") classes.push(match[2])
else if (match[3][0] == "[") {
var pair = attrParser.exec(match[3])
cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" :true)
}
}
if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" ")
cell.children = hasAttrs ? args[2] : args[1]
for (var attrName in attrs) {
if (attrName == classAttrName) cell.attrs[attrName] = (cell.attrs[attrName] || "") + " " + attrs[attrName]
else cell.attrs[attrName] = attrs[attrName]
}
return cell
}
function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) {
//`build` is a recursive function that manages creation/diffing/removal of DOM elements based on comparison between `data` and `cached`
//`parentElement` is a DOM element used for W3C DOM API calls
//`parentTag` is only used for handling a corner case for textarea values
//`parentCache` is used to remove nodes in some multi-node cases
//`parentIndex` and `index` are used to figure out the offset of nodes. They're artifacts from before arrays started being flattened and are likely refactorable
//`data` and `cached` are, respectively, the new and old nodes being diffed
//`shouldReattach` is a flag indicating whether a parent node was recreated (if so, and if this node is reused, then this node must reattach itself to the new parent)
//`editable` is a flag that indicates whether an ancestor is contenteditable
//`namespace` indicates the closest HTML namespace as it cascades down from an ancestor
//`configs` is a list of config functions to run after the topmost `build` call finishes running
//there's logic that relies on the assumption that null and undefined data are equivalent to empty strings
//- this prevents lifecycle surprises from procedural helpers that mix implicit and explicit return statements
//- it simplifies diffing code
if (data === undefined || data === null) data = ""
if (data.subtree === "retain") return cached
var cachedType = type.call(cached), dataType = type.call(data)
if (cached === undefined || cached === null || cachedType != dataType) {
if (cached !== null && cached !== undefined) {
if (parentCache && parentCache.nodes) {
var offset = index - parentIndex
var end = offset + (dataType == "[object Array]" ? data : cached.nodes).length
clear(parentCache.nodes.slice(offset, end), parentCache.slice(offset, end))
}
else if (cached.nodes) clear(cached.nodes, cached)
}
cached = new data.constructor
cached.nodes = []
}
if (dataType == "[object Array]") {
data = flatten(data)
var nodes = [], intact = cached.length === data.length, subArrayCount = 0
//key algorithm: sort elements without recreating them if keys are present
//1) create a map of all existing keys, and mark all for deletion
//2) add new keys to map and mark them for addition
//3) if key exists in new list, change action from deletion to a move
//4) for each key, handle its corresponding action as marked in previous steps
//5) copy unkeyed items into their respective gaps
var DELETION = 1, INSERTION = 2 , MOVE = 3
var existing = {}, unkeyed = [], shouldMaintainIdentities = false
for (var i = 0; i < cached.length; i++) {
if (cached[i] && cached[i].attrs && cached[i].attrs.key !== undefined) {
shouldMaintainIdentities = true
existing[cached[i].attrs.key] = {action: DELETION, index: i}
}
}
if (shouldMaintainIdentities) {
for (var i = 0; i < data.length; i++) {
if (data[i] && data[i].attrs) {
if (data[i].attrs.key !== undefined) {
var key = data[i].attrs.key
if (!existing[key]) existing[key] = {action: INSERTION, index: i}
else existing[key] = {action: MOVE, index: i, from: existing[key].index, element: parentElement.childNodes[existing[key].index]}
}
else unkeyed.push({index: i, element: parentElement.childNodes[i]})
}
}
var actions = Object.keys(existing).map(function(key) {return existing[key]})
var changes = actions.sort(function(a, b) {return a.action - b.action || a.index - b.index})
var newCached = cached.slice()
for (var i = 0, change; change = changes[i]; i++) {
if (change.action == DELETION) {
clear(cached[change.index].nodes, cached[change.index])
newCached.splice(change.index, 1)
}
if (change.action == INSERTION) {
var dummy = window.document.createElement("div")
dummy.key = data[change.index].attrs.key
parentElement.insertBefore(dummy, parentElement.childNodes[change.index])
newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]})
}
if (change.action == MOVE) {
if (parentElement.childNodes[change.index] !== change.element && change.element !== null) {
parentElement.insertBefore(change.element, parentElement.childNodes[change.index])
}
newCached[change.index] = cached[change.from]
}
}
for (var i = 0; i < unkeyed.length; i++) {
var change = unkeyed[i]
parentElement.insertBefore(change.element, parentElement.childNodes[change.index])
newCached[change.index] = cached[change.index]
}
cached = newCached
cached.nodes = []
for (var i = 0, child; child = parentElement.childNodes[i]; i++) cached.nodes.push(child)
}
//end key algorithm
for (var i = 0, cacheCount = 0; i < data.length; i++) {
var item = build(parentElement, parentTag, cached, index, data[i], cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace, configs)
if (item === undefined) continue
if (!item.nodes.intact) intact = false
var isArray = type.call(item) == "[object Array]"
subArrayCount += isArray ? item.length : 1
cached[cacheCount++] = item
}
if (!intact) {
for (var i = 0; i < data.length; i++) {
if (cached[i] !== undefined) nodes = nodes.concat(cached[i].nodes)
}
for (var i = 0, node; node = cached.nodes[i]; i++) {
if (node.parentNode !== null && nodes.indexOf(node) < 0) node.parentNode.removeChild(node)
}
for (var i = cached.nodes.length, node; node = nodes[i]; i++) {
if (node.parentNode === null) parentElement.appendChild(node)
}
if (data.length < cached.length) cached.length = data.length
cached.nodes = nodes
}
}
else if (data !== undefined && dataType == "[object Object]") {
//if an element is different enough from the one in cache, recreate it
if (data.tag != cached.tag || Object.keys(data.attrs).join() != Object.keys(cached.attrs).join() || data.attrs.id != cached.attrs.id) {
clear(cached.nodes)
if (cached.configContext && typeof cached.configContext.onunload == "function") cached.configContext.onunload()
}
if (typeof data.tag != "string") return
var node, isNew = cached.nodes.length === 0
if (data.attrs.xmlns) namespace = data.attrs.xmlns
else if (data.tag === "svg") namespace = "http://www.w3.org/2000/svg"
else if (data.tag === "math") namespace = "http://www.w3.org/1998/Math/MathML"
if (isNew) {
node = namespace === undefined ? window.document.createElement(data.tag) : window.document.createElementNS(namespace, data.tag)
cached = {
tag: data.tag,
//process children before attrs so that select.value works correctly
children: data.children !== undefined ? build(node, data.tag, undefined, undefined, data.children, cached.children, true, 0, data.attrs.contenteditable ? node : editable, namespace, configs) : [],
attrs: setAttributes(node, data.tag, data.attrs, {}, namespace),
nodes: [node]
}
parentElement.insertBefore(node, parentElement.childNodes[index] || null)
}
else {
node = cached.nodes[0]
setAttributes(node, data.tag, data.attrs, cached.attrs, namespace)
cached.children = data.children !== undefined ? build(node, data.tag, undefined, undefined, data.children, cached.children, false, 0, data.attrs.contenteditable ? node : editable, namespace, configs) : []
cached.nodes.intact = true
if (shouldReattach === true && node !== null) parentElement.insertBefore(node, parentElement.childNodes[index] || null)
}
//schedule configs to be called. They are called after `build` finishes running
if (typeof data.attrs["config"] === "function") {
configs.push(data.attrs["config"].bind(window, node, !isNew, cached.configContext = cached.configContext || {}, cached))
}
}
else if (typeof dataType != "function") {
//handle text nodes
var nodes
if (cached.nodes.length === 0) {
if (data.$trusted) {
nodes = injectHTML(parentElement, index, data)
}
else {
nodes = [window.document.createTextNode(data)]
parentElement.insertBefore(nodes[0], parentElement.childNodes[index] || null)
}
cached = "string number boolean".indexOf(typeof data) > -1 ? new data.constructor(data) : data
cached.nodes = nodes
}
else if (cached.valueOf() !== data.valueOf() || shouldReattach === true) {
nodes = cached.nodes
if (!editable || editable !== window.document.activeElement) {
if (data.$trusted) {
clear(nodes, cached)
nodes = injectHTML(parentElement, index, data)
}
else {
//corner case: replacing the nodeValue of a text node that is a child of a textarea/contenteditable doesn't work
if (parentTag === "textarea") parentElement.value = data
else if (editable) editable.innerHTML = data
else {
if (nodes[0].nodeType == 1 || nodes.length > 1) { //was a trusted string
clear(cached.nodes, cached)
nodes = [window.document.createTextNode(data)]
}
parentElement.insertBefore(nodes[0], parentElement.childNodes[index] || null)
nodes[0].nodeValue = data
}
}
}
cached = new data.constructor(data)
cached.nodes = nodes
}
else cached.nodes.intact = true
}
return cached
}
function setAttributes(node, tag, dataAttrs, cachedAttrs, namespace) {
var groups = {}
for (var attrName in dataAttrs) {
var dataAttr = dataAttrs[attrName]
var cachedAttr = cachedAttrs[attrName]
if (!(attrName in cachedAttrs) || (cachedAttr !== dataAttr) || node === window.document.activeElement) {
cachedAttrs[attrName] = dataAttr
if (attrName === "config") continue
else if (typeof dataAttr == "function" && attrName.indexOf("on") == 0) {
node[attrName] = autoredraw(dataAttr, node)
}
else if (attrName === "style" && typeof dataAttr == "object") {
for (var rule in dataAttr) {
if (cachedAttr === undefined || cachedAttr[rule] !== dataAttr[rule]) node.style[rule] = dataAttr[rule]
}
for (var rule in cachedAttr) {
if (!(rule in dataAttr)) node.style[rule] = ""
}
}
else if (namespace !== undefined) {
if (attrName === "href") node.setAttributeNS("http://www.w3.org/1999/xlink", "href", dataAttr)
else if (attrName === "className") node.setAttribute("class", dataAttr)
else node.setAttribute(attrName, dataAttr)
}
else if (attrName === "value" && tag === "input") {
if (node.value !== dataAttr) node.value = dataAttr
}
else if (attrName in node && !(attrName == "list" || attrName == "style")) {
node[attrName] = dataAttr
}
else node.setAttribute(attrName, dataAttr)
}
}
return cachedAttrs
}
function clear(nodes, cached) {
for (var i = nodes.length - 1; i > -1; i--) {
if (nodes[i] && nodes[i].parentNode) {
nodes[i].parentNode.removeChild(nodes[i])
cached = [].concat(cached)
if (cached[i]) unload(cached[i])
}
}
if (nodes.length != 0) nodes.length = 0
}
function unload(cached) {
if (cached.configContext && typeof cached.configContext.onunload == "function") cached.configContext.onunload()
if (cached.children) {
if (type.call(cached.children) == "[object Array]") for (var i = 0; i < cached.children.length; i++) unload(cached.children[i])
else if (cached.children.tag) unload(cached.children)
}
}
function injectHTML(parentElement, index, data) {
var nextSibling = parentElement.childNodes[index]
if (nextSibling) {
var isElement = nextSibling.nodeType != 1
var placeholder = window.document.createElement("span")
if (isElement) {
parentElement.insertBefore(placeholder, nextSibling)
placeholder.insertAdjacentHTML("beforebegin", data)
parentElement.removeChild(placeholder)
}
else nextSibling.insertAdjacentHTML("beforebegin", data)
}
else parentElement.insertAdjacentHTML("beforeend", data)
var nodes = []
while (parentElement.childNodes[index] !== nextSibling) {
nodes.push(parentElement.childNodes[index])
index++
}
return nodes
}
function flatten(data) {
var flattened = []
for (var i = 0; i < data.length; i++) {
var item = data[i]
if (type.call(item) == "[object Array]") flattened.push.apply(flattened, flatten(item))
else flattened.push(item)
}
return flattened
}
function autoredraw(callback, object, group) {
return function(e) {
e = e || event
m.redraw.strategy("diff")
m.startComputation()
try {return callback.call(object, e)}
finally {
if (!lastRedrawId) lastRedrawId = -1;
m.endComputation()
}
}
}
var html
var documentNode = {
insertAdjacentHTML: function(_, data) {
window.document.write(data)
window.document.close()
},
appendChild: function(node) {
if (html === undefined) html = window.document.createElement("html")
if (node.nodeName == "HTML") html = node
else html.appendChild(node)
if (window.document.documentElement && window.document.documentElement !== html) {
window.document.replaceChild(html, window.document.documentElement)
}
else window.document.appendChild(html)
},
insertBefore: function(node) {
this.appendChild(node)
},
childNodes: []
}
var nodeCache = [], cellCache = {}
m.render = function(root, cell, forceRecreation) {
var configs = []
if (!root) throw new Error("Please ensure the DOM element exists before rendering a template into it.")
var id = getCellCacheKey(root)
var node = root == window.document || root == window.document.documentElement ? documentNode : root
if (cellCache[id] === undefined) clear(node.childNodes)
if (forceRecreation === true) reset(root)
cellCache[id] = build(node, null, undefined, undefined, cell, cellCache[id], false, 0, null, undefined, configs)
for (var i = 0; i < configs.length; i++) configs[i]()
}
function getCellCacheKey(element) {
var index = nodeCache.indexOf(element)
return index < 0 ? nodeCache.push(element) - 1 : index
}
m.trust = function(value) {
value = new String(value)
value.$trusted = true
return value
}
m.prop = function(store) {
var prop = function() {
if (arguments.length) store = arguments[0]
return store
}
prop.toJSON = function() {
return store
}
return prop
}
var roots = [], modules = [], controllers = [], lastRedrawId = 0, computePostRedrawHook = null, prevented = false
m.module = function(root, module) {
var index = roots.indexOf(root)
if (index < 0) index = roots.length
var isPrevented = false
if (controllers[index] && typeof controllers[index].onunload == "function") {
var event = {
preventDefault: function() {isPrevented = true}
}
controllers[index].onunload(event)
}
if (!isPrevented) {
m.redraw.strategy("all")
m.startComputation()
roots[index] = root
modules[index] = module
controllers[index] = new module.controller
m.endComputation()
}
}
m.redraw = function() {
var cancel = window.cancelAnimationFrame || window.clearTimeout
var defer = window.requestAnimationFrame || window.setTimeout
if (lastRedrawId) {
cancel(lastRedrawId)
lastRedrawId = defer(redraw, 0)
}
else {
redraw()
lastRedrawId = defer(function() {lastRedrawId = null}, 0)
}
}
m.redraw.strategy = m.prop()
function redraw() {
var mode = m.redraw.strategy()
for (var i = 0; i < roots.length; i++) {
if (controllers[i] && mode != "none") m.render(roots[i], modules[i].view(controllers[i]), mode == "all")
}
if (computePostRedrawHook) {
computePostRedrawHook()
computePostRedrawHook = null
}
lastRedrawId = null
m.redraw.strategy("diff")
}
var pendingRequests = 0
m.startComputation = function() {pendingRequests++}
m.endComputation = function() {
pendingRequests = Math.max(pendingRequests - 1, 0)
if (pendingRequests == 0) m.redraw()
}
m.withAttr = function(prop, withAttrCallback) {
return function(e) {
e = e || event
var currentTarget = e.currentTarget || this
withAttrCallback(prop in currentTarget ? currentTarget[prop] : currentTarget.getAttribute(prop))
}
}
//routing
var modes = {pathname: "", hash: "#", search: "?"}
var redirect = function() {}, routeParams = {}, currentRoute
m.route = function() {
if (arguments.length === 0) return currentRoute
else if (arguments.length === 3 && typeof arguments[1] == "string") {
var root = arguments[0], defaultRoute = arguments[1], router = arguments[2]
redirect = function(source) {
var path = currentRoute = normalizeRoute(source)
if (!routeByValue(root, router, path)) {
m.route(defaultRoute, true)
}
}
var listener = m.route.mode == "hash" ? "onhashchange" : "onpopstate"
window[listener] = function() {
if (currentRoute != normalizeRoute(window.location[m.route.mode])) {
redirect(window.location[m.route.mode])
}
}
computePostRedrawHook = setScroll
window[listener]()
}
else if (arguments[0].addEventListener) {
var element = arguments[0]
var isInitialized = arguments[1]
if (element.href.indexOf(modes[m.route.mode]) < 0) {
element.href = window.location.pathname + modes[m.route.mode] + element.pathname
}
if (!isInitialized) {
element.removeEventListener("click", routeUnobtrusive)
element.addEventListener("click", routeUnobtrusive)
}
}
else if (typeof arguments[0] == "string") {
currentRoute = arguments[0]
var querystring = typeof arguments[1] == "object" ? buildQueryString(arguments[1]) : null
if (querystring) currentRoute += (currentRoute.indexOf("?") === -1 ? "?" : "&") + querystring
var shouldReplaceHistoryEntry = (arguments.length == 3 ? arguments[2] : arguments[1]) === true
if (window.history.pushState) {
computePostRedrawHook = function() {
window.history[shouldReplaceHistoryEntry ? "replaceState" : "pushState"](null, window.document.title, modes[m.route.mode] + currentRoute)
setScroll()
}
redirect(modes[m.route.mode] + currentRoute)
}
else window.location[m.route.mode] = currentRoute
}
}
m.route.param = function(key) {return routeParams[key]}
m.route.mode = "search"
function normalizeRoute(route) {return route.slice(modes[m.route.mode].length)}
function routeByValue(root, router, path) {
routeParams = {}
var queryStart = path.indexOf("?")
if (queryStart !== -1) {
routeParams = parseQueryString(path.substr(queryStart + 1, path.length))
path = path.substr(0, queryStart)
}
for (var route in router) {
if (route == path) {
m.module(root, router[route])
return true
}
var matcher = new RegExp("^" + route.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$")
if (matcher.test(path)) {
path.replace(matcher, function() {
var keys = route.match(/:[^\/]+/g) || []
var values = [].slice.call(arguments, 1, -2)
for (var i = 0; i < keys.length; i++) routeParams[keys[i].replace(/:|\./g, "")] = decodeSpace(values[i])
m.module(root, router[route])
})
return true
}
}
}
function routeUnobtrusive(e) {
e = e || event
if (e.ctrlKey || e.metaKey || e.which == 2) return
e.preventDefault()
m.route(e.currentTarget[m.route.mode].slice(modes[m.route.mode].length))
}
function setScroll() {
if (m.route.mode != "hash" && window.location.hash) window.location.hash = window.location.hash
else window.scrollTo(0, 0)
}
function buildQueryString(object, prefix) {
var str = []
for(var prop in object) {
var key = prefix ? prefix + "[" + prop + "]" : prop, value = object[prop]
str.push(typeof value == "object" ? buildQueryString(value, key) : encodeURIComponent(key) + "=" + encodeURIComponent(value))
}
return str.join("&")
}
function parseQueryString(str) {
var pairs = str.split("&"), params = {}
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i].split("=")
params[decodeSpace(pair[0])] = pair[1] ? decodeSpace(pair[1]) : (pair.length === 1 ? true : "")
}
return params
}
function decodeSpace(string) {
return decodeURIComponent(string.replace(/\+/g, " "))
}
function reset(root) {
var cacheKey = getCellCacheKey(root)
clear(root.childNodes, cellCache[cacheKey])
cellCache[cacheKey] = undefined
}
var none = {}
m.deferred = function() {
var resolvers = [], rejecters = [], resolved = none, rejected = none, promise = m.prop()
var object = {
resolve: function(value) {
if (resolved === none) promise(resolved = value)
for (var i = 0; i < resolvers.length; i++) resolvers[i](value)
resolvers.length = rejecters.length = 0
},
reject: function(value) {
if (rejected === none) rejected = value
for (var i = 0; i < rejecters.length; i++) rejecters[i](value)
resolvers.length = rejecters.length = 0
},
promise: promise
}
object.promise.resolvers = resolvers
object.promise.then = function(success, error) {
var next = m.deferred()
if (!success) success = identity
if (!error) error = identity
function callback(method, callback) {
return function(value) {
try {
var result = callback(value)
if (result && typeof result.then == "function") result.then(next[method], error)
else next[method](result !== undefined ? result : value)
}
catch (e) {
if (type.call(e) == "[object Error]" && e.constructor !== Error) throw e
else next.reject(e)
}
}
}
if (resolved !== none) callback("resolve", success)(resolved)
else if (rejected !== none) callback("reject", error)(rejected)
else {
resolvers.push(callback("resolve", success))
rejecters.push(callback("reject", error))
}
return next.promise
}
return object
}
m.sync = function(args) {
var method = "resolve"
function synchronizer(pos, resolved) {
return function(value) {
results[pos] = value
if (!resolved) method = "reject"
if (--outstanding == 0) {
deferred.promise(results)
deferred[method](results)
}
return value
}
}
var deferred = m.deferred()
var outstanding = args.length
var results = new Array(outstanding)
if (args.length > 0) {
for (var i = 0; i < args.length; i++) {
args[i].then(synchronizer(i, true), synchronizer(i, false))
}
}
else deferred.resolve()
return deferred.promise
}
function identity(value) {return value}
function ajax(options) {
var xhr = new window.XMLHttpRequest
xhr.open(options.method, options.url, true, options.user, options.password)
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) options.onload({type: "load", target: xhr})
else options.onerror({type: "error", target: xhr})
}
}
if (options.serialize == JSON.stringify && options.method != "GET") {
xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8");
}
if (typeof options.config == "function") {
var maybeXhr = options.config(xhr, options)
if (maybeXhr !== undefined) xhr = maybeXhr
}
xhr.send(options.method == "GET" ? "" : options.data)
return xhr
}
function bindData(xhrOptions, data, serialize) {
if (data && Object.keys(data).length > 0) {
if (xhrOptions.method == "GET") {
xhrOptions.url = xhrOptions.url + (xhrOptions.url.indexOf("?") < 0 ? "?" : "&") + buildQueryString(data)
}
else xhrOptions.data = serialize(data)
}
return xhrOptions
}
function parameterizeUrl(url, data) {
var tokens = url.match(/:[a-z]\w+/gi)
if (tokens && data) {
for (var i = 0; i < tokens.length; i++) {
var key = tokens[i].slice(1)
url = url.replace(tokens[i], data[key])
delete data[key]
}
}
return url
}
m.request = function(xhrOptions) {
if (xhrOptions.background !== true) m.startComputation()
var deferred = m.deferred()
var serialize = xhrOptions.serialize = xhrOptions.serialize || JSON.stringify
var deserialize = xhrOptions.deserialize = xhrOptions.deserialize || JSON.parse
var extract = xhrOptions.extract || function(xhr) {
return xhr.responseText.length === 0 && deserialize === JSON.parse ? null : xhr.responseText
}
xhrOptions.url = parameterizeUrl(xhrOptions.url, xhrOptions.data)
xhrOptions = bindData(xhrOptions, xhrOptions.data, serialize)
xhrOptions.onload = xhrOptions.onerror = function(e) {
try {
e = e || event
var unwrap = (e.type == "load" ? xhrOptions.unwrapSuccess : xhrOptions.unwrapError) || identity
var response = unwrap(deserialize(extract(e.target, xhrOptions)))
if (e.type == "load") {
if (type.call(response) == "[object Array]" && xhrOptions.type) {
for (var i = 0; i < response.length; i++) response[i] = new xhrOptions.type(response[i])
}
else if (xhrOptions.type) response = new xhrOptions.type(response)
}
deferred[e.type == "load" ? "resolve" : "reject"](response)
}
catch (e) {
if (e instanceof SyntaxError) throw new SyntaxError("Could not parse HTTP response. See http://lhorie.github.io/mithril/mithril.request.html#using-variable-data-formats")
else if (type.call(e) == "[object Error]" && e.constructor !== Error) throw e
else deferred.reject(e)
}
if (xhrOptions.background !== true) m.endComputation()
}
ajax(xhrOptions)
return deferred.promise
}
//testing API
m.deps = function(mock) {return window = mock}
//for internal testing only, do not use `m.deps.factory`
m.deps.factory = app
return m
}(typeof window != "undefined" ? window : {})
if (typeof module != "undefined" && module !== null) module.exports = m
if (typeof define == "function" && define.amd) define(function() {return m})
;;;
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
color: inherit;
-webkit-appearance: none;
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #eaeaea url('bg.png');
color: #4d4d4d;
width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
button,
input[type="checkbox"] {
outline: none;
}
#todoapp {
background: #fff;
background: rgba(255, 255, 255, 0.9);
margin: 130px 0 40px 0;
border: 1px solid #ccc;
position: relative;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.15);
}
#todoapp:before {
content: '';
border-left: 1px solid #f5d6d6;
border-right: 1px solid #f5d6d6;
width: 2px;
position: absolute;
top: 0;
left: 40px;
height: 100%;
}
#todoapp input::-webkit-input-placeholder {
font-style: italic;
}
#todoapp input::-moz-placeholder {
font-style: italic;
color: #a9a9a9;
}
#todoapp h1 {
position: absolute;
top: -120px;
width: 100%;
font-size: 70px;
font-weight: bold;
text-align: center;
color: #b3b3b3;
color: rgba(255, 255, 255, 0.3);
text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility;
-o-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
#header {
padding-top: 15px;
border-radius: inherit;
}
#header:before {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 15px;
z-index: 2;
border-bottom: 1px solid #6c615c;
background: #8d7d77;
background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
border-top-left-radius: 1px;
border-top-right-radius: 1px;
}
#new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.02);
z-index: 2;
box-shadow: none;
}
#main {
position: relative;
z-index: 2;
border-top: 1px dotted #adadad;
}
label[for='toggle-all'] {
display: none;
}
#toggle-all {
position: absolute;
top: -42px;
left: -4px;
width: 40px;
text-align: center;
/* Mobile Safari */
border: none;
}
#toggle-all:before {
content: '»';
font-size: 28px;
color: #d9d9d9;
padding: 0 25px 7px;
}
#toggle-all:checked:before {
color: #737373;
}
#todo-list {
margin: 0;
padding: 0;
list-style: none;
}
#todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px dotted #ccc;
}
#todo-list li:last-child {
border-bottom: none;
}
#todo-list li.editing {
border-bottom: none;
padding: 0;
}
#todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
#todo-list li.editing .view {
display: none;
}
#todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
/* Mobile Safari */
border: none;
-webkit-appearance: none;
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
#todo-list li .toggle:after {
content: '✔';
/* 40 + a couple of pixels visual adjustment */
line-height: 43px;
font-size: 20px;
color: #d9d9d9;
text-shadow: 0 -1px 0 #bfbfbf;
}
#todo-list li .toggle:checked:after {
color: #85ada7;
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
}
#todo-list li label {
white-space: pre;
word-break: break-word;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
-webkit-transition: color 0.4s;
transition: color 0.4s;
}
#todo-list li.completed label {
color: #a9a9a9;
text-decoration: line-through;
}
#todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 22px;
color: #a88a8a;
-webkit-transition: all 0.2s;
transition: all 0.2s;
}
#todo-list li .destroy:hover {
text-shadow: 0 0 1px #000,
0 0 10px rgba(199, 107, 107, 0.8);
-webkit-transform: scale(1.3);
-ms-transform: scale(1.3);
transform: scale(1.3);
}
#todo-list li .destroy:after {
content: '✖';
}
#todo-list li:hover .destroy {
display: block;
}
#todo-list li .edit {
display: none;
}
#todo-list li.editing:last-child {
margin-bottom: -1px;
}
#footer {
color: #777;
padding: 0 15px;
position: absolute;
right: 0;
bottom: -31px;
left: 0;
height: 20px;
z-index: 1;
text-align: center;
}
#footer:before {
content: '';
position: absolute;
right: 0;
bottom: 31px;
left: 0;
height: 50px;
z-index: -1;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
0 6px 0 -3px rgba(255, 255, 255, 0.8),
0 7px 1px -3px rgba(0, 0, 0, 0.3),
0 43px 0 -6px rgba(255, 255, 255, 0.8),
0 44px 2px -6px rgba(0, 0, 0, 0.2);
}
#todo-count {
float: left;
text-align: left;
}
#filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
#filters li {
display: inline;
}
#filters li a {
color: #83756f;
margin: 2px;
text-decoration: none;
}
#filters li a.selected {
font-weight: bold;
}
#clear-completed {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
background: rgba(0, 0, 0, 0.1);
font-size: 11px;
padding: 0 10px;
border-radius: 3px;
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
}
#clear-completed:hover {
background: rgba(0, 0, 0, 0.15);
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
}
#info {
margin: 65px auto 0;
color: #a6a6a6;
font-size: 12px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
text-align: center;
}
#info a {
color: inherit;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox and Opera
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all,
#todo-list li .toggle {
background: none;
}
#todo-list li .toggle {
height: 40px;
}
#toggle-all {
top: -56px;
left: -15px;
width: 65px;
height: 41px;
-webkit-transform: rotate(90deg);
-ms-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
.hidden {
display: none;
}
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #C5C5C5;
border-bottom: 1px dashed #F7F7F7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
-webkit-transition-property: left;
transition-property: left;
-webkit-transition-duration: 500ms;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
margin: 0 0 0 300px;
}
.learn-bar > .learn {
left: 8px;
}
.learn-bar #todoapp {
width: 550px;
margin: 130px auto 40px auto;
}
}
(function () {
'use strict';
// 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') {
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 redirect() {
if (location.hostname === 'tastejs.github.io') {
location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com');
}
}
function findRoot() {
var base;
[/labs/, /\w*-examples/].forEach(function (href) {
var match = location.href.match(href);
if (!base && match) {
base = location.href.indexOf(match);
}
});
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]').getAttribute('data-framework');
}
if (template && learnJSON[framework]) {
this.frameworkJSON = learnJSON[framework];
this.template = template;
this.append();
}
}
Learn.prototype.append = function () {
var aside = document.createElement('aside');
aside.innerHTML = _.template(this.template, this.frameworkJSON);
aside.className = 'learn';
// Localize demo links
var demoLinks = aside.querySelectorAll('.demo-link');
Array.prototype.forEach.call(demoLinks, function (demoLink) {
demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href'));
});
document.body.className = (document.body.className + ' learn-bar').trim();
document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
};
redirect();
getFile('learn.json', Learn);
})();
<!doctype html>
<html lang="en" data-framework="mithril">
<head>
<meta charset="utf-8">
<title>Mithril • TodoMVC</title>
<link rel="stylesheet" href="bower_components/todomvc-common/base.css">
</head>
<body>
<section id="todoapp"></section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://taylorhakes.com">Taylor Hakes</a> and <a href="http://blogue.jpmonette.net">Jean-Philippe Monette</a> <br>(Special thanks to <a
href="https://github.com/lhorie/">Leo Horie</a>)</p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script src="bower_components/todomvc-common/base.js"></script>
<script src="bower_components/mithril/mithril.js"></script>
<script src="js/models/todo.js"></script>
<script src="js/models/storage.js"></script>
<script src="js/controllers/todo.js"></script>
<script src="js/views/main-view.js"></script>
<script src="js/views/footer-view.js"></script>
<script src="js/app.js"></script>
</body>
</html>
'use strict';
/*global m */
var app = app || {};
app.ENTER_KEY = 13;
app.ESC_KEY = 27;
m.route.mode = 'hash';
m.route(document.getElementById('todoapp'), '/', {
'/': app,
'/:filter': app
});
'use strict';
/*global m */
var app = app || {};
app.controller = function () {
// Todo collection
this.list = app.storage.get();
// Update with props
this.list = this.list.map(function(item) {
return new app.Todo(item);
});
// Temp title placeholder
this.title = m.prop('');
// Todo list filter
this.filter = m.prop(m.route.param('filter') || '');
this.add = function () {
var title = this.title().trim();
if (title) {
this.list.push(new app.Todo({title: title}));
app.storage.put(this.list);
}
this.title('');
};
this.isVisible = function (todo) {
switch (this.filter()) {
case 'active':
return !todo.completed();
case 'completed':
return todo.completed();
default:
return true;
}
};
this.complete = function (todo) {
if (todo.completed()) {
todo.completed(false);
} else {
todo.completed(true);
}
app.storage.put(this.list);
};
this.edit = function (todo) {
todo.previousTitle = todo.title();
todo.editing(true);
};
this.doneEditing = function (todo, index) {
todo.editing(false);
todo.title(todo.title().trim());
if (!todo.title()) {
this.list.splice(index, 1);
}
app.storage.put(this.list);
};
this.cancelEditing = function (todo) {
todo.title(todo.previousTitle);
todo.editing(false);
};
this.clearTitle = function () {
this.title('');
};
this.remove = function (key) {
this.list.splice(key, 1);
app.storage.put(this.list);
};
this.clearCompleted = function () {
for (var i = this.list.length - 1; i >= 0; i--) {
if (this.list[i].completed()) {
this.list.splice(i, 1);
}
}
app.storage.put(this.list);
};
this.amountCompleted = function () {
var amount = 0;
for (var i = 0; i < this.list.length; i++) {
if (this.list[i].completed()) {
amount++;
}
}
return amount;
};
this.allCompleted = function () {
for (var i = 0; i < this.list.length; i++) {
if (!this.list[i].completed()) {
return false;
}
}
return true;
};
this.completeAll = function () {
var allCompleted = this.allCompleted();
for (var i = 0; i < this.list.length; i++) {
this.list[i].completed(!allCompleted);
}
app.storage.put(this.list);
};
};
'use strict';
var app = app || {};
(function () {
var STORAGE_ID = 'todos-mithril';
app.storage = {
get: function () {
return JSON.parse(localStorage.getItem(STORAGE_ID) || '[]');
},
put: function (todos) {
localStorage.setItem(STORAGE_ID, JSON.stringify(todos));
}
};
})();
'use strict';
/*global m */
var app = app || {};
// Todo Model
app.Todo = function (data) {
this.title = m.prop(data.title);
this.completed = m.prop(data.completed || false);
this.editing = m.prop(data.editing || false);
};
'use strict';
/*global m */
var app = app || {};
app.footer = function (ctrl) {
var amountCompleted = ctrl.amountCompleted();
var amountActive = ctrl.list.length - amountCompleted;
return m('footer#footer', [
m('span#todo-count', [
m('strong', amountActive), ' item' + (amountActive !== 1 ? 's' : '') + ' left'
]),
m('ul#filters', [
m('li', [
m('a[href=/]', {
config: m.route,
class: ctrl.filter() === '' ? 'selected' : ''
}, 'All')
]),
m('li', [
m('a[href=/active]', {
config: m.route,
class: ctrl.filter() === 'active' ? 'selected' : ''
}, 'Active')
]),
m('li', [
m('a[href=/completed]', {
config: m.route,
class: ctrl.filter() === 'completed' ? 'selected' : ''
}, 'Completed')
])
]), ctrl.amountCompleted() === 0 ? '' : m('button#clear-completed', {
onclick: ctrl.clearCompleted.bind(ctrl)
}, 'Clear completed (' + amountCompleted + ')')
]);
};
'use strict';
/*global m */
var app = app || {};
// View utility
app.watchInput = function (onenter, onescape) {
return function (e) {
if (e.keyCode === app.ENTER_KEY) {
onenter();
} else if (e.keyCode === app.ESC_KEY) {
onescape();
}
};
};
app.view = (function() {
var focused = false;
return function (ctrl) {
return [
m('header#header', [
m('h1', 'todos'), m('input#new-todo[placeholder="What needs to be done?"]', {
onkeyup: app.watchInput(ctrl.add.bind(ctrl),
ctrl.clearTitle.bind(ctrl)),
value: ctrl.title(),
oninput: m.withAttr('value', ctrl.title),
config: function (element) {
if (!focused) {
element.focus();
focused = true;
}
}
})
]),
m('section#main', {
style: {
display: ctrl.list.length ? '' : 'none'
}
}, [
m('input#toggle-all[type=checkbox]', {
onclick: ctrl.completeAll.bind(ctrl),
checked: ctrl.allCompleted()
}),
m('ul#todo-list', [
ctrl.list.filter(ctrl.isVisible.bind(ctrl)).map(function (task, index) {
return m('li', { class: (function () {
var classes = '';
classes += task.completed() ? 'completed' : '';
classes += task.editing() ? ' editing' : '';
return classes;
})()
}, [
m('.view', [
m('input.toggle[type=checkbox]', {
onclick: m.withAttr('checked', ctrl.complete.bind(ctrl, task)),
checked: task.completed()
}),
m('label', {
ondblclick: ctrl.edit.bind(ctrl, task)
}, task.title()),
m('button.destroy', {
onclick: ctrl.remove.bind(ctrl, index)
})
]), m('input.edit', {
value: task.title(),
onkeyup: app.watchInput(ctrl.doneEditing.bind(ctrl, task, index),
ctrl.cancelEditing.bind(ctrl, task)),
oninput: m.withAttr('value', task.title),
config: function (element) {
if (task.editing()) {
element.focus();
element.selectionStart = element.value.length;
}
},
onblur: ctrl.doneEditing.bind(ctrl, task, index)
})
]);
})
])
]), ctrl.list.length === 0 ? '' : app.footer(ctrl)
];
}
})();
# Mithril TodoMVC Example
> [Mithril](http://lhorie.github.io/mithril/) is a client-side MVC framework - a tool to organize code in a way that is easy to think about and to maintain.
> _[Mithril - lhorie.github.io/mithril/](http://lhorie.github.io/mithril/)_
## Learning Mithril
The [Mithril website](http://lhorie.github.io/mithril/getting-started.html) is a great resource for getting started.
Here are some links you may find helpful:
* [Official Documentation](http://lhorie.github.io/mithril/mithril.html)
_If you have other helpful links to share, or find any of the links above no longer work, please [let us know](https://github.com/tastejs/todomvc/issues)._
## Credit
This TodoMVC application was created by [taylorhakes](https://github.com/taylorhakes).
\ No newline at end of file
......@@ -120,6 +120,9 @@
<li class="routing">
<a href="dependency-examples/flight/" data-source="http://flightjs.github.io/" data-content="Flight is a lightweight, component-based JavaScript framework that maps behavior to DOM nodes. Twitter uses it for their web applications.">Flight</a>
</li>
<li class="routing">
<a href="architecture-examples/mithril/" data-source="http://lhorie.github.io/mithril/" data-content="Mithril is a client-side MVC framework - a tool to organize code in a way that is easy to think about and to maintain.">Mithril</a>
</li>
</ul>
</div>
<div class="js-app-list" data-app-list="ctojs">
......
......@@ -1458,6 +1458,43 @@
}]
}]
},
"mithril": {
"name": "Mithril",
"description": "Mithril is a client-side MVC framework - a tool to organize code in a way that is easy to think about and to maintain.",
"homepage": "lhorie.github.io/mithril/",
"examples": [{
"name": "Architecture Example",
"url": "architecture-examples/mithril"
}],
"link_groups": [{
"heading": "Official Resources",
"links": [{
"name": "Documentation",
"url": "http://lhorie.github.io/mithril/getting-started.html"
}, {
"name": "API Reference",
"url": "http://lhorie.github.io/mithril/mithril.html"
}, {
"name": "Tutorials",
"url": "http://lhorie.github.io/mithril-blog/"
}, {
"name": "Mithril on Github",
"url": "https://github.com/lhorie/mithril.js"
}]
}, {
"heading": "Community",
"links": [{
"name": "Mailing list on Google Groups",
"url": "https://groups.google.com/forum/#!forum/mithriljs"
}, {
"name": "StackOverflow",
"url": "http://stackoverflow.com/questions/tagged/mithril.js"
}, {
"name": "Projects and Snippets",
"url": "https://github.com/lhorie/mithril.js/wiki/Community-Projects-and-Snippets"
}]
}]
},
"montage": {
"name": "MontageJS",
"description": "MontageJS is a framework for building rich HTML5 applications optimized for today and tomorrow’s range of connected devices. It offers time-tested design patterns and software principles, a modular architecture, a friendly method to achieve a clean separation of concerns, and supports sharing packages and modules with your NodeJS server.",
......
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