From 70b24895394b314da45e3a4d27ee24fa90a3fd2c Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Sun, 16 Mar 2014 22:07:53 -0400 Subject: [PATCH] initial release --- .gitignore | 1 + Gruntfile.js | 116 +++ README.md | 5 +- archive/v0.1/comparisons/angular.parsing.html | 2 + .../v0.1/comparisons/angular.rendering.html | 14 + archive/v0.1/comparisons/angular.safety.html | 13 + .../v0.1/comparisons/backbone.parsing.html | 4 + .../v0.1/comparisons/backbone.rendering.html | 30 + archive/v0.1/comparisons/backbone.safety.html | 29 + archive/v0.1/comparisons/jquery.parsing.html | 2 + .../v0.1/comparisons/jquery.rendering.html | 17 + archive/v0.1/comparisons/jquery.safety.html | 16 + archive/v0.1/comparisons/mithril.parsing.html | 2 + .../v0.1/comparisons/mithril.rendering.html | 20 + archive/v0.1/comparisons/mithril.safety.html | 19 + archive/v0.1/index.html | 196 +++++ archive/v0.1/lib/prism/prism.css | 126 +++ archive/v0.1/lib/prism/prism.js | 9 + archive/v0.1/mithril-tests.js | 725 ++++++++++++++++++ archive/v0.1/mithril.min.js | 8 + archive/v0.1/mithril.min.map | 1 + archive/v0.1/mithril.min.zip | Bin 0 -> 19855 bytes archive/v0.1/pages.json | 4 + archive/v0.1/style.css | 86 +++ archive/v0.1/tools/template-compiler.sjs | 64 ++ archive/v0.1/tools/template-converter.html | 9 + archive/v0.1/tools/template-converter.js | 89 +++ docs/auto-redrawing.md | 109 +++ docs/change-log.md | 3 + docs/comparison.md | 135 ++++ docs/compiling-templates.md | 94 +++ docs/components.md | 153 ++++ docs/getting-started.md | 566 ++++++++++++++ docs/how-to-read-signatures.md | 166 ++++ docs/integration.md | 104 +++ docs/layout/api.html | 77 ++ docs/layout/comparisons/angular.parsing.html | 2 + .../layout/comparisons/angular.rendering.html | 14 + docs/layout/comparisons/angular.safety.html | 13 + docs/layout/comparisons/backbone.parsing.html | 4 + .../comparisons/backbone.rendering.html | 30 + docs/layout/comparisons/backbone.safety.html | 29 + docs/layout/comparisons/jquery.parsing.html | 2 + docs/layout/comparisons/jquery.rendering.html | 17 + docs/layout/comparisons/jquery.safety.html | 16 + docs/layout/comparisons/mithril.parsing.html | 2 + .../layout/comparisons/mithril.rendering.html | 20 + docs/layout/comparisons/mithril.safety.html | 19 + docs/layout/guide.html | 59 ++ docs/layout/index.html | 196 +++++ docs/layout/lib/prism/prism.css | 126 +++ docs/layout/lib/prism/prism.js | 9 + docs/layout/pages.json | 4 + docs/layout/style.css | 86 +++ docs/layout/tools/template-compiler.sjs | 64 ++ docs/layout/tools/template-converter.html | 9 + docs/layout/tools/template-converter.js | 89 +++ docs/mithril.computation.md | 122 +++ docs/mithril.deferred.md | 85 ++ docs/mithril.md | 323 ++++++++ docs/mithril.module.md | 138 ++++ docs/mithril.prop.md | 87 +++ docs/mithril.redraw.md | 27 + docs/mithril.render.md | 67 ++ docs/mithril.request.md | 371 +++++++++ docs/mithril.route.md | 225 ++++++ docs/mithril.sync.md | 48 ++ docs/mithril.trust.md | 68 ++ docs/mithril.withAttr.md | 68 ++ docs/practices.md | 106 +++ docs/refactoring.md | 6 + docs/roadmap.md | 24 + docs/routing.md | 107 +++ docs/tools.md | 31 + docs/web-services.md | 226 ++++++ mithril.js | 415 ++++++++++ package.json | 19 + tests/index.html | 7 + tests/mithril-tests.js | 226 ++++++ tests/mock.js | 71 ++ tests/test.js | 13 + 81 files changed, 6702 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 Gruntfile.js create mode 100644 archive/v0.1/comparisons/angular.parsing.html create mode 100644 archive/v0.1/comparisons/angular.rendering.html create mode 100644 archive/v0.1/comparisons/angular.safety.html create mode 100644 archive/v0.1/comparisons/backbone.parsing.html create mode 100644 archive/v0.1/comparisons/backbone.rendering.html create mode 100644 archive/v0.1/comparisons/backbone.safety.html create mode 100644 archive/v0.1/comparisons/jquery.parsing.html create mode 100644 archive/v0.1/comparisons/jquery.rendering.html create mode 100644 archive/v0.1/comparisons/jquery.safety.html create mode 100644 archive/v0.1/comparisons/mithril.parsing.html create mode 100644 archive/v0.1/comparisons/mithril.rendering.html create mode 100644 archive/v0.1/comparisons/mithril.safety.html create mode 100644 archive/v0.1/index.html create mode 100644 archive/v0.1/lib/prism/prism.css create mode 100644 archive/v0.1/lib/prism/prism.js create mode 100644 archive/v0.1/mithril-tests.js create mode 100644 archive/v0.1/mithril.min.js create mode 100644 archive/v0.1/mithril.min.map create mode 100644 archive/v0.1/mithril.min.zip create mode 100644 archive/v0.1/pages.json create mode 100644 archive/v0.1/style.css create mode 100644 archive/v0.1/tools/template-compiler.sjs create mode 100644 archive/v0.1/tools/template-converter.html create mode 100644 archive/v0.1/tools/template-converter.js create mode 100644 docs/auto-redrawing.md create mode 100644 docs/change-log.md create mode 100644 docs/comparison.md create mode 100644 docs/compiling-templates.md create mode 100644 docs/components.md create mode 100644 docs/getting-started.md create mode 100644 docs/how-to-read-signatures.md create mode 100644 docs/integration.md create mode 100644 docs/layout/api.html create mode 100644 docs/layout/comparisons/angular.parsing.html create mode 100644 docs/layout/comparisons/angular.rendering.html create mode 100644 docs/layout/comparisons/angular.safety.html create mode 100644 docs/layout/comparisons/backbone.parsing.html create mode 100644 docs/layout/comparisons/backbone.rendering.html create mode 100644 docs/layout/comparisons/backbone.safety.html create mode 100644 docs/layout/comparisons/jquery.parsing.html create mode 100644 docs/layout/comparisons/jquery.rendering.html create mode 100644 docs/layout/comparisons/jquery.safety.html create mode 100644 docs/layout/comparisons/mithril.parsing.html create mode 100644 docs/layout/comparisons/mithril.rendering.html create mode 100644 docs/layout/comparisons/mithril.safety.html create mode 100644 docs/layout/guide.html create mode 100644 docs/layout/index.html create mode 100644 docs/layout/lib/prism/prism.css create mode 100644 docs/layout/lib/prism/prism.js create mode 100644 docs/layout/pages.json create mode 100644 docs/layout/style.css create mode 100644 docs/layout/tools/template-compiler.sjs create mode 100644 docs/layout/tools/template-converter.html create mode 100644 docs/layout/tools/template-converter.js create mode 100644 docs/mithril.computation.md create mode 100644 docs/mithril.deferred.md create mode 100644 docs/mithril.md create mode 100644 docs/mithril.module.md create mode 100644 docs/mithril.prop.md create mode 100644 docs/mithril.redraw.md create mode 100644 docs/mithril.render.md create mode 100644 docs/mithril.request.md create mode 100644 docs/mithril.route.md create mode 100644 docs/mithril.sync.md create mode 100644 docs/mithril.trust.md create mode 100644 docs/mithril.withAttr.md create mode 100644 docs/practices.md create mode 100644 docs/refactoring.md create mode 100644 docs/roadmap.md create mode 100644 docs/routing.md create mode 100644 docs/tools.md create mode 100644 docs/web-services.md create mode 100644 mithril.js create mode 100644 package.json create mode 100644 tests/index.html create mode 100644 tests/mithril-tests.js create mode 100644 tests/mock.js create mode 100644 tests/test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b512c09d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 00000000..98a43966 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,116 @@ +module.exports = function(grunt) { + + var version = "0.1" + + var inputFolder = "./docs" + var tempFolder = "./temp" + var archiveFolder = "./archive" + var outputFolder = "../public" + + var guideLayout = "guide" + var guide = [ + "auto-redrawing", + "compiling-templates", + "comparison", + "components", + "getting-started", + "integration", + "practices", + "refactoring", + "routing", + "tools", + "web-services" + ] + var apiLayout = "api" + var api = [ + "change-log", + "roadmap", + "how-to-read-signatures", + "mithril", + "mithril.computation", + "mithril.deferred", + "mithril.module", + "mithril.prop", + "mithril.redraw", + "mithril.render", + "mithril.request", + "mithril.route", + "mithril.sync", + "mithril.trust", + "mithril.withAttr", + "mithril.xhr" + ] + + + + var md2htmlTasks = {} + var makeTasks = function(layout, pages) { + pages.map(function(name) { + md2htmlTasks[name] = { + options: {layout: inputFolder + "/layout/" + layout + ".html"}, + files: [{src: [inputFolder + "/" + name + ".md"], dest: tempFolder + "/" + name + ".html"}] + } + }) + } + makeTasks("guide", guide) + makeTasks("api", api) + + var currentVersionArchiveFolder = archiveFolder + "/v" + version + grunt.initConfig({ + md2html: md2htmlTasks, + uglify: { + options: {banner: "/*\nMithril v" + version + "\nhttp://github.com/lhorie/mithril.js\n(c) Leo Horie\nLicense: MIT\n*/", sourceMap: true}, + mithril: {src: "mithril.js", dest: currentVersionArchiveFolder + "/mithril.min.js"} + }, + concat: { + test: {src: ["mithril.js", "./tests/test.js", "./tests/mock.js", "./tests/mithril-tests.js"], dest: currentVersionArchiveFolder + "/mithril-tests.js"} + }, + zip: { + distribution: { + cwd: currentVersionArchiveFolder + "/", + src: [currentVersionArchiveFolder + "/mithril.min.js", currentVersionArchiveFolder + "/mithril.min.map"], + dest: currentVersionArchiveFolder + "/mithril.min.zip" + } + }, + replace: { + options: {force: true, patterns: [{match: /\.md/g, replacement: ".html"}, {match: /\$version/g, replacement: version}]}, + links: {expand: true, flatten: true, src: [tempFolder + "/**/*.html"], dest: outputFolder + "/"}, + index: {src: inputFolder + "/layout/index.html", dest: currentVersionArchiveFolder + "/index.html"}, + }, + copy: { + style: {src: inputFolder + "/layout/style.css", dest: currentVersionArchiveFolder + "/style.css"}, + pages: {src: inputFolder + "/layout/pages.json", dest: currentVersionArchiveFolder + "/pages.json"}, + lib: {expand: true, cwd: inputFolder + "/layout/lib/", src: "./**", dest: currentVersionArchiveFolder + "/lib/"}, + tools: {expand: true, cwd: inputFolder + "/layout/tools/", src: "./**", dest: currentVersionArchiveFolder + "/tools/"}, + comparisons: {expand: true, cwd: inputFolder + "/layout/comparisons/", src: "./**", dest: currentVersionArchiveFolder + "/comparisons/"}, + publish: {expand: true, cwd: currentVersionArchiveFolder, src: "./**", dest: outputFolder}, + archive: {expand: true, cwd: currentVersionArchiveFolder, src: "./**", dest: outputFolder + "/archive/v" + version} + }, + execute: { + tests: {src: [currentVersionArchiveFolder + "/mithril-tests.js"]} + }, + clean: { + options: {force: true}, + generated: [tempFolder] + }, + watch: { + files: ["./**/*"], + tasks: ["build"] + } + }); + + grunt.loadNpmTasks("grunt-contrib-clean"); + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks("grunt-contrib-copy"); + grunt.loadNpmTasks("grunt-contrib-uglify"); + grunt.loadNpmTasks("grunt-contrib-watch"); + grunt.loadNpmTasks('grunt-execute'); + grunt.loadNpmTasks("grunt-md2html"); + grunt.loadNpmTasks("grunt-replace"); + grunt.loadNpmTasks('grunt-zip'); + + grunt.registerTask("build", ["test", "uglify", "zip", "md2html", "replace", "copy", "clean"]); + grunt.registerTask("test", ["concat", "execute"]); + grunt.registerTask("default", ["build", "watch"]); + +}; diff --git a/README.md b/README.md index 6f411899..bfdce655 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -mithril.js -========== +# Mithril A Javascript Framework for Building Brilliant Applications + +See the [website](http://lhorie.github.io/mithril) for documentation \ No newline at end of file diff --git a/archive/v0.1/comparisons/angular.parsing.html b/archive/v0.1/comparisons/angular.parsing.html new file mode 100644 index 00000000..642d6448 --- /dev/null +++ b/archive/v0.1/comparisons/angular.parsing.html @@ -0,0 +1,2 @@ + +To run an execution time test on this page, run the profiler from your browser's developer tools and measure the running time of a page refresh. (Lower is better) \ No newline at end of file diff --git a/archive/v0.1/comparisons/angular.rendering.html b/archive/v0.1/comparisons/angular.rendering.html new file mode 100644 index 00000000..8cf81d82 --- /dev/null +++ b/archive/v0.1/comparisons/angular.rendering.html @@ -0,0 +1,14 @@ + + + +

To run an execution time test on this page, run the profiler from your browser's developer tools and measure the running time of a page refresh. (Lower is better)

+
+ +
+ + + diff --git a/archive/v0.1/comparisons/angular.safety.html b/archive/v0.1/comparisons/angular.safety.html new file mode 100644 index 00000000..01403424 --- /dev/null +++ b/archive/v0.1/comparisons/angular.safety.html @@ -0,0 +1,13 @@ + + + +
+ +
+ + + diff --git a/archive/v0.1/comparisons/backbone.parsing.html b/archive/v0.1/comparisons/backbone.parsing.html new file mode 100644 index 00000000..e30e1eaf --- /dev/null +++ b/archive/v0.1/comparisons/backbone.parsing.html @@ -0,0 +1,4 @@ + + + +To run an execution time test on this page, run the profiler from your browser's developer tools and measure the running time of a page refresh. (Lower is better) \ No newline at end of file diff --git a/archive/v0.1/comparisons/backbone.rendering.html b/archive/v0.1/comparisons/backbone.rendering.html new file mode 100644 index 00000000..07f38489 --- /dev/null +++ b/archive/v0.1/comparisons/backbone.rendering.html @@ -0,0 +1,30 @@ + + + + + + + +

To run an execution time test on this page, run the profiler from your browser's developer tools and measure the running time of a page refresh. (Lower is better)

+
+ + + + + diff --git a/archive/v0.1/comparisons/backbone.safety.html b/archive/v0.1/comparisons/backbone.safety.html new file mode 100644 index 00000000..e5a71e08 --- /dev/null +++ b/archive/v0.1/comparisons/backbone.safety.html @@ -0,0 +1,29 @@ + + + + + + + +
+ + + + + diff --git a/archive/v0.1/comparisons/jquery.parsing.html b/archive/v0.1/comparisons/jquery.parsing.html new file mode 100644 index 00000000..1fa16592 --- /dev/null +++ b/archive/v0.1/comparisons/jquery.parsing.html @@ -0,0 +1,2 @@ + +To run an execution time test on this page, run the profiler from your browser's developer tools and measure the running time of a page refresh. (Lower is better) \ No newline at end of file diff --git a/archive/v0.1/comparisons/jquery.rendering.html b/archive/v0.1/comparisons/jquery.rendering.html new file mode 100644 index 00000000..dde611ec --- /dev/null +++ b/archive/v0.1/comparisons/jquery.rendering.html @@ -0,0 +1,17 @@ + + + +

To run an execution time test on this page, run the profiler from your browser's developer tools and measure the running time of a page refresh. (Lower is better)

+
+ + + diff --git a/archive/v0.1/comparisons/jquery.safety.html b/archive/v0.1/comparisons/jquery.safety.html new file mode 100644 index 00000000..e9667269 --- /dev/null +++ b/archive/v0.1/comparisons/jquery.safety.html @@ -0,0 +1,16 @@ + + + +
+ + + diff --git a/archive/v0.1/comparisons/mithril.parsing.html b/archive/v0.1/comparisons/mithril.parsing.html new file mode 100644 index 00000000..c2957a15 --- /dev/null +++ b/archive/v0.1/comparisons/mithril.parsing.html @@ -0,0 +1,2 @@ + +To run an execution time test on this page, run the profiler from your browser's developer tools and measure the running time of a page refresh. (Lower is better) \ No newline at end of file diff --git a/archive/v0.1/comparisons/mithril.rendering.html b/archive/v0.1/comparisons/mithril.rendering.html new file mode 100644 index 00000000..717f5d5e --- /dev/null +++ b/archive/v0.1/comparisons/mithril.rendering.html @@ -0,0 +1,20 @@ + + + +

To run an execution time test on this page, run the profiler from your browser's developer tools and measure the running time of a page refresh. (Lower is better)

+
+ + + diff --git a/archive/v0.1/comparisons/mithril.safety.html b/archive/v0.1/comparisons/mithril.safety.html new file mode 100644 index 00000000..1e6ba153 --- /dev/null +++ b/archive/v0.1/comparisons/mithril.safety.html @@ -0,0 +1,19 @@ + + + +
+ + + diff --git a/archive/v0.1/index.html b/archive/v0.1/index.html new file mode 100644 index 00000000..06ad8341 --- /dev/null +++ b/archive/v0.1/index.html @@ -0,0 +1,196 @@ + + + + Mithril + + + + + +
+ +
+ +
+
+
+

Mithril

+ +

A Javascript Framework for Building Brilliant Applications

+ + Guide + Download v0.1 +
+
+ +
+
+
+

Light-weight

+
    +
  • Only 3kb gzipped, no dependencies
  • +
  • Small API, small learning curve
  • +
+
+ +
+

Robust

+
    +
  • Safe-by-default templates
  • +
  • Hierarchical MVC via components
  • +
+
+ +
+

Fast

+
    +
  • Virtual DOM diffing and compilable templates
  • +
  • Intelligent auto-redrawing system
  • +
+
+
+
+ +
+
+
+

Sample code

+ +
//namespace
+var app = {};
+
+//model
+app.PageList = function() {
+	return m.request({method: "GET", url: "pages.json"});
+};
+
+//controller
+app.controller = function() {
+	this.pages = app.PageList();
+};
+
+//view
+app.view = function(ctrl) {
+	return ctrl.pages().map(function(page) {
+		return m("a", {href: page.url}, page.title);
+	});
+};
+
+//initialize
+m.module(document.getElementById("example"), app);
+ +
+
+

Output

+ + + + +
+
+ +
+ +
+
+

Performance

+

To run the execution time tests below, click on their respective links, run the profiler from your desired browser's developer tools and measure the running time of a page refresh. (Lower is better)

+
+
+

Loading

+ + + + + +
Mithril 0.28ms
jQuery 13.11ms
Backbone 18.54ms
Angular 7.49ms
+
+
+

Rendering

+ + + + + +
Mithril 9.44ms (uncompiled)
jQuery 40.27ms
Backbone 23.05ms
Angular 118.63ms
+
+
+
+
+ +
+
+
+
+

Safety

+

Mithril templates are safe by default, i.e. you can't unintentionally create security holes.

+

To run the tests for each framework, click on the respective links. If you see an alert box, ensuring security with that framework is more work for you.

+
+
+

Test Summary

+ Mithril (pass) ✓
+ jQuery (fail) ✗
+ Backbone (fail) ✗
+ Angular (pass) ✓
+
+
+
+
+ +
+
+
+

Guide

+

Build a simple app, learn the ropes

+

Read Guide

+
+ +
+

API

+

Docs and code samples for your reference

+

Read Docs

+
+
+
+
+ + + + \ No newline at end of file diff --git a/archive/v0.1/lib/prism/prism.css b/archive/v0.1/lib/prism/prism.css new file mode 100644 index 00000000..1e61e11d --- /dev/null +++ b/archive/v0.1/lib/prism/prism.css @@ -0,0 +1,126 @@ +/** + * prism.js default theme for JavaScript, CSS and HTML + * Based on dabblet (http://dabblet.com) + * @author Lea Verou + */ + +code[class*="language-"], +pre[class*="language-"] { + color: black; + text-shadow: 0 1px white; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { + text-shadow: none; + background: #b3d4fc; +} + +pre[class*="language-"]::selection, pre[class*="language-"] ::selection, +code[class*="language-"]::selection, code[class*="language-"] ::selection { + text-shadow: none; + background: #b3d4fc; +} + +@media print { + code[class*="language-"], + pre[class*="language-"] { + text-shadow: none; + } +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +:not(pre) > code[class*="language-"], +pre[class*="language-"] { + background: #f5f2f0; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: .1em; + border-radius: .3em; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: slategray; +} + +.token.punctuation { + color: #999; +} + +.namespace { + opacity: .7; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol { + color: #905; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.builtin { + color: #690; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string, +.token.variable { + color: #a67f59; + background: hsla(0,0%,100%,.5); +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: #07a; +} + + +.token.regex, +.token.important { + color: #e90; +} + +.token.important { + font-weight: bold; +} + +.token.entity { + cursor: help; +} + diff --git a/archive/v0.1/lib/prism/prism.js b/archive/v0.1/lib/prism/prism.js new file mode 100644 index 00000000..b7f84870 --- /dev/null +++ b/archive/v0.1/lib/prism/prism.js @@ -0,0 +1,9 @@ +/** + * Prism: Lightweight, robust, elegant syntax highlighting + * MIT license http://www.opensource.org/licenses/mit-license.php/ + * @author Lea Verou http://lea.verou.me + */(function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=self.Prism={util:{type:function(e){return Object.prototype.toString.call(e).match(/\[object (\w+)\]/)[1]},clone:function(e){var n=t.util.type(e);switch(n){case"Object":var r={};for(var i in e)e.hasOwnProperty(i)&&(r[i]=t.util.clone(e[i]));return r;case"Array":return e.slice()}return e}},languages:{extend:function(e,n){var r=t.util.clone(t.languages[e]);for(var i in n)r[i]=n[i];return r},insertBefore:function(e,n,r,i){i=i||t.languages;var s=i[e],o={};for(var u in s)if(s.hasOwnProperty(u)){if(u==n)for(var a in r)r.hasOwnProperty(a)&&(o[a]=r[a]);o[u]=s[u]}return i[e]=o},DFS:function(e,n){for(var r in e){n.call(e,r,e[r]);t.util.type(e)==="Object"&&t.languages.DFS(e[r],n)}}},highlightAll:function(e,n){var r=document.querySelectorAll('code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code');for(var i=0,s;s=r[i++];)t.highlightElement(s,e===!0,n)},highlightElement:function(r,i,s){var o,u,a=r;while(a&&!e.test(a.className))a=a.parentNode;if(a){o=(a.className.match(e)||[,""])[1];u=t.languages[o]}if(!u)return;r.className=r.className.replace(e,"").replace(/\s+/g," ")+" language-"+o;a=r.parentNode;/pre/i.test(a.nodeName)&&(a.className=a.className.replace(e,"").replace(/\s+/g," ")+" language-"+o);var f=r.textContent;if(!f)return;f=f.replace(/&/g,"&").replace(/e.length)break e;if(p instanceof i)continue;a.lastIndex=0;var d=a.exec(p);if(d){l&&(c=d[1].length);var v=d.index-1+c,d=d[0].slice(c),m=d.length,g=v+m,y=p.slice(0,v+1),b=p.slice(g+1),w=[h,1];y&&w.push(y);var E=new i(u,f?t.tokenize(d,f):d);w.push(E);b&&w.push(b);Array.prototype.splice.apply(s,w)}}}return s},hooks:{all:{},add:function(e,n){var r=t.hooks.all;r[e]=r[e]||[];r[e].push(n)},run:function(e,n){var r=t.hooks.all[e];if(!r||!r.length)return;for(var i=0,s;s=r[i++];)s(n)}}},n=t.Token=function(e,t){this.type=e;this.content=t};n.stringify=function(e,r,i){if(typeof e=="string")return e;if(Object.prototype.toString.call(e)=="[object Array]")return e.map(function(t){return n.stringify(t,r,e)}).join("");var s={type:e.type,content:n.stringify(e.content,r,i),tag:"span",classes:["token",e.type],attributes:{},language:r,parent:i};s.type=="comment"&&(s.attributes.spellcheck="true");t.hooks.run("wrap",s);var o="";for(var u in s.attributes)o+=u+'="'+(s.attributes[u]||"")+'"';return"<"+s.tag+' class="'+s.classes.join(" ")+'" '+o+">"+s.content+""};if(!self.document){self.addEventListener("message",function(e){var n=JSON.parse(e.data),r=n.language,i=n.code;self.postMessage(JSON.stringify(t.tokenize(i,t.languages[r])));self.close()},!1);return}var r=document.getElementsByTagName("script");r=r[r.length-1];if(r){t.filename=r.src;document.addEventListener&&!r.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)}})();; +Prism.languages.markup={comment:/<!--[\w\W]*?-->/g,prolog:/<\?.+?\?>/,doctype:/<!DOCTYPE.+?>/,cdata:/<!\[CDATA\[[\w\W]*?]]>/i,tag:{pattern:/<\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|\w+))?\s*)*\/?>/gi,inside:{tag:{pattern:/^<\/?[\w:-]+/i,inside:{punctuation:/^<\/?/,namespace:/^[\w-]+?:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/gi,inside:{punctuation:/=|>|"/g}},punctuation:/\/?>/g,"attr-name":{pattern:/[\w:-]+/g,inside:{namespace:/^[\w-]+?:/}}}},entity:/&#?[\da-z]{1,8};/gi};Prism.hooks.add("wrap",function(e){e.type==="entity"&&(e.attributes.title=e.content.replace(/&/,"&"))});; +Prism.languages.clike={comment:{pattern:/(^|[^\\])(\/\*[\w\W]*?\*\/|(^|[^:])\/\/.*?(\r?\n|$))/g,lookbehind:!0},string:/("|')(\\?.)*?\1/g,"class-name":{pattern:/((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/ig,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/g,"boolean":/\b(true|false)\b/g,"function":{pattern:/[a-z0-9_]+\(/ig,inside:{punctuation:/\(/}}, number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/g,operator:/[-+]{1,2}|!|<=?|>=?|={1,3}|(&){1,2}|\|?\||\?|\*|\/|\~|\^|\%/g,ignore:/&(lt|gt|amp);/gi,punctuation:/[{}[\];(),.:]/g}; +; +Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(var|let|if|else|while|do|for|return|in|instanceof|function|get|set|new|with|typeof|try|throw|catch|finally|null|break|continue)\b/g,number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?|NaN|-?Infinity)\b/g});Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/g,lookbehind:!0}});Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/(<|<)script[\w\W]*?(>|>)[\w\W]*?(<|<)\/script(>|>)/ig,inside:{tag:{pattern:/(<|<)script[\w\W]*?(>|>)|(<|<)\/script(>|>)/ig,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.javascript}}});; diff --git a/archive/v0.1/mithril-tests.js b/archive/v0.1/mithril-tests.js new file mode 100644 index 00000000..51036a2c --- /dev/null +++ b/archive/v0.1/mithril-tests.js @@ -0,0 +1,725 @@ +new function(window) { + var selectorCache = {} + var type = {}.toString + var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.+?)\2)?\]/ + + Mithril = m = function() { + var args = arguments + var hasAttrs = type.call(args[1]) == "[object Object]" + var attrs = hasAttrs ? args[1] : {} + var classAttrName = "class" in attrs ? "class" : "className" + var cell = selectorCache[args[0]] + if (cell === undefined) { + selectorCache[args[0]] = 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] || true + } + } + if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" ") + } + cell = clone(cell) + cell.attrs = clone(cell.attrs) + 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(parent, data, cached) { + if (data === null || data === undefined) return + + var cachedType = type.call(cached), dataType = type.call(data) + if (cachedType != dataType) { + if (cached !== null && cached !== undefined) clear(cached.nodes) + cached = new data.constructor + cached.nodes = [] + } + + if (dataType == "[object Array]") { + var nodes = [], intact = cached.length === data.length + for (var i = 0; i < data.length; i++) { + var item = build(parent, data[i], cached[i]) + if (!item.nodes.intact) intact = false + cached[i] = item + } + if (!intact) { + for (var i = 0; i < data.length; i++) nodes = nodes.concat(cached[i].nodes) + for (var i = nodes.length, node; node = cached.nodes[i]; i++) if (node.parentNode !== null) node.parentNode.removeChild(node) + for (var i = cached.nodes.length, node; node = nodes[i]; i++) if (node.parentNode === null) parent.appendChild(node) + cached.length = data.length + cached.nodes = nodes + } + } + else if (dataType == "[object Object]") { + if (data.tag != cached.tag || Object.keys(data.attrs).join() != Object.keys(cached.attrs).join()) clear(cached.nodes) + + var node, isNew = cached.nodes.length === 0 + if (isNew) { + node = window.document.createElement(data.tag) + cached = {tag: data.tag, attrs: setAttributes(node, data.attrs, {}), children: build(node, data.children, cached.children), nodes: [node]} + parent.appendChild(node) + } + else { + node = cached.nodes[0] + setAttributes(node, data.attrs, cached.attrs) + cached.children = build(node, data.children, cached.children) + cached.nodes.intact = true + } + if (type.call(data.attrs["config"]) == "[object Function]") data.attrs["config"](node, !isNew) + } + else { + var node + if (cached.nodes.length === 0) { + if (data.$trusted) { + var lastChild = parent.lastChild + parent.insertAdjacentHTML("beforeend", data) + node = lastChild ? lastChild.nextSibling : parent.firstChild + } + else { + node = window.document.createTextNode(data) + parent.appendChild(node) + } + cached = "string number boolean".indexOf(typeof data) > -1 ? new data.constructor(data) : data + cached.nodes = [node] + } + else if (cached.valueOf() !== data.valueOf()) { + if (data.$trusted) { + var current = cached.nodes[0], nodes = [current] + if (current) { + while (current = current.nextSibling) nodes.push(current) + clear(nodes) + var lastChild = parent.lastChild + parent.insertAdjacentHTML("beforeend", data) + node = lastChild ? lastChild.nextSibling : parent.firstChild + } + else parent.innerHTML = data + } + else { + node = cached.nodes[0] + parent.appendChild(node) + node.nodeValue = data + } + cached = new data.constructor(data) + cached.nodes = [node] + } + else cached.nodes.intact = true + } + + return cached + } + function setAttributes(node, dataAttrs, cachedAttrs) { + for (var attrName in dataAttrs) { + var dataAttr = dataAttrs[attrName] + if (!(attrName in cachedAttrs) || (cachedAttrs[attrName] !== dataAttr)) { + cachedAttrs[attrName] = dataAttr + if (attrName == "config") continue + if (attrName.indexOf("on") == 0 && typeof dataAttr == "function") dataAttr = autoredraw(dataAttr, node) + if (attrName == "style") for (var rule in dataAttr) node.style[rule] = dataAttr[rule] + else if (attrName in node) node[attrName] = dataAttr + else node.setAttribute(attrName, dataAttr) + } + } + return cachedAttrs + } + function clear(nodes) { + for (var i = 0; i < nodes.length; i++) nodes[i].parentNode.removeChild(nodes[i]) + nodes.length = 0 + } + function clone(object) { + var result = {} + for (var prop in object) result[prop] = object[prop] + return result + } + function autoredraw(callback, object) { + return function() { + m.startComputation() + var output = callback.apply(object || window, arguments) + m.endComputation() + return output + } + } + + 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 !== html) { + window.document.replaceChild(html, window.document.documentElement) + } + } + } + var nodeCache = [], cellCache = {} + m.render = function(root, cell) { + var index = nodeCache.indexOf(root) + var id = index < 0 ? nodeCache.push(root) - 1 : index + var node = root == window.document || root == window.document.documentElement ? documentNode : root + cellCache[id] = build(node, cell, cellCache[id]) + } + + m.trust = function(value) { + value = new String(value) + value.$trusted = true + return value + } + + var currentRoot, currentModule = {view: function() {}}, currentController = {}, now = 0, lastRedraw = 0, lastRedrawId = 0 + m.module = function(root, module) { + m.startComputation() + currentRoot = root + currentModule = module + currentController = new module.controller + m.endComputation() + } + m.redraw = function() { + m.render(currentRoot || window.document, currentModule.view(currentController)) + lastRedraw = now + } + function redraw() { + now = window.performance && window.performance.now ? window.performance.now() : new window.Date().getTime() + if (now - lastRedraw > 16) m.redraw() + else { + var cancel = window.cancelAnimationFrame || window.clearTimeout + var defer = window.requestAnimationFrame || window.setTimeout + cancel(lastRedrawId) + lastRedrawId = defer(m.redraw, 0) + } + } + + var pendingRequests = 0, computePostRedrawHook = null + m.startComputation = function() {pendingRequests++} + m.endComputation = function() { + pendingRequests = Math.max(pendingRequests - 1, 0) + if (pendingRequests == 0) { + redraw() + if (computePostRedrawHook) { + computePostRedrawHook() + computePostRedrawHook = null + } + } + } + + m.withAttr = function(prop, withAttrCallback) { + return function(e) {withAttrCallback(prop in e.currentTarget ? e.currentTarget[prop] : e.currentTarget.getAttribute(prop))} + } + + //routing + var modes = {pathname: "", hash: "#", search: "?"} + var redirect = function() {}, routeParams = {} + m.route = function() { + if (arguments.length == 3) { + var root = arguments[0], defaultRoute = arguments[1], router = arguments[2] + redirect = function(source) { + var path = source.slice(modes[m.route.mode].length) + if (!routeByValue(root, router, path)) { + m.route(defaultRoute, true) + } + } + var listener = m.route.mode == "hash" ? "onhashchange" : "onpopstate" + window[listener] = function() { + redirect(window.location[m.route.mode]) + } + computePostRedrawHook = scrollToHash + window[listener]() + } + else if (arguments[0].addEventListener) { + var element = arguments[0] + var isInitialized = arguments[1] + if (!isInitialized) { + element.removeEventListener("click", routeUnobtrusive) + element.addEventListener("click", routeUnobtrusive) + } + } + else if (typeof arguments[0] == "string") { + var route = arguments[0] + var shouldReplaceHistoryEntry = arguments[1] === true + if (window.history.pushState) { + computePostRedrawHook = function() { + window.history[shouldReplaceHistoryEntry ? "replaceState" : "pushState"](null, window.document.title, modes[m.route.mode] + route) + scrollToHash() + } + redirect(modes[m.route.mode] + route) + } + else window.location[m.route.mode] = route + } + } + m.route.param = function(key) {return routeParams[key]} + m.route.mode = "search" + function routeByValue(root, router, path) { + for (var route in router) { + if (route == path) return !void m.module(root, router[route]) + + var matcher = new RegExp("^" + route.replace(/:[^\/]+/g, "([^\\/]+)") + "$") + if (matcher.test(path)) { + return !void path.replace(matcher, function() { + var keys = route.match(/:[^\/]+/g) + var values = [].slice.call(arguments, 1, -2) + routeParams = {} + for (var i = 0; i < keys.length; i++) routeParams[keys[i].slice(1)] = values[i] + m.module(root, router[route]) + }) + } + } + } + function routeUnobtrusive(e) { + e.preventDefault() + m.route(e.currentTarget.getAttribute("href")) + } + function scrollToHash() { + if (m.route.mode != "hash" && window.location.hash) window.location.hash = window.location.hash + } + + //model + m.prop = function(store) { + return function() { + if (arguments.length) store = arguments[0] + return store + } + } + + m.deferred = function() { + var resolvers = [], rejecters = [] + var object = { + resolve: function(value) { + for (var i = 0; i < resolvers.length; i++) resolvers[i](value) + }, + reject: function(value) { + for (var i = 0; i < rejecters.length; i++) rejecters[i](value) + }, + promise: m.prop() + } + object.promise.resolvers = resolvers + object.promise.then = function(success, error) { + var next = m.deferred() + if (!success) success = identity + if (!error) error = identity + resolvers.push(function(value) { + var result = success(value) + next.resolve(result !== undefined ? result : value) + }) + rejecters.push(function(value) { + var result = error(value) + next.reject(result !== undefined ? result : value) + }) + return next.promise + } + return object + } + m.sync = function(args) { + var method = "resolve" + function synchronizer(resolved) { + return function(value) { + results.push(value) + if (!resolved) method = "reject" + if (results.length == args.length) { + deferred.promise(results) + deferred[method](results) + } + return value + } + } + + var deferred = m.deferred() + var results = [] + for (var i = 0; i < args.length; i++) { + args[i].then(synchronizer(true), synchronizer(false)) + } + return deferred.promise + } + function identity(value) {return value} + + function ajax(options) { + var xhr = window.XDomainRequest ? new window.XDomainRequest : new window.XMLHttpRequest + xhr.open(options.method, options.url, true, options.user, options.password) + xhr.onload = typeof options.onload == "function" ? options.onload : function() {} + xhr.onerror = typeof options.onerror == "function" ? options.onerror : function() {} + xhr.withCredentials = true + if (typeof options.config == "function") options.config(xhr, options) + xhr.send(options.data) + return xhr + } + function querystring(object, prefix) { + var str = [] + for(var prop in object) { + var key = prefix ? prefix + "[" + prop + "]" : prop, value = object[prop] + str.push(typeof value == "object" ? querystring(value, key) : encodeURIComponent(key) + "=" + encodeURIComponent(value)) + } + return str.join("&") + } + function bindData(xhrOptions, data, serialize) { + if (data && Object.keys(data).length > 0) { + if (xhrOptions.method == "GET") { + xhrOptions.url = xhrOptions.url + (xhrOptions.url.indexOf("?") < 0 ? "?" : "&") + querystring(data) + } + else xhrOptions.data = serialize(data) + } + return xhrOptions + } + function parameterizeUrl(url, data) { + var tokens = url.match(/:\w+/g) + 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) { + m.startComputation() + var deferred = m.deferred() + var serialize = xhrOptions.serialize || JSON.stringify + var deserialize = xhrOptions.deserialize || JSON.parse + xhrOptions.url = parameterizeUrl(xhrOptions.url, xhrOptions.data) + xhrOptions = bindData(xhrOptions, xhrOptions.data, serialize) + xhrOptions.onload = xhrOptions.onerror = function(e) { + var unwrap = (e.type == "load" ? xhrOptions.unwrapSuccess : xhrOptions.unwrapError) || identity + var response = unwrap(deserialize(e.target.responseText)) + if (response instanceof 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.promise(response) + deferred[e.type == "load" ? "resolve" : "reject"](response) + m.endComputation() + } + ajax(xhrOptions) + deferred.promise.then = propBinder(deferred.promise) + return deferred.promise + } + function propBinder(promise) { + var bind = promise.then + return function(success, error) { + var next = bind(function(value) {return next(success(value))}, function(value) {return next(error(value))}) + next.then = propBinder(next) + return next + } + } + + //testing API + m.deps = function(mock) {return window = mock} +}(this) +function test(condition) { + try {if (!condition()) throw new Error} + catch (e) {test.failures.push(condition)} + test.total++ +} +test.total = 0 +test.failures = [] +test.print = function(print) { + for (var i = 0; i < test.failures.length; i++) { + print(test.failures[i].toString()) + } + print("tests: " + test.total + "\nfailures: " + test.failures.length) +} +var mock = {} +mock.window = new function() { + var window = {} + window.document = {} + window.document.childNodes = [] + window.document.createElement = function(tag) { + return { + childNodes: [], + nodeName: tag.toUpperCase(), + appendChild: window.document.appendChild, + removeChild: window.document.removeChild, + replaceChild: window.document.replaceChild, + setAttribute: function(name, value) { + this[name] = value.toString() + }, + getAttribute: function(name, value) { + return this[name] + }, + } + } + window.document.createTextNode = function(text) { + return {nodeValue: text.toString()} + } + window.document.documentElement = null + window.document.replaceChild = function(newChild, oldChild) { + var index = this.childNodes.indexOf(oldChild) + if (index > -1) this.childNodes.splice(index, 1, newChild) + else this.childNodes.push(newChild) + newChild.parentNode = this + oldChild.parentNode = null + } + window.document.appendChild = function(child) { + this.childNodes.push(child) + child.parentNode = this + } + window.document.removeChild = function(child) { + var index = this.childNodes.indexOf(child) + this.childNodes.splice(index, 1) + child.parentNode = null + } + window.performance = new function () { + var timestamp = 50 + this.$elapse = function(amount) {timestamp = amount} + this.now = function() {return timestamp} + } + window.cancelAnimationFrame = function() {} + window.requestAnimationFrame = function(callback) {callback()} + window.XMLHttpRequest = new function() { + var request = function() { + this.open = function(method, url) { + this.method = method + this.url = url + } + this.send = function() { + this.responseText = JSON.stringify(this) + request.$events.push({type: "load", target: this}) + } + } + request.$events = [] + return request + } + window.location = {search: "", pathname: "", hash: ""}, + window.history = {} + window.history.pushState = function(data, title, url) { + window.location.pathname = window.location.search = window.location.hash = url + }, + window.history.replaceState = function(data, title, url) { + window.location.pathname = window.location.search = window.location.hash = url + } + return window +} +function testMithril(mock) { + m.deps(mock) + + //m + test(function() {return m("div").tag === "div"}) + test(function() {return m(".foo").tag === "div"}) + test(function() {return m(".foo").attrs.className === "foo"}) + test(function() {return m("[title=bar]").tag === "div"}) + test(function() {return m("[title=bar]").attrs.title === "bar"}) + test(function() {return m("[title=\'bar\']").attrs.title === "bar"}) + test(function() {return m("[title=\"bar\"]").attrs.title === "bar"}) + test(function() {return m("div", "test").children === "test"}) + test(function() {return m("div", ["test"]).children[0] === "test"}) + test(function() {return m("div", {title: "bar"}, "test").attrs.title === "bar"}) + test(function() {return m("div", {title: "bar"}, "test").children === "test"}) + test(function() {return m("div", {title: "bar"}, ["test"]).children[0] === "test"}) + test(function() {return m("div", {title: "bar"}, m("div")).children.tag === "div"}) + test(function() {return m("div", {title: "bar"}, [m("div")]).children[0].tag === "div"}) + test(function() {return m("div", ["a", "b"]).children.length === 2}) + test(function() {return m("div", [m("div")]).children[0].tag === "div"}) + test(function() {return m("div", m("div")).attrs.tag === "div"}) //yes, this is expected behavior: see method signature + + //m.module + for (var i = 0; i < 2; i++) { + //first iteration tests immediate rendering + //second iteration tests deferred rendering + test(function() { + var root = mock.document.createElement("div") + m.module(root, { + controller: function() {this.value = "test"}, + view: function(ctrl) {return ctrl.value} + }) + return root.childNodes[0].nodeValue === "test" + }) + } + + //m.withAttr + test(function() { + var value + var handler = m.withAttr("test", function(data) {value = data}) + handler({currentTarget: {test: "foo"}}) + return value === "foo" + }) + + //m.trust + test(function() {return m.trust("test").valueOf() === "test"}) + + //m.render + test(function() { + var root = mock.document.createElement("div") + m.render(root, "test") + return root.childNodes[0].nodeValue === "test" + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("div", {id: "a"})) + var elementBefore = root.childNodes[0] + m.render(root, m("div", {id: "b"})) + var elementAfter = root.childNodes[0] + return elementBefore === elementAfter + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("#a")) + var elementBefore = root.childNodes[0] + m.render(root, m("#b")) + var elementAfter = root.childNodes[0] + return elementBefore === elementAfter + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("div", {id: "a"})) + var elementBefore = root.childNodes[0] + m.render(root, m("div", {title: "b"})) + var elementAfter = root.childNodes[0] + return elementBefore !== elementAfter + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("#a")) + var elementBefore = root.childNodes[0] + m.render(root, m("[title=b]")) + var elementAfter = root.childNodes[0] + return elementBefore !== elementAfter + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("#a")) + var elementBefore = root.childNodes[0] + m.render(root, "test") + var elementAfter = root.childNodes[0] + return elementBefore !== elementAfter + }) + + //m.redraw + test(function() { + var controller + var root = mock.document.createElement("div") + m.module(root, { + controller: function() {controller = this}, + view: function(ctrl) {return ctrl.value} + }) + controller.value = "foo" + m.redraw() + return root.childNodes[0].nodeValue === "foo" + }) + + //m.route + test(function() { + var root = mock.document.createElement("div") + m.route.mode = "search" + m.route(root, "/test1", { + "/test1": {controller: function() {}, view: function() {return "foo"}} + }) + return mock.location.search == "?/test1" && root.childNodes[0].nodeValue === "foo" + }) + test(function() { + var root = mock.document.createElement("div") + m.route.mode = "pathname" + m.route(root, "/test2", { + "/test2": {controller: function() {}, view: function() {return "foo"}} + }) + return mock.location.pathname == "/test2" && root.childNodes[0].nodeValue === "foo" + }) + test(function() { + var root = mock.document.createElement("div") + m.route.mode = "hash" + m.route(root, "/test3", { + "/test3": {controller: function() {}, view: function() {return "foo"}} + }) + return mock.location.hash == "#/test3" && root.childNodes[0].nodeValue === "foo" + }) + test(function() { + var root = mock.document.createElement("div") + m.route.mode = "search" + m.route(root, "/test4/foo", { + "/test4/:test": {controller: function() {}, view: function() {return m.route.param("test")}} + }) + return mock.location.search == "?/test4/foo" && root.childNodes[0].nodeValue === "foo" + }) + + //m.prop + test(function() { + var prop = m.prop("test") + return prop() === "test" + }) + test(function() { + var prop = m.prop("test") + prop("foo") + return prop() == "foo" + }) + + //m.request + test(function() { + var prop = m.request({method: "GET", url: "test"}) + var e = mock.XMLHttpRequest.$events.pop() + e.target.onload(e) + return prop().method === "GET" && prop().url === "test" + }) + test(function() { + var prop = m.request({method: "GET", url: "test"}).then(function(value) {return "foo"}) + var e = mock.XMLHttpRequest.$events.pop() + e.target.onload(e) + return prop() === "foo" + }) + + //m.deferred + test(function() { + var value + var deferred = m.deferred() + deferred.promise.then(function(data) {value = data}) + deferred.resolve("test") + return value === "test" + }) + test(function() { + var value + var deferred = m.deferred() + deferred.promise.then(function(value) {return "foo"}).then(function(data) {value = data}) + deferred.resolve("test") + return value === "foo" + }) + test(function() { + var value + var deferred = m.deferred() + deferred.promise.then(null, function(data) {value = data}) + deferred.reject("test") + return value === "test" + }) + test(function() { + var value + var deferred = m.deferred() + deferred.promise.then(null, function(value) {return "foo"}).then(null, function(data) {value = data}) + deferred.reject("test") + return value === "foo" + }) + + //m.sync + test(function() { + var value + var deferred1 = m.deferred() + var deferred2 = m.deferred() + m.sync([deferred1.promise, deferred2.promise]).then(function(data) {value = data}) + deferred1.resolve("test") + deferred2.resolve("foo") + return value[0] === "test" && value[1] === "foo" + }) + + //m.startComputation/m.endComputation + test(function() { + var controller + var root = mock.document.createElement("div") + m.module(root, { + controller: function() {controller = this}, + view: function(ctrl) {return ctrl.value} + }) + m.startComputation() + controller.value = "foo" + m.endComputation() + return root.childNodes[0].nodeValue === "foo" + }) +} + +//mocks +testMithril(mock.window) + +test.print(console.log) \ No newline at end of file diff --git a/archive/v0.1/mithril.min.js b/archive/v0.1/mithril.min.js new file mode 100644 index 00000000..87daecf8 --- /dev/null +++ b/archive/v0.1/mithril.min.js @@ -0,0 +1,8 @@ +/* +Mithril v0.1 +http://github.com/lhorie/mithril.js +(c) Leo Horie +License: MIT +*/ +!new function(a){function b(e,f,g){if(null!==f&&void 0!==f){var h=s.call(g),i=s.call(f);if(h!=i&&(null!==g&&void 0!==g&&d(g.nodes),g=new f.constructor,g.nodes=[]),"[object Array]"==i){for(var j=[],k=g.length===f.length,l=0;l-1?new f.constructor(f):f,g.nodes=[n]}else if(g.valueOf()!==f.valueOf()){if(f.$trusted){var q=g.nodes[0],j=[q];if(q){for(;q=q.nextSibling;)j.push(q);d(j);var p=e.lastChild;e.insertAdjacentHTML("beforeend",f),n=p?p.nextSibling:e.firstChild}else e.innerHTML=f}else n=g.nodes[0],e.appendChild(n),n.nodeValue=f;g=new f.constructor(f),g.nodes=[n]}else g.nodes.intact=!0}return g}}function c(a,b,c){for(var d in b){var e=b[d];if(!(d in c)||c[d]!==e){if(c[d]=e,"config"==d)continue;if(0==d.indexOf("on")&&"function"==typeof e&&(e=f(e,a)),"style"==d)for(var g in e)a.style[g]=e[g];else d in a?a[d]=e:a.setAttribute(d,e)}}return c}function d(a){for(var b=0;b16)m.redraw();else{var b=a.cancelAnimationFrame||a.clearTimeout,c=a.requestAnimationFrame||a.setTimeout;b(E),E=c(m.redraw,0)}}function h(a,b,c){for(var d in b){if(d==c)return!void m.module(a,b[d]);var e=new RegExp("^"+d.replace(/:[^\/]+/g,"([^\\/]+)")+"$");if(e.test(c))return!void c.replace(e,function(){var c=d.match(/:[^\/]+/g),e=[].slice.call(arguments,1,-2);J={};for(var f=0;f0&&("GET"==a.method?a.url=a.url+(a.url.indexOf("?")<0?"?":"&")+n(b):a.data=c(b)),a}function p(a,b){var c=a.match(/:\w+/g);if(c&&b)for(var d=0;d0&&(f.attrs[d]=h.join(" "))}f=e(f),f.attrs=e(f.attrs),f.children=b?a[2]:a[1];for(var j in c)f.attrs[j]=j==d?(f.attrs[j]||"")+" "+c[j]:c[j];return f};var v,w={insertAdjacentHTML:function(b,c){a.document.write(c),a.document.close()},appendChild:function(b){void 0===v&&(v=a.document.createElement("html")),"HTML"==b.nodeName?v=b:v.appendChild(b),a.document.documentElement!==v&&a.document.replaceChild(v,a.document.documentElement)}},x=[],y={};m.render=function(c,d){var e=x.indexOf(c),f=0>e?x.push(c)-1:e,g=c==a.document||c==a.document.documentElement?w:c;y[f]=b(g,d,y[f])},m.trust=function(a){return a=new String(a),a.$trusted=!0,a};var z,A={view:function(){}},B={},C=0,D=0,E=0;m.module=function(a,b){m.startComputation(),z=a,A=b,B=new b.controller,m.endComputation()},m.redraw=function(){m.render(z||a.document,A.view(B)),D=C};var F=0,G=null;m.startComputation=function(){F++},m.endComputation=function(){F=Math.max(F-1,0),0==F&&(g(),G&&(G(),G=null))},m.withAttr=function(a,b){return function(c){b(a in c.currentTarget?c.currentTarget[a]:c.currentTarget.getAttribute(a))}};var H={pathname:"",hash:"#",search:"?"},I=function(){},J={};m.route=function(){if(3==arguments.length){var b=arguments[0],c=arguments[1],d=arguments[2];I=function(a){var e=a.slice(H[m.route.mode].length);h(b,d,e)||m.route(c,!0)};var e="hash"==m.route.mode?"onhashchange":"onpopstate";a[e]=function(){I(a.location[m.route.mode])},G=j,a[e]()}else if(arguments[0].addEventListener){var f=arguments[0],g=arguments[1];g||(f.removeEventListener("click",i),f.addEventListener("click",i))}else if("string"==typeof arguments[0]){var k=arguments[0],l=arguments[1]===!0;a.history.pushState?(G=function(){a.history[l?"replaceState":"pushState"](null,a.document.title,H[m.route.mode]+k),j()},I(H[m.route.mode]+k)):a.location[m.route.mode]=k}},m.route.param=function(a){return J[a]},m.route.mode="search",m.prop=function(a){return function(){return arguments.length&&(a=arguments[0]),a}},m.deferred=function(){var a=[],b=[],c={resolve:function(b){for(var c=0;ceb^P|9R`jXOrl9qq~@d$snHOgTlA8z0uA` zR@dqkz74wh$zn8mX4~E8&#&S^XJeb~!ndnnw$Zoe#V{C+^1XsJNZZ}QJ~s8A*@NfL z)4jdSUbH*;UU3q4qItpU*#ZN=pUjilB240$WhU*j^MaK-i`&B}Og5a^EVw<-+4cZn z#KAQ{q5 ziql{gO_KMZ$@AygUU3$Us7oOSK z$=>>8Au9^|Cq9BRNqli5PhNAQh(Ci6lmH@FA z<|mc`$olqButL%il-_I0MA`-7X|rGngZ9oQ=MUCnk`?vq^;P-SCb?z2_4@H|N zQAagmYDdLUFi$+vW{2$pM~DbmMpP z$?Y_XyBpoYAAjCC_|9nH&^;I|rK(Ntr4rmLUIn8?gcBf)Fc8b$lhnA(@)!2y@|>i+ zR1>g&Xr%aHL-0nKDESwu!`J4>eL;zY9j|QlzCtVKNS?9JM-jr6va2%5@v|>tA%F!3fk3i{B~jwQY7N?pB+7TJsPK^D9j?&sh=(+;rw#wo z{zH%&-!}Y9aMnK081gmK&AGyUy4>Dgof`2}n!dLE?O|!(LjoTZ+GpXp9pGo*u%W%$ z*v$(M%tT6{eBs*|7zeY&i^tPN5{PS8$?Vu|`hd}GKD@sVtYFp?YdSBar;m&D57~WI zC&Tw14u>*$u*B1720D*}NeDiybs=`w2j7k4X*v;X1jP~zccIvelHr}=h7c{ zUKPe5S7#Pn=L^CiLy-XC1+Ex5lfhUpIh+OK2*4LZ7>5~;h!=?!0-{-TxrpY;_bb6$ zvu?kgFBhz`9p=;9tZi^QCEZ5D6OAK7&I=9IU&pK_ZWz%?DglIFMe&i?du>wI&oXXWx}vR%j(HgmtvNx&2p6M&C+ZS9tDxgYUZ z8mOtfgm$M0qJ;g-(J-tCiHh?P0zHk)mQ1#Gte;;L_J7SvSr`vaTh}T>sff# zH7DeE3RIfzIfA5hWFN9*8&HD5@mX{Qhf#{U!D5tvvmi2<0|FJp#f;(2F@T8@@_wak zx&AEb<_d)s7lx3sTtAribI)vZ1aB8P8^wsT5UUnB4+8*&%8oz`*7})KF9@X3m0)$* zC~wRbMd))W9tVTTN0lV;F^>oLRER&<-yI?PHY-#035ziTtbDsTj*@=dvD(GOY-By# zW&=JIpJ_0kU&pf!B9US|8O1@ze!{f1&D=rS*vqi~LAFM-Sv>o#y)r4ArS*9CH4+97 z+6Imcg3%nYhxH^7YM-A!XKul$BOh9A0zQY40Fs>`3D8~95*Y**h~|cn!lf-!W4rVY z2kp(=88l-vcb?npSY6mR_=`{-X-=d4m6`7zO!8eAp)jK zD_!lR2~A|vpQ_43VfO<-mf9T7z&b_9qdL(jN}>&RQp#jNxm-D+pqTWdNh;Dy!3*f` z>89XB^2<@M+fWeBk@!8Vtkg>1I(bA4m|Oapj@)5i=Aj61Fg$h!yJXGrQ%vHf1cFv# zZ{-j6@_)Mj>Hcf60Gs*K*W%Z+ujjCvh5P*1v*PB#*Yg7G>B8RndX`7Gfc17hcmGfK z1?hf$QNWU|eUmoZV|xj?A_HQjPS9%GPvkl#{0`1`K*P{(BV^b)-vg*n+z?UoJO@7= z9+>u?e&54KAyBqwXTjO_xx_-6IWs{8#dO&2YQB=|46bq}U-|a13ITdnpIX$jlVWre zfr2=zuwQ^L%0U`?gh)~yG+&sohI2oyCM5?d4myuk6w?)b)j950ud4~4Oz)oaI^p=h zUMK{;vt2ASna9}x5S-fsjNRWq+kWV0RVH17K(BA~Z$pf{Ez!T3X2&FinKVg={Nj9% zlwR79A>*MC2n$YU!*hFxxbYy}kAeI99KGpAZZkx0k3UNxe%PmoU0K)mx9_u?m3fqS zDwAbi&jxT`A(S?gd5+>a!j=aslT2q$0L7$60#0ASbX-9heVMb?H*Vdqh;qxYfKd^Wt=URT zLdztHk$tl?av%r-hd)LKH!442;pd&b2Mk4KOFbD$Qf*!2;#+o#oiyt_IQdiYEmqmF3dBc=onc!oM<1 zX&TXFZld#gDt5N+>;T8MaR|M?xWi-?k4A9m-$%^EK||Tq(L7g4o4;f5n$pN}ii9=q zLJUf_rzji(!ivqx5P*Cf$!E_z+}z}4>mmEv0=ph0eK?kz{Nc|#h;=QL1P&pV2;{8_ zz7>AuDq8Mwk&yvIv(+Ku(T#~;+D>?ac0Le|To3II!u9@H07YCMT95wd6%^|u``Z-2 zP9Xk!xtzuLZV$H1nj_2)(LBgKSk)C=V5%jdtceh_Gq8fY5WUo}8@r^oti2kCePiN~ zE`zm!QD1doQ#1EL3~53l2)G2-`J*#)v&5$7=_UJpWMxbs@9)h#q9zn~6v@cez}8+1 zY!8s+65p^NOnMO_hj=oLr{H`N<@N(O>@0HCOw79u0ytQ)hZZ-$KBd)<6++wR#h}wE zGe&I z5}&;cS~v- zC2u2C4-gQgm*$tx9_@qn9sn*dlY4q6Yece>gm_|OlwyQ5j_jD0I&#N5OZFcV`+6{UPIDy6NI6Y-ixHk>X7ged`=g|4^z(yZJZ!9 z&cO_3=_e%xdVG`$(i8VtDhOC8APe&CJnPr^-6<@|y}VJxTT|*aOuu(-;1!K*d;NN5 z=E$Crst)c@`uVl#zlU37sd&)6rOAd&Kc0ua&7+j@ny(?-l5`x)C2RZiALd z$l1{JdP#PZ`b5Yrn6txvg+*F0rh=2}Sukxb!Z4c8VGOiiW_k`EZ~?lZpqR&UXeeqQ zpfQrydBj>cXa!f*g&&2jN0kf3ZF%HkD96*6sb3GY@2%dldZ2xm^-QfraA}@>6T|Cf z@VvXW0JlMERh?LYZ=XHFWy$}&RF=-U?s0%YOE_ZuwLC3N=9UAQIW;-c`HJ%^sSfP+ z!$UrSF)!@@xV81u#ynok!l;hO8q95dd}pT*nSB2o2h$I0kEQdw(trKy)(=1M-`_oY zEXCq)xvOY4N3AHgw~PA50M+5WT=u}>JcqKeTshC}o#l$fuV48uU4!{^E60FDrmyi1 z5np3Vdx3WneD%o=4ayTfVG#BsPAm{ZcLx(`>)vaGRNsVkU;<>4_^K zBIYp^J$;!(L-m>?{Fg*ye6SQHF*LXrpVdPmI+@2q^fDO>dUz_suU0+K;M*P7@a+y? zKHlLon7@yRa~iOsx%TvieztVZo#|^AnrVEF=JJ6F?e9}C3}#-F&|AGGp`$mZuSar# zya=IX-g3A|ON?mIF3KYFT!J=fBQGInlkALf1y*5M+9U3ox-S=DeXu~NA*E?_W+?dL zqmK>ixMMgR!E)ehYFMQI9Hmf<(2;Wi-pbnnW2PK=2en@-(_;t)QxxsdkUBlTY50To zdZdqmC0wYvqnjzpqgP`5Xyr_$9WClNIsRFyVDFbG4Oax_gh6cm_w2!YAn%Q7IEaD2Sr*naLjefRLjqW^T)^{8qu{bS|@{~0ELz7WFJ_9MO zCbwt`VE6{Z;0B*~%IY;iy6F?M^@=kG)(XqABcdW|MHvlL8~F=|hfi8gMpK=aG{kwK z^{CE^5v}8*!4fV$crlsXsxL(knxH!xlokX!U@HzeH;3hA{y{ z1k;(oaoiH5*59Ow+}p0zN1! z;5RK9WSU;a%7FmFag*9(Rhr$Ag}tzy-@A1_9ok7`4XG=(gK929etQKCinq4^<5&3uq!lx3bk zSAcJtM4`D%Or*^a98t$=d^w5+V}0}K&*BMSr8JoX6l}T+-7Sxqm}a=kO=-Z3b|&0K zPh=in4Lp|(6Ok_a5|N^p2B2taNSYa=i$zz0P<%Vll(39raafGg7!V<}Vt9s_D7P>F z^Bl*qDvndNT8`6X^TPXLHPH4rT=%T9<22+K?TX|5;&ME&e8)L1vAOM*ttL8$TsdSN>bO}mwc8R z=Wd-x)Zg>h_p!>`DScq4$+cBkRBKwNjyG~Ynnf+A(y%UE$N3^RzY#!(9`12!*i|Z( zt-9l!mgSgZ+0^2dC!g7_o$!_`Pq3+Sq~h?m`eivwD#>A<& zgZ&esR|qFO`g~jS-Wwsbbm@7FmBfku-H3N zkui|HbScVE=@3lYQJ4gFZwD$ua#f2& z(2p+A3tPJ~=D(2TCqNdIY50V1T2(atC2MLhAv%G;dlmkSZa|VgPY-8N9y)?^Av&Q)#Y*r?@}lJulou;3HjR3}}`=h?F&f za(Q}49Tn?WC1214bwMNi-ndz8Rl>O%po1+{l~1S=Da~oW)%_uy#CJ_Q-13S^&S6O@ISrsfKTPdjdMjt6ryjwt;(*6~Q`rL<{GKstJlFK3>{JjHfV}=OXi)wr+OsW$FdB;v z=nI1nH~=|K3$KL~gP8CDXbVSGfHej<5qAS21*Pxg_`0~2(-Jog)Kr|x388?=6UtX~ ze*-$ev(IB zx-2$Koj?6h8AhD3;(YB^03e)&nFAFvoK}KpGzn*jbxH_dn7pNqlLRuzyyudW2Em5c zgzSJ2#FvrXdx<^D!Y4$E)IB+|DPq&C3bDlow`Rh!#;X!!0Ez^nmmEQ)T(clYUX5qJ zLXbsNs(=~SQ;0;W>U{#|_=7Tu9}Sm11{4fgoy-ayqmv`#Rcyt_zmU)p2)AI?4+TEZ z6Nd(}N=$sYj^jNT8JHk-6k@BIh{JgMyllA4&Iin1s2`9ciP`B z^TtU@B50rIc^^c)B85R*H7*u-tIlQ#u7D&EumZ(ie!(#skjIMK6#16;6ADU%1>dHS zyG{`b5*aUFYkZ?3CP#whU~?h_)NL&}K_Fm+;M7w>0i*^GI~4|C$^*XSwJ)pg13bQMj(WP@^f(m&+3RsR4n_DksWn;_RLBz`l#2%r-k1m4S>wpnfQ9j#vp@$68q#T6(8MWB2DdbcFhUe=nINA4AdMEst~I&P zPK#Tv%5q4xMW%t!*zft~uFtYj_%e4T@NS&)Vqe2Sim>;(B>bwL3O;QCXu^Qq`xKLS z#)gVApKhrM>`_=v3ZF62>K(8KgfKRmb_mG@2?_IsPhDM>2wGF5_N|Jov}L&dN+4>$ zkb@R#)TiZ5QMl@BNhskXd%*Vig$%|nqzp?>SGfRgf}zy#RW()4Plu2~Ai@|Oi5P)q zgbz{%xNG<+Bi*BJY>EUxSb6r9+L3D-d0!mn`zq2+7?4Jsrd`~R%zlB965lnl?83X`5yVCK^CA2$atyRlz{IZ;T-zEajcUg@ASf2)Pyz zVs%SkEAP58X_zP+=($R!+MU=U_(Ed8&9j`QCc0JFTPRo^0FZlxW+D32+Tr0q2}A0T zwFbc(O+@fUH4eEZ(9YB{5U=qbXdTHTl^6f5be1*@C-1l*CKre#RN=@-gIkpZU2bY6 z@ey@w*PROBHFPTuQgYe_iv2ArHsql)S3^c5r366$@l}%P0bsKUt4|xP%*HR|8<($^ zivg&K@zMxOO%S3ntf{FBK^PAPNV!K!U|=4>F>B<)Izm$j7_1zl2Q#C5QznXg?ZXgP zoU1QlD*!K)(RnMYTAN3J!TTfxX<7@^-LBPp2i6-G2g7(X#e?G!k%7Z%3l;;BWgqGT zs(nxa8mKxX?o|8^tS7*)U1*1d*>Q&gRpUZ|6BKF$=LXd}TmZepZ6_1qL4yQu(Fwlx zzz$6NV$DKH{3$NXMj<)DtjcZ2*K!jWE=FQ)Emk2s1Zf+PY7sV4lFp-ybgc&3308>3 zu!de3{^!>l<>^2^zGx0gVSs~5?Sb-G3y5*tm#K1tBdvv^r5Dv<+pvStjMo>piya`# z7d1`XaAGz<@~9+GFbviZ5TyHde`B8p3yqn&6t!}qW12l8%fdz7)kQ)8NoZ8$s%q|# z9^s}F@i#S;JLOsscjy5yf?@zuLr^2WCLQ4U)V#`;R>d0qDkWd+a0QZulhVy&!D^sv zZs0_U2JnT1VG0ZVI}x$^Ed|SK3)E(F17#Beg(-6)heit^;D3s(oT zPD3DeKR)`%^;MBu#PC4!2--lG0*M6%O;w7@ka0GoOR0juj&rvv8zY%Z*5|gV!tgP} zDhnrptS7a-k8QS(Ux}rUH5l`81gUM#)zTZ5vEl%A8@lSsFGZtbSH#^1TS3Jwx2LN7 z`iK{yY>7KY?s_fn<&ne+u`7}9mA8J~t0Rl4q83Tlr?3&UsE$zZhG_HIe#&ze=5xLYH4xj-?f0w|dHI(|4)>!fa1X~2U zqB8eVKt}2WU>KW=+M*OjDx-bvB0vL%;zW^TqbYQ%NSqK6m760iz?tx08KHR-qcnv2 z@NU=#cL6SXhjTeFoCrNTvEYS3LArhSLi!MUz*9xB0v~^T$ZTlK-Ibk4ff1{gk3{}7 z7i@C=lN7j(IQSAEMt$)IMxtAgT)=_S3wbL-^h;?=?h6QSAVE?UB*vu@-8vOuAP;2b?k-9WF7M9+PAgF)#r9@H=%8A3;o8TVfppir0>cg2v=N;z`bPaaE0C1pJpn zsz3}aWe5s;S00FaQCg8AjKR2lDBNV42z33ML-DW>H4;;4n&zXhAoRYFMi#%kH>t{S zrJyLWCj=C+EJJcr2)A-$m4%S*)%f@JAv)h-~gkNMr%1Kagt3 zDQRV1w3=^Ku1$&y`@DV|DgBI|7!0@GeXJ8S@$ClT~G=*}Dcf1JP5z?tQ zf~28$5s64A>g0?^e8qQ0Q@VaqMEDQ}$bLDBu)ADR&C~MUZ>~pj zxU1F=oYs=Xcl^YyJR0TGD<(-9W^ZTw!9?_3>O;Pk`A^@7u?h<=rXi6nuo~P+2-BQT?rm*peMF+)+8Z5%QebiA zl9r}?S9&Rr@#-p(aDy*G!rwf3Q%~u?69c9vn=#y$Jy|ulsfA5Y&WGpxg5R#zaeNfd zD0Z+DppCp-!7kqj7U`T=OeMF_U(>*>+WEVpza(SJIbml1iGe>1EfPM^qXoqgV}{HQ z^g@LSgAFmSP}6O(W38#FXOpcGv7{9e)Rciy^QGL;fV99w%v_TsY5*~IcW5e{&zj_* zPRkpbZ|Dn9lQuWZ{>%-pBCEBYE15(0WyiC_Aj8Qc{oSAk1ps4);r5Rev9xsAdiUu94!GqK8^gfaF24rmj|h*B&g8vE$H30P8wDEf5zPTPHvmKtSXH z;ZEa#@U4i#2?AdR1X==P^we{Q6o;;(0gI_s!ARja6QSr>EtDJJdQcYlrKGMXkZYko zSgo(braD!Z`WYS*6t2&tT2o5PC!d6)VsjuRz!d-ht?H)y)q=XNPbe$lNWO#vlYy(L z=m|I`(J^>TY!R+X7X>cL_aW>thecL=S#oMZHtYugKC>!Mvv#}&AXkd<*Cr65W@1D^ zT&XTl8>m4&k4;UWNJeujYpCAn*jot|bRQy9VU>!+Tq5`;79coyU0zKBsJc$dAL~>R zm#=RFdG1Ou${0<>U2+%54n_eMTr&{(gm?;_CB4M<@u!8;amJ_wW5fj}d`Aj~UQLtZ zcc|E+m!7ZIQHXc~D{5wtiOxKfVV2#~rSx8lGN<+wkjiVXKT=u0;6pdESTFbRq4pQQ z`}-e$`@ew3|1#R@f9ap*>wgdJ@!rS(4*{!vKjXirmfb7=zXX5%H(c;L{NF7bb^iP} D7ty!G literal 0 HcmV?d00001 diff --git a/archive/v0.1/pages.json b/archive/v0.1/pages.json new file mode 100644 index 00000000..788db1c5 --- /dev/null +++ b/archive/v0.1/pages.json @@ -0,0 +1,4 @@ +[ +{"title": "Getting Started", "url": "getting-started.html"}, +{"title": "Documentation", "url": "mithril.html"} +] \ No newline at end of file diff --git a/archive/v0.1/style.css b/archive/v0.1/style.css new file mode 100644 index 00000000..81942c15 --- /dev/null +++ b/archive/v0.1/style.css @@ -0,0 +1,86 @@ +.container {margin:auto;max-width:1000px;padding:0 20px;position:relative;} +.container:after,.row:after {content:"";display:table;clear:both;} +.container,.row,[class*='col('] {-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;} +[class*='col('] {float:left;} +[class*='(3,'] {width:25%;} +[class*='(4,'] {width:33.33333%;} +[class*='(6,'] {width:50%;} +[class*='(8,'] {width:66.66667%;} +[class*='(9,'] {width:75%;} +@media (max-width:1000px) { +[class*=',3,'] {width:25%;} +[class*=',4,'] {width:33.33333%;} +[class*=',6,'] {width:50%;} +[class*=',8,'] {width:66.66667%;} +[class*=',9,'] {width:75%;} +} +@media (max-width:750px) { +[class*=',6)'] {width:50%;} +[class*=',12)'] {width:100%;} +} + +html {background:#999;color:#222;font:14px Helvetica;} +html,body {margin:0;padding:0;} +header,footer {background:#999;} +nav {text-align:right;} +nav a:first-child,nav a:first-child:visited {color:#fff;font-size:27px;float:left;line-height:1.3em;padding:0;text-decoration:none;} +nav a {color:#fff;display:inline-block;padding:10px;} +nav a:visited {color:#ddd;} +footer {text-align:center;padding:10px 0;} +footer,footer a,footer a:visited {color:#fff;} +h1,h2 {font-family:Palatino;margin:0 0 10px;} +h1 {font-size:3em;text-shadow:0.01em 0.01em #777, -0.01em -0.01em #fff;} +h1 span {animation:logo 2s;display:inline-block;} +h2 {color:#888;font-style:italic;} +h3 {margin:10px 0;} +p {margin:15px 0;} +ul {margin:15px 0;padding:0 0 0 1em;} +li {margin:0 0 10px;} +a {color:#161;} +a:visited {color:#383;} +a:hover {text-decoration:none;} +pre {background:#ffe;border:1px solid #ddd;overflow:auto;margin:0 0 15px;padding:5px 10px;white-space:pre;-webkit-overflow-scrolling:touch;} +pre[class*="language-"],code {background:#ffe;font:12px/15px Lucida Console,Monaco,monospace;} +hr {border-top:1px solid #ccc;border-width:1px 0 0;margin:20px 0;} +table {margin:0 0 10px;width:100%;} +.cta {padding:30px 0 20px;text-align:center;} +.cta { +background: +linear-gradient(27deg, #e5e5e5 5px, rgba(255,255,255,0) 5px) 0 5px, +linear-gradient(207deg, #e5e5e5 5px, rgba(255,255,255,0) 5px) 10px 0px, +linear-gradient(27deg, #f2f2f2 5px, rgba(255,255,255,0) 5px) 0px 10px, +linear-gradient(207deg, #f2f2f2 5px, rgba(255,255,255,0) 5px) 10px 5px, +linear-gradient(90deg, #ebebeb 10px, rgba(255,255,255,0) 10px), +linear-gradient(#ededed 25%, #eaeaea 25%, #eaeaea 50%, rgba(255,255,255,0) 50%, rgba(255,255,255,0) 75%, #f4f4f4 75%, #f4f4f4); +background-color: #e3e3e3; +background-size: 20px 20px; +} +.logo {color:#d3d3d3;font-family:Georgia;font-style:italic;} +.logo :before {content:"\25CB";position:absolute;margin:-0.17em 0 0 -0.10em;} +.logo :after {content:"\25CB";position:absolute;margin:-0.17em 0 0 -0.5em;} +.button,.button:visited {background:#5a5;border-radius:5px;box-shadow:1px 1px #777, -1px -1px #fff;color:#fff;display:inline-block;font:normal bold 16px Helvetica;margin:0 10px 10px;padding:10px 30px;text-decoration:none;} +.features {background:#fff;padding:30px 0 0;} +.feature {margin:0 0 30px;padding:0 20px 0 0;} +.sample {background:#f5f5f5;padding:30px 0 10px;} +.example {background:#ffe;border:1px solid #ddd;display:block;font:Courier New;margin-bottom:20px;padding:5px 10px;} +.example span {color:#383;font-weight:bold;} +.example small {color:#888;font-size:1em;} +.more {background:#ddd;padding:30px 0;} +.output a,.more a {display:block;margin:0 0 10px;} +.output a:after,.more a:after {content:" \bb";} +.performance {background:#fff;padding:30px 0;} +.performance td:first-child {text-align:right;width:1%;} +.bar {background:red;height:4px;float:left;margin:0.5em 1em 0 0;} +.security {background:#f5f5f5;padding:30px 0;} +.success {color:#383;} +.error {color:#f00;} +.content {background:#f5f5f5;padding:30px 0;} + +@media (min-width:750px) { +.sample pre {margin-right:20px;} +} + +@keyframes logo { + from {opacity:0;transform:scale(2) rotate(359deg);} + to {opacity:1;transform:scale(1) rotate(0deg);} +} \ No newline at end of file diff --git a/archive/v0.1/tools/template-compiler.sjs b/archive/v0.1/tools/template-compiler.sjs new file mode 100644 index 00000000..4d5924c0 --- /dev/null +++ b/archive/v0.1/tools/template-compiler.sjs @@ -0,0 +1,64 @@ +/* +Compiles Mithril templates + +Requires sweet.js (https://github.com/mozilla/sweet.js) +Installation: npm install -g sweet.js +Usage: sjs --module /mithril.compile.sjs --output .js .js +*/ + +macro m { + case { _ ($selector) } => { + return #{m($selector, {}, [])}; + } + case { _ ($selector, $partial) } => { + var partialSyntax = #{$partial}; + var partial = unwrapSyntax(partialSyntax); + return partial.value == "{}" ? #{m($selector, $partial, [])} : #{m($selector, {}, partial)}; + } + case { _ ($selector, $dynAttrs, $children) } => { + var selectorSyntax = #{$selector}; + var selector = unwrapSyntax(selectorSyntax); + + var dynAttrsSyntax = #{$dynAttrs}; + var dynAttrs = unwrapSyntax(dynAttrsSyntax); + + var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g; + var attrParser = /\[(.+?)=("|'|)(.+?)\2\]/; + var _match = null; + var classes = []; + var cell = {tag: "div", attrs: {}, children: []}; + + while (_match = parser.exec(selector)) { + 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]; + } + } + if (classes.length > 0) cell.attrs["class"] = classes.join(" "); + + var tag = makeValue(cell.tag, #{here}); + var attrsBody = Object.keys(cell.attrs).reduce(function(memo, attrName) { + return memo.concat([ + makeValue(attrName, #{here}), + makePunc(":", #{here}), + makeValue(cell.attrs[attrName], #{here}), + makePunc(",", #{here}) + ]); + }, []).concat(dynAttrs.inner); + var attrs = [makeDelim("{}", attrsBody, #{here})]; + var children = cell.children.map(function(child) { + return makeValue(child, #{here}); + }) + letstx $tag = [tag], $attrs = attrs; + + return #{ ({tag: $tag, attrs: $attrs , children: $children}) }; + } + case { _ } => { + return #{Mithril}; + } +} + +export m; \ No newline at end of file diff --git a/archive/v0.1/tools/template-converter.html b/archive/v0.1/tools/template-converter.html new file mode 100644 index 00000000..e3ec7de5 --- /dev/null +++ b/archive/v0.1/tools/template-converter.html @@ -0,0 +1,9 @@ +

If you already have your HTML written and want to convert it into a Mithril template, paste the HTML below and press the "Convert" button.

+ +
+ + + + \ No newline at end of file diff --git a/archive/v0.1/tools/template-converter.js b/archive/v0.1/tools/template-converter.js new file mode 100644 index 00000000..5501df8e --- /dev/null +++ b/archive/v0.1/tools/template-converter.js @@ -0,0 +1,89 @@ +var templateConverter = {}; + +templateConverter.DOMFragment = function(markup) { + if (markup.indexOf(" -1) return [new DOMParser().parseFromString(markup, "text/html").childNodes[1]] + var container = document.createElement("div"); + container.insertAdjacentHTML("beforeend", markup); + return container.childNodes; +} +templateConverter.VirtualFragment = function recurse(domFragment) { + var virtualFragment = []; + for (var i = 0, el; el = domFragment[i]; i++) { + if (el.nodeType == 3) { + virtualFragment.push(el.nodeValue); + } + else { + var attrs = {}; + for (var j = 0, attr; attr = el.attributes[j]; j++) { + attrs[attr.name] = attr.value; + } + + virtualFragment.push({tag: el.tagName.toLowerCase(), attrs: attrs, children: recurse(el.childNodes)}); + } + } + return virtualFragment; +} +templateConverter.Template = function recurse() { + if (Object.prototype.toString.call(arguments[0]) == "[object String]") { + return new recurse(new templateConverter.VirtualFragment(new templateConverter.DOMFragment(arguments[0]))); + } + + var virtualFragment = arguments[0], level = arguments[1] + if (!level) level = 1; + + var tab = "\n" + new Array(level + 1).join("\t"); + var virtuals = []; + for (var i = 0, el; el = virtualFragment[i]; i++) { + if (typeof el == "string") { + if (el.match(/\t| {2,}/g) && el.trim().length == 0) virtuals.indented = true; + else virtuals.push('"' + el.replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n") + '"'); + } + else { + var virtual = ""; + if (el.tag != "div") virtual += el.tag; + if (el.attrs["class"]) { + virtual += "." + el.attrs["class"].replace(/\t+/g, " ").split(" ").join("."); + delete el.attrs["class"]; + } + var attrNames = Object.keys(el.attrs).sort() + for (var j = 0, attrName; attrName = attrNames[j]; j++) { + if (attrName != "style") virtual += "[" + attrName + "='" + el.attrs[attrName].replace(/'/g, "\\'") + "']"; + } + virtual = '"' + virtual + '"'; + + var style = "" + if (el.attrs.style) { + virtual += ", {style: " + ("{\"" + el.attrs.style.replace(/:/g, "\": \"").replace(/;/g, "\", \"") + "}").replace(/, "}|"}/, "}") + "}" + } + + if (el.children.length > 0) { + virtual += ", " + recurse(el.children, level + 1); + } + virtual = "m(" + virtual + ")"; + virtuals.push(virtual); + } + } + if (!virtuals.indented) tab = ""; + + var isInline = virtuals.length == 1 && virtuals[0].charAt(0) == '"'; + var template = isInline ? virtuals.join(", ") : "[" + tab + virtuals.join("," + tab) + tab.slice(0, -1) + "]"; + return new String(template); +} + +templateConverter.controller = function() { + this.source = m.prop(""); + this.output = m.prop(""); + + this.convert = function() { + return this.output(new templateConverter.Template(this.source())); + }; + +}; + +templateConverter.view = function(ctrl) { + return [ + m("textarea", {autofocus: true, style: {width:"100%", height: "40%"}, onchange: m.withAttr("value", ctrl.source)}, ctrl.source()), + m("button", {onclick: ctrl.convert.bind(ctrl)}, "Convert"), + m("textarea", {style: {width:"100%", height: "40%"}}, ctrl.output()) + ]; +}; \ No newline at end of file diff --git a/docs/auto-redrawing.md b/docs/auto-redrawing.md new file mode 100644 index 00000000..fac7299f --- /dev/null +++ b/docs/auto-redrawing.md @@ -0,0 +1,109 @@ +## Integrating with The Auto-Redrawing System + +If you need to do custom asynchronous calls without using Mithril's API, and find that your views are not redrawing, or that you're being forced to call [`m.redraw`](mithril.redraw.md) manually, you should consider using `m.startComputation` / `m.endComputation` so that Mithril can intelligently auto-redraw once your custom code finishes running. + +In order to integrate asynchronous code to Mithril's autoredrawing system, you should call `m.startComputation` BEFORE making an asynchronous call, and `m.endComputation` after the asynchronous callback completes. + +```javascript +//this service waits 1 second, logs "hello" and then notifies the view that +//it may start redrawing (if no other asynchronous operations are pending) +var doStuff = function() { + m.startComputation(); //call `startComputation` before the asynchronous `setTimeout` + + setTimeout(function() { + console.log("hello"); + + m.endComputation(); //call `endComputation` at the end of the callback + }, 1000); +}; +``` + +To integrate synchronous code, call `m.startComputation` at the beginning of the method, and `m.endComputation` at the end. + +```javascript +window.onfocus = function() { + m.startComputation(); //call before everything else in the event handler + + doStuff(); + + m.endComputation(); //call after everything else in the event handler +} +``` + +For each `m.startComputation` call a library makes, it MUST also make one and ONLY one corresponding `m.endComputation` call. + +You should not use these methods if your code is intended to run repeatedly (e.g. by using `setInterval`). If you want to repeatedly redraw the view without necessarily waiting for user input, you should manually call [`m.redraw`](mithril.redraw.md) within the repeatable context. + + +--- + +### Integrating multiple execution threads + +When [integrating with third party libraries](integration.md), you might find that you need to call asynchronous methods from outside of Mithril's API. + +In order to integrate non-trivial asynchronous code to Mithril's auto-redrawing system, you need to ensure all execution threads call `m.startComputation` / `m.endComputation`. + +An execution thread is basically any amount of code that runs before other asynchronous threads start to run. + +Integrating multiple execution threads can be done in a two different ways: in a layered fashion or in comprehensive fashion + +#### Layered integration + +Layered integration is recommended for modular code where many different APIs may be put together at the application level. + +Below is an example where various methods implemented with a third party library can be integrated in layered fashion: any of the methods can be used in isolation or in combination. + +Notice how `doBoth` repeatedly calls `m.startComputation` since that method calls both `doSomething` and `doAnother`. This is perfectly valid: there are three asynchronous computations pending after the `jQuery.when` method is called, and therefore, three pairs of `m.startComputation` / `m.endComputation` in play. + +```javascript +var doSomething = function(callback) { + m.startComputation(); //call `startComputation` before the asynchronous AJAX request + + return jQuery.ajax("/something").done(function() { + if (callback) callback(); + + m.endComputation(); //call `endComputation` at the end of the callback + }); +}; +var doAnother = function(callback) { + m.startComputation(); //call `startComputation` before the asynchronous AJAX request + + return jQuery.ajax("/another").done(function() { + if (callback) callback(); + m.endComputation(); //call `endComputation` at the end of the callback + }); +}; +var doBoth = function(callback) { + m.startComputation(); //call `startComputation` before the asynchronous synchronization method + + jQuery.when(doSomething(), doAnother()).then(function() { + if (callback) callback(); + + m.endComputation(); //call `endComputation` at the end of the callback + }) +}; +``` + +#### Comprehensive integration + +Comprehensive integration is recommended if integrating a monolithic series of asynchronous operations. In contrast to layered integration, it minimizes the number of `m.startComputation` / `m.endComputation` to avoid clutter. + +The example below shows a convoluted series of AJAX requests implemented with a third party library. + +```javascript +var doSomething = function(callback) { + m.startComputation(); //call `startComputation` before everything else + + jQuery.ajax("/something").done(function() { + doStuff(); + jQuery.ajax("/another").done(function() { + doMoreStuff(); + jQuery.ajax("/more").done(function() { + if (callback) callback(); + + m.endComputation(); //call `endComputation` at the end of everything + }); + }); + }); +}; +``` \ No newline at end of file diff --git a/docs/change-log.md b/docs/change-log.md new file mode 100644 index 00000000..33b35777 --- /dev/null +++ b/docs/change-log.md @@ -0,0 +1,3 @@ +## Change Log + +v0.1 - Initial release \ No newline at end of file diff --git a/docs/comparison.md b/docs/comparison.md new file mode 100644 index 00000000..3217b95c --- /dev/null +++ b/docs/comparison.md @@ -0,0 +1,135 @@ +## How is Mithril Different from Other Frameworks + +There are a lot of different Javascript MVC frameworks and evaluating their merits and shortcomings can be a daunting task. + +This page aims to provide a comparison between Mithril and some of the most widely used frameworks, as well as some of the younger, but relevant ones. + +### Code Size + +One of the most obvious differences between Mithril and most frameworks is in file size: Mithril is less than 3kb gzipped and has no dependencies on other libraries. + +Note that while a small gzipped size can look appealing, that number is often used to "hide the weight" of the uncompressed code: remember that the decompressed Javascript still needs to be parsed and evaluated on every page load, and this cost (which can be in the dozens of milliseconds range for some frameworks in some browsers) cannot be cached. + +This cost might be less of a concern in single page apps, but not necessarily if the app is typically opened simultaneously in multiple tabs, or in less powerful devices. + +The performance tests in the homepage show execution times for parsing and evaluation of Mithril's code, compared to some popular frameworks. As you can see, it paints a much less flattering picture for some frameworks than when we look at gzipped size alone. + +### Documentation + +Another point of comparison is documentation. Most of the popular frameworks have at least a bare minimum amount of documentation nowadays, but many leave a bit to be desired: some lack usage examples, and some frameworks' communities need to rely heavily on third party sites for explanations of more advanced topics, and sometimes even for learning the basics. + +This is a problem particularly for frameworks that had breaking changes in the past: It's common to find answers in StackOverflow that are out-of-date and no longer work with the latest version of said frameworks. + +Mithril has more documentation in this site than the amount of code in the framework, and none of the documentation is auto-generated. + +All API points are explained in prose, and have code examples. Because the entire documentation is hand-crafted, you get the benefit of actually having explanations for things that documentation generator tools don't support well (for example, interfaces and callback parameter documentation). + +In addition, this guide section covers topics related to how to fit all the pieces together. + +From the get-go, Mithril's build system produces archived versions of the code and documentation so that you'll never be stuck without docs for out-of-date versions. + +Given how young Mithril is, hopefully you can appreciate the level of commitment for providing good documentation. + +### Architecture + +In terms of architecture, one of Mithril's main differences is that it does not provide base classes to extend from. + +It's often said that *frameworks*, in contrast to *libraries*, dictate how code should be written. In this sense, one could argue that Mithril isn't really a framework. + +Instead of locking developers down to very specific implementations of design patterns, Mithril's approach is to provide an idiomatic pattern to follow, and tools to aid the developer when required. This approach means that developers can get discoverable codebases without necessarily getting locked into the framework. + +One related difference is that other frameworks often hard-coded base classes where every conceivable convenience method are inherited by the developer's classes (remember, in Javascript, this can mean copying all of the utility methods over to the child class, regardless of whether they're going to be used or now). + +Mithril's on-demand tooling approach means there are no hidden performance costs when implementing core MVC patterns, and there's also no extra learning curve for framework-specific syntax for those patterns. + +### View Layer Paradigm + +Some of the older frameworks among the popular ones (out-of-the-box jQuery and Backbone, specifically) take a more procedural paradigm when it comes to the view layer: this means every action requires the developer to write custom view-level code to handle it. + +This can get noticeably bulky when you look at thing like collections: you often need to implement insertion code and deletion code, in addition to a "draw everything" routine for performance. And this is for every list that needs to be displayed in some way. + +Mithril's view layer paradigm is designed be **declarative**, much like HTML, such that the same code implicitly does everything it needs to. As it turns out, this design decision is actually a compromise: it offers the benefit of decreased application code complexity at the cost of some performance loss. However, as the performance tests in the homepage show, this does not necessarily hurt Mithril in a meaningful way. + +--- + +## Specific Framework Comparisons + +Warning: this section is likely biased. Take it with a grain of salt. + +### jQuery + +jQuery is ubiquitous and has a large ecosystem, but it's not an MVC framework. + +There's no idiomatic way to organize jQuery code in an MVC pattern and many frameworks were created specifically to overcome that shortcoming. + +As summarized above, Mithril differs from jQuery by allowing DOM-related code to be written largely in a declarative style (thereby decreasing code complexity), in addition to providing an idiomatic way to structure applications. + +One other difference that is extremely clear is the treatment of data. In jQuery it's common to use the DOM as a data storage mechanism, whereas Mithril encourages data to exist in an isolated model layer. + +### Backbone + +Backbone was originally designed as a way to structure jQuery-based applications. One of its selling points is that it allows developers to leverage their existing jQuery knowledge, while providing some "walls" to organize the code in a more structured manner. + +As with jQuery, Mithril differs from Backbone by enforcing view code to be written in a declarative style. + +Another marking difference is that Backbone is workflow agnostic, that is, there's no one idiomatic way to organize applications. This is good for framework adoption, but not necessarily ideal for team scalability and codebase discoverability. + +In contrast, Mithril encourages that applications be developed using the patterns found throughout this guide. This discourages "bastardized" MVC pattern variations and architecturing style fragmentation. + +One technical aspect that is also different is that Backbone is heavily event-oriented. Mithril, on the other hand, purposely avoids the observer pattern in an attempt to abolish "come-from hell", i.e. a class of debugging problems where you don't know what triggers some code because of a long chain of events triggering other events. + +A particularly nasty instance of this problem that sometimes occurs in "real-time" applications is when event triggering chains become circular due to a conditional statement bug, casuing infinite loops and browser crashes. + +Another significant difference between Backbone and Mithril is in their approach to familiarity: Backbone appeals to people familiar w/ jQuery; Mithril is designed to be familiar to people with server-side MVC framework experience. + +### Angular + +Angular is an MVC framework maintained by Google, and it provides a declarative view layer and an emphasis on testability. It leverages developer experience with server-side MVC frameworks, and in many ways, is very similar in scope to Mithril. + +The main difference between Angular templates and Mithril templates is that Angular templates follow the tradition of being defined in HTML. This has the benefit of cleaner syntax for writing static text, but it comes with the disadvantage of features getting awkwardly tied to HTML syntax, as well as providing poor debugging support. + +One thing you may have noticed in the homepage is that, out of the box, Angular is not as performant as other frameworks. Steep performance degradation is a notoriously common issue in non-trivial Angular applications and there are several third party libraries which attempt to get around performance problems. Speaking from experience, it's generally difficult to reason about performance in Angular. + +Mithril takes some learnings from that and implements a templating redrawing system that renders less aggressively, is less complex and is easier to profile. + +A noteworthy difference between Angular and Mithril is in framework complexity: Angular implements several subsystems that would seem more logical in programming language implementations (e.g. a parser, a dynamic scoping mechanism, decorators, etc). Mithril, on the other hand, tries to provide only features to support a more classic MVC paradigm. + +### Ember + +Ember is a highly comprehensive MVC framework, providing a large API that covers not only traditional MVC patterns, but also a vast range of helper utilities as well. + +The biggest difference between Ember and Mithril is summarized in the Architecture section above: Ember's comprehensiveness come at a cost of a steep learning curve, and a high degree of vendor lock-in. + +Ember is also more opinionated in terms of how application architecture should look, and as a result, tends to be less transparent in terms of what is actually happening under the hood. + +### React + +React is a templating engine developed by Facebook. It's relevant for comparison because it uses the same architecture as Mithril's templating engine: i.e. it acknowledges that DOM operations are the bottleneck of templating systems, and implements a virtual DOM tree which keeps track of changes and only applies diffs to the real DOM where needed. + +The most visible difference between React and Mithril is that React's *JSX* syntax does not run natively in the browser, whereas Mithril's uncompiled templates do. Both can be compiled, but React's compiled code still has function calls for each virtual DOM element; Mithril templates compile into static javascript data structures. + +Another difference is that Mithril, being an MVC framework, rather than a templating engine, provides an auto-redrawing system that is aware of network asynchrony and that can render views efficiently without cluttering application code with redraw calls, and without letting the developer unintentionally bleed out of the MVC pattern. + +Note also that, despite having a bigger scope, Mithril has a smaller file size than React. + +### Knockout + +Knockout is a library focused on data binding. It is not an MVC framework in the traditional sense, but idiomatic Knockout code uses the similar concept of view models. + +A Knockout view model is an amalgamation of model and controller layers in a single class. In contrast, Mithril separates the two layers more distinctly. + +Generally speaking, Knockout applications tend to be more tightly coupled than Mithril since Knockout doesn't provide an equivalent to Mithril's modules and components. + +As with Angular, Knockout templates are written in HTML, and therefore have the same pros and cons as Angular templates. + +### Vue + +Vue is a relatively new and unknown templating engine, but it boasts impressive results in its performance benchmark. + +It is not a full MVC framework, but it is similar to Angular templates, and uses the same terminology for its features (e.g. directives and filters). + +The most relevant difference is that Vue uses browser features that don't work (and cannot be made to work) in Internet Explorer 8. Mithril allows developers to support browsers all the way back to IE6 and Blackberry. + +Vue's implementation cleverly hijacks array methods, but it should be noted that Javascript Arrays cannot be truly subclassed and as such, Vue suffers from abstraction leaks. + +In contrast, Mithril avoids "magic" types. diff --git a/docs/compiling-templates.md b/docs/compiling-templates.md new file mode 100644 index 00000000..faa66fa3 --- /dev/null +++ b/docs/compiling-templates.md @@ -0,0 +1,94 @@ +## Compiling Templates + +If performance is absolutely critical, you can optionally pre-compile templates that use `m()` by running the [`template-compiler.sjs`](tools/template-compiler.sjs) macro with [Sweet.js](https://github.com/mozilla/sweet.js) + +Compiling a template transforms the nested function calls into a raw virtual DOM tree (which is merely a collection of native Javascript objects that is ready to be rendered via [`m.render`](mithril.render.md)) + +For example, given the following template: + +```javascript +var view = function() { + return m("a", {href: "http://google.com"}, "test"); +} +``` + +It would be compiled into: + +```javascript +var view = function() { + return {tag: "a", attrs: {href: "http://google.com"}, children: "test"}; +} +``` + +Note that compiled templates are meant to be generated by an automated build process and are not meant to be human editable. + +--- + +### Installing NodeJS and SweetJS for one-off compilations + +SweetJS requires a [NodeJS](http://nodejs.org) environment. To install it, go to its website and use the installer provided. + +To install SweetJS, NodeJS provides a command-line package manager tool. In a command line, type: + +``` +npm install -g sweet.js +``` + +To compile a file, type: + +``` +sjs --module /mithril.compile.sjs --output .js .js +``` + +--- + +### Automating Compilation + +If you want to automate compilation, you can use [GruntJS](http://gruntjs.com), a task automation tool. If you're not familiar with GruntJS, you can find a tutorial on their website. + +Assuming NodeJS is already installed, run the following command to install GruntJS: + +``` +npm install -g grunt-cli +``` + +Once installed, create two files in the root of your project, `package.json` and `Gruntfile.js` + +`package.json` + +```javascript +{ + "name": "project-name-goes-here", + "version": "0.0.0", //must follow this format + "devDependencies": { + "grunt-sweet.js": "*" + } +} +``` + +`Gruntfile.js` + +```javascript +module.exports = function(grunt) { + grunt.initConfig({ + sweetjs: { + modules: ["mithril.compile.sjs"], + compile: {expand: true, cwd: ".", src: "**/*.js", dest: "destination-folder-goes-here/"} + } + }); + + grunt.loadNpmTasks('grunt-sweet.js'); + + grunt.registerTask('default', ['sweetjs']); +} +``` + +Make sure to replace the `project-name-goes-here` and `destination-folder-goes-here` placeholders with appropriate values. + +To run the automation task, run the following command from the root folder of your project: + +``` +grunt +``` + +More documentation on the grunt-sweet.js task and its options [can be found here](https://github.com/natefaubion/grunt-sweet.js) diff --git a/docs/components.md b/docs/components.md new file mode 100644 index 00000000..4a51fa19 --- /dev/null +++ b/docs/components.md @@ -0,0 +1,153 @@ +## Components + +Components are Mithril's mechanism for [hierarchical MVC](http://en.wikipedia.org/wiki/Hierarchical_model%E2%80%93view%E2%80%93controller). + +They allow complex, repeating logic to be abstracted into a logical unit of code, and they help modularize applications with widgets or multi-concern views (e.g. dashboards). + +You can also use components for a number of other advanced techniques, like recursive templating (e.g. tree views) and partial template mixins (i.e. injecting part of a template into another). + +--- + +### Nesting components + +Here's an example of nested modules in a widgetization scenario: + +```javascript +//root module +var dashboard = {}; + +dashboard.controller = function() { + this.userProfile = new userProfile.controller(); + this.projectList = new projectList.controller(); +} + +dashboard.view = function(ctrl) { + return m("#example", [ + m(".profile", [ + new userProfile.view(this.userProfile); + ]), + m(".projects", [ + new projectList.view(this.projectList); + ]) + ]) +} + + + +//components + +//user profile component +var userProfile = {}; + +userProfile.controller = function() { + this.name = m.prop("John Doe"); +}; + +userProfile.view = function(ctrl) { + return [ + m("h1", "Profile"), + "Name: " + ctrl.name() + ]; +}; + + + +//project list component +var projectList = {}; + +projectList.controller = function() {}; + +projectList.view = function(ctrl) { + return "There are no projects"; +}; + + + +//initialize +m.module(document.body, dashboard); +``` + +As you can see, components look exactly like regular modules - it's turtles all the way down! Remember that modules are simply dumb containers for `controller` and `view` classes. + +This means components are decoupled both *horizontally* and *vertically*. It's possible to refactor each component as a isolated unit of logic (which itself follows the MVC pattern). And we can do so without touching the rest of the application (as long as the component API stays the same). + +Similarly, it's possible to mix and match different classes to make mix-in anonymous components (e.g. it's straightforward to build several views - for, say, a mobile app - that use the same controller). + +It's also possible to keep references to parent and even sibling components. This is useful, for example, when implementing notification badges in a navigation component, which are triggered and managed by other components in the system. + +--- + +### Librarization + +Applications often reuse rich UI controls that aren't provided out of the box by HTML. Below is a basic example of a component of that type: a minimalist autocompleter component. + +*Note: Be mindful that, for the sake of code clarity and brevity, the example below does not support keyboard navigation and other real world features.* + +```javascript +var autocompleter = {}; + +autocompleter.controller = function(data, getter) { + //binding for the text input + this.value = m.prop(""); + //store for the list of items + this.data = m.prop([]); + + //method to determine what property of a list item to compare the text input's value to + this.getter = getter; + + //this method changes the relevance list depending on what's currently in the text input + this.change = function(value) { + this.value(value); + + var data = value === "" ? [] : data.filter(function(item) { + return this.getter(item).toLowerCase().indexOf(value.toLowerCase()) > -1; + }, this); + this.data(data); + }; + + //this method is called when an option is selected. It triggers an `onchange` event + this.select = function(value) { + this.value(value); + this.data([]); + if (this.onchange) this.onchange({target: {value: value}}); + }; +} + +autocompleter.view = function(ctrl, options) { + if (options) ctrl.onchange = options.onchange; + return [ + m("input", {oninput: m.withAttr("value", ctrl.change.bind(ctrl)), value: ctrl.value()}), + ctrl.data().map(function(item) { + return m("div", {data: ctrl.getter(item), onclick: m.withAttr("data", ctrl.select.bind(ctrl))}, ctrl.getter(item)); + }) + ]; +} + + + +//here's an example of using the autocompleter +var dashboard = {} + +dashboard.controller = function() { + this.names = m.prop([{id: 1, name: "John"}, {id: 2, name: "Bob"}, {id: 2, name: "Mary"}]); + this.autocompleter = new autocompleter.controller(this.names(), function(item) { + return item.name; + }); +}; + +dashboard.view = function(ctrl) { + //assuming there's an element w/ id = "example" somewhere on the page + return m("#example", [ + new autocompleter.view(ctrl.autocompleter, {onchange: m.withAttr("value", console.log)}), + ]); +}; + + + +//initialize +m.module(document.body, dashboard); +``` + +It's recommended that libraries that provide extra functionality to Mithril be implemented using this modular pattern, as opposed to trying to hide implementation in a [virtual element's `config` attribute](mithril.md). + +You should only consider using `config`-based components when leveraging existing libraries. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 00000000..69e1f8a9 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,566 @@ +## Getting Started + +### What is Mithril? + +Mithril is a client-side Javascript MVC framework, i.e. it's a tool to make application code divided into a data layer (called "**M**odel"), a UI layer (called **V**iew), and a glue layer (called **C**ontroller) + +Mithril is around 3kb gzipped thanks to its [small, focused, API](mithril.md). It provides a templating engine with a virtual DOM diff implementation for performant rendering, utilities for high-level modelling via functional composition, as well as support for routing and componentization. + +The goal of the framework is to make application code discoverable, readable and maintainable, and hopefully help you become an even better developer. + +Unlike some frameworks, Mithril tries very hard to avoid locking you into a web of dependencies: you can use as *little* of the framework as you need. + +However, using its entire toolset idiomatically can bring lots of benefits: learning to use functional programming in real world scenarios and solidifying good coding practices for OOP and MVC are just some of them. + +--- + +## A Simple Application + +Getting started is surprisingly boilerplate-free: + +```markup + + + +``` + +Yes, this is valid HTML 5! According to the specs, the ``, `` and `` tags can be omitted, but their respective DOM elements will still be there implicitly when a browser renders that markup. + +--- + +### Model + +In Mithril, typically an application lives in an namespace and contains modules. Modules are merely structures that represent a viewable "page" or component. + +For simplicity, our application will have only one module, and we're going to use it as the namespace for our application: + +```markup + +``` + +This object will namespace our two Model classes: + +```javascript +var todo = {}; + +//for simplicity, we use this module to namespace the model classes + +//the Todo class has two properties +todo.Todo = function(data) { + this.description = m.prop(data.description); + this.done = m.prop(false); +}; + +//the TodoList class is a list of Todo's +todo.TodoList = Array; +``` + +[`m.prop`](mithril.prop.md) is simply a factory for a getter-setter function. Getter-setters work like this: + +```javascript +//define a getter-setter with initial value `John` +var name = m.prop("John"); + +//read the value +var a = name(); //a == "John" + +//set the value to `Mary` +name("Mary"); //Mary + +//read the value +var b = name(); //b == "Mary" +``` + +Note that the `Todo` and `TodoList` classes we defined above are plain vanilla Javascript constructors. They can be initialized and used like this: + +```javascript +var myTask = new todo.Todo({description: "Write code"}); + +//read the description +myTask.description(); //Write code + +//is it done? +var isDone = myTask.done(); //isDone == false + +//mark as done +myTask.done(true); //true + +//now it's done +isDone = myTask.done(); //isDone == true +``` + +The `TodoList` class is simply an alias of the native `Array` class. + +```javascript +var list = new todo.TodoList(); +list.length; //0 +``` + +--- + +### Controller + +Our next step is to write a controller that will use our model classes. + +```javascript +//the controller uses 3 model-level entities, of which one is a custom defined class: +//`Todo` is the central class in this application +//`list` is merely a generic array, with standard array methods +//`description` is a temporary storage box that holds a string +// +//the `add` method simply adds a new todo to the list +todo.controller = function() { + this.list = new todo.TodoList(); + this.description = m.prop(""); + + this.add = function(description) { + if (description()) { + this.list.push(new todo.Todo({description: description()})); + this.description(""); + } + }; +} +``` + +The code above should hopefully be self-explanatory. You can use the controller like this: + +```javascript +var ctrl = new todo.controller(); + +ctrl.description(); //[empty string] + +//try adding a to-do +ctrl.add(ctrl.description); +ctrl.list.length; //0 + +//you can't add a to-do with an empty description + +//add it properly +ctrl.description("Write code"); +ctrl.add(ctrl.description); +ctrl.list.length; //1 +``` + +--- + +### View + +The next step is to write a view so users can interact with the application + +```javascript +todo.view = function(ctrl) { + return m("html", [ + m("body", [ + m("input"), + m("button", "Add"), + m("table", [ + m("tr", [ + m("td", [ + m("input[type=checkbox]") + ]), + m("td", "task description"), + ]) + ]) + ]) + ]); +}; +``` + +The utility method `m()` creates virtual DOM elements. As you can see, you can use CSS selectors to specify attributes. You can also use the `.` syntax to add CSS classes and the `#` to add an id. + +The view can be rendered using the `m.render` method: + +```javascript +//assuming the `ctrl` variable from earlier +m.render(document, todo.view(ctrl)); +``` + +Notice that we pass a root DOM element to attach our template to, as well as the template itself. + +This renders the following markup: + +```markup + + + + + + + + + +
task description
+ + +``` + +--- + +#### Data Bindings + +Let's implement a **data binding** on the text input. Data bindings connect a DOM element to a javascript variable so that updating one updates the other. + +```javascript +m("input") + +//becomes +m("input", {value: ctrl.description()}) +``` + +This binds the `description` getter-setter to the text input. Updating the value of the description updates the input when Mithril redraws. + +```javascript +var ctrl = new todo.controller(); +ctrl.description(); // empty string +m.render(todo.view(ctrl)); // input is empty +ctrl.description("Write code"); //set the description in the controller +m.render(todo.view(ctrl)); // input now says "Write code" +``` + +Note that calling the `todo.view` method multiple times does not re-render the entire template. + +Mithril internally keeps a virtual representation of the DOM in cache, scans for changes, and then only modifies the minimum required to apply the change. + +In this case, Mithril only touches the `value` attribute of the input. + +--- + +Bindings can also be **bi-directional**: that is, they can be made such that, in addition to what we saw just now, a user typing on the input updates the description getter-setter. + +Here's the idiomatic way of implementing the view-to-controller part of the binding: + +```javascript +m("input", {onchange: m.withAttr("value", ctrl.description), value: ctrl.description()}) +``` + +The code bound to the `onchange` can be read like this: "with the attribute value, set ctrl.description". + +Note that Mithril does not prescribe how the binding updates: you can bind it to `onchange`, `onkeypress`, `oninput`, `onblur` or any other event that you prefer. + +You can also specify what attribute to bind. This means that just as you are able to bind the `value` attribute in an `` progressive enhancement mode +select2.view = function(ctrl) { + return m("select", {config: select2.config(ctrl)}, [ + ctrl.data.map(function(item) { + return m("option", {value: item.id}, item.name) + }) + ]); +}; + +//end component + + + +//usage +var dashboard = {}; + +dashboard.controller = function() { + //list of users to show + this.data = [{id: 1, name: "John"}, {id: 2, name: "Mary"}, {id: 3, name: "Jane"}]; + + //select Mary + this.currentUser = this.data[1]; + + this.changeUser = function(id) { + console.log(id) + }; +} + +dashboard.view = function(ctrl) { + return m("div", [ + m("label", "User:"), + select2.view({data: ctrl.data, value: ctrl.currentUser.id, onchange: ctrl.changeUser}) + ]); +} + +m.module(document.body, dashboard); +``` + +`select2.config` is a factory that creates a `config` function based on a given controller. We declare this outside of the `select2.view` function to avoid cluttering the template. + +The `config` function created by our factory only runs the initialization code if it hasn't already. This `if` statement is important, because this function may be called multiple times by Mithril's auto-redrawing system and we don't want to re-initialize select2 at every redraw. + +The initialization code defines a `change` event handler. Because this handler is not created using Mithril's templating engine (i.e. we're not defining an attribute in a virtual element), we must manually integrate it to the auto-redrawing system. + +This can be done by simply calling `m.startComputation` at the beginning, and `m.endComputation` at the end of the function. You must add a pair of these calls for each asynchronous execution thread, unless the thread is already integrated. + +For example, if you were to call a web service using `m.request`, you would not need to add more calls to `m.startComputation` / `m.endComputation` (you would still need the first pair in the event handler, though). + +On the other hand, if you were to call a web service using jQuery, then you would be responsible for adding a `m.startComputation` call before the jQuery ajax call, and for adding a `m.endComputation` call at the end of the completion callback, in addition to the calls within the `change` event handler. Refer to the [`auto-redrawing`](auto-redrawing.md) guide for an example. + +One important note about the `config` method is that you should avoid calling `m.redraw`, `m.startComputation` and `m.endComputation` in the `config` function's execution thread. (An execution thread is basically any amount of code that runs before other asynchronous threads start to run) + +While Mithril technically does support this use case, relying on multiple redraw passes degrades performance and makes it possible to code yourself into an infinite execution loop situation, which is extremely difficult to debug. + +The `dashboard` module in the example shows how a developer would consume the select2 component. + +You should always document integration components so that others can find out what attribute parameters can be used to initialize the component. + diff --git a/docs/layout/api.html b/docs/layout/api.html new file mode 100644 index 00000000..92b6d947 --- /dev/null +++ b/docs/layout/api.html @@ -0,0 +1,77 @@ + + + + Mithril + + + + + +
+ +
+
+
+ +
+
+
+
+ Released under the MIT license +
© 2014 Leo Horie +
+
+ + + \ No newline at end of file diff --git a/docs/layout/comparisons/angular.parsing.html b/docs/layout/comparisons/angular.parsing.html new file mode 100644 index 00000000..642d6448 --- /dev/null +++ b/docs/layout/comparisons/angular.parsing.html @@ -0,0 +1,2 @@ + +To run an execution time test on this page, run the profiler from your browser's developer tools and measure the running time of a page refresh. (Lower is better) \ No newline at end of file diff --git a/docs/layout/comparisons/angular.rendering.html b/docs/layout/comparisons/angular.rendering.html new file mode 100644 index 00000000..8cf81d82 --- /dev/null +++ b/docs/layout/comparisons/angular.rendering.html @@ -0,0 +1,14 @@ + + + +

To run an execution time test on this page, run the profiler from your browser's developer tools and measure the running time of a page refresh. (Lower is better)

+
+ +
+ + + diff --git a/docs/layout/comparisons/angular.safety.html b/docs/layout/comparisons/angular.safety.html new file mode 100644 index 00000000..01403424 --- /dev/null +++ b/docs/layout/comparisons/angular.safety.html @@ -0,0 +1,13 @@ + + + +
+ +
+ + + diff --git a/docs/layout/comparisons/backbone.parsing.html b/docs/layout/comparisons/backbone.parsing.html new file mode 100644 index 00000000..e30e1eaf --- /dev/null +++ b/docs/layout/comparisons/backbone.parsing.html @@ -0,0 +1,4 @@ + + + +To run an execution time test on this page, run the profiler from your browser's developer tools and measure the running time of a page refresh. (Lower is better) \ No newline at end of file diff --git a/docs/layout/comparisons/backbone.rendering.html b/docs/layout/comparisons/backbone.rendering.html new file mode 100644 index 00000000..07f38489 --- /dev/null +++ b/docs/layout/comparisons/backbone.rendering.html @@ -0,0 +1,30 @@ + + + + + + + +

To run an execution time test on this page, run the profiler from your browser's developer tools and measure the running time of a page refresh. (Lower is better)

+
+ + + + + diff --git a/docs/layout/comparisons/backbone.safety.html b/docs/layout/comparisons/backbone.safety.html new file mode 100644 index 00000000..e5a71e08 --- /dev/null +++ b/docs/layout/comparisons/backbone.safety.html @@ -0,0 +1,29 @@ + + + + + + + +
+ + + + + diff --git a/docs/layout/comparisons/jquery.parsing.html b/docs/layout/comparisons/jquery.parsing.html new file mode 100644 index 00000000..1fa16592 --- /dev/null +++ b/docs/layout/comparisons/jquery.parsing.html @@ -0,0 +1,2 @@ + +To run an execution time test on this page, run the profiler from your browser's developer tools and measure the running time of a page refresh. (Lower is better) \ No newline at end of file diff --git a/docs/layout/comparisons/jquery.rendering.html b/docs/layout/comparisons/jquery.rendering.html new file mode 100644 index 00000000..dde611ec --- /dev/null +++ b/docs/layout/comparisons/jquery.rendering.html @@ -0,0 +1,17 @@ + + + +

To run an execution time test on this page, run the profiler from your browser's developer tools and measure the running time of a page refresh. (Lower is better)

+
+ + + diff --git a/docs/layout/comparisons/jquery.safety.html b/docs/layout/comparisons/jquery.safety.html new file mode 100644 index 00000000..e9667269 --- /dev/null +++ b/docs/layout/comparisons/jquery.safety.html @@ -0,0 +1,16 @@ + + + +
+ + + diff --git a/docs/layout/comparisons/mithril.parsing.html b/docs/layout/comparisons/mithril.parsing.html new file mode 100644 index 00000000..c2957a15 --- /dev/null +++ b/docs/layout/comparisons/mithril.parsing.html @@ -0,0 +1,2 @@ + +To run an execution time test on this page, run the profiler from your browser's developer tools and measure the running time of a page refresh. (Lower is better) \ No newline at end of file diff --git a/docs/layout/comparisons/mithril.rendering.html b/docs/layout/comparisons/mithril.rendering.html new file mode 100644 index 00000000..717f5d5e --- /dev/null +++ b/docs/layout/comparisons/mithril.rendering.html @@ -0,0 +1,20 @@ + + + +

To run an execution time test on this page, run the profiler from your browser's developer tools and measure the running time of a page refresh. (Lower is better)

+
+ + + diff --git a/docs/layout/comparisons/mithril.safety.html b/docs/layout/comparisons/mithril.safety.html new file mode 100644 index 00000000..1e6ba153 --- /dev/null +++ b/docs/layout/comparisons/mithril.safety.html @@ -0,0 +1,19 @@ + + + +
+ + + diff --git a/docs/layout/guide.html b/docs/layout/guide.html new file mode 100644 index 00000000..e33c48fd --- /dev/null +++ b/docs/layout/guide.html @@ -0,0 +1,59 @@ + + + + Mithril + + + + + +
+ +
+
+
+ +
+
+
+
+ Released under the MIT license +
© 2014 Leo Horie +
+
+ + + \ No newline at end of file diff --git a/docs/layout/index.html b/docs/layout/index.html new file mode 100644 index 00000000..22c49c0f --- /dev/null +++ b/docs/layout/index.html @@ -0,0 +1,196 @@ + + + + Mithril + + + + + +
+ +
+ +
+
+
+

Mithril

+ +

A Javascript Framework for Building Brilliant Applications

+ + Guide + Download v$version +
+
+ +
+
+
+

Light-weight

+
    +
  • Only 3kb gzipped, no dependencies
  • +
  • Small API, small learning curve
  • +
+
+ +
+

Robust

+
    +
  • Safe-by-default templates
  • +
  • Hierarchical MVC via components
  • +
+
+ +
+

Fast

+
    +
  • Virtual DOM diffing and compilable templates
  • +
  • Intelligent auto-redrawing system
  • +
+
+
+
+ +
+
+
+

Sample code

+ +
//namespace
+var app = {};
+
+//model
+app.PageList = function() {
+	return m.request({method: "GET", url: "pages.json"});
+};
+
+//controller
+app.controller = function() {
+	this.pages = app.PageList();
+};
+
+//view
+app.view = function(ctrl) {
+	return ctrl.pages().map(function(page) {
+		return m("a", {href: page.url}, page.title);
+	});
+};
+
+//initialize
+m.module(document.getElementById("example"), app);
+ +
+
+

Output

+ + + + +
+
+ +
+ +
+
+

Performance

+

To run the execution time tests below, click on their respective links, run the profiler from your desired browser's developer tools and measure the running time of a page refresh. (Lower is better)

+
+
+

Loading

+ + + + + +
Mithril 0.28ms
jQuery 13.11ms
Backbone 18.54ms
Angular 7.49ms
+
+
+

Rendering

+ + + + + +
Mithril 9.44ms (uncompiled)
jQuery 40.27ms
Backbone 23.05ms
Angular 118.63ms
+
+
+
+
+ +
+
+
+
+

Safety

+

Mithril templates are safe by default, i.e. you can't unintentionally create security holes.

+

To run the tests for each framework, click on the respective links. If you see an alert box, ensuring security with that framework is more work for you.

+
+
+

Test Summary

+ Mithril (pass) ✓
+ jQuery (fail) ✗
+ Backbone (fail) ✗
+ Angular (pass) ✓
+
+
+
+
+ +
+
+
+

Guide

+

Build a simple app, learn the ropes

+

Read Guide

+
+ +
+

API

+

Docs and code samples for your reference

+

Read Docs

+
+
+
+
+
+
+ Released under the MIT license
+ © 2014 Leo Horie +
+
+ + + \ No newline at end of file diff --git a/docs/layout/lib/prism/prism.css b/docs/layout/lib/prism/prism.css new file mode 100644 index 00000000..1e61e11d --- /dev/null +++ b/docs/layout/lib/prism/prism.css @@ -0,0 +1,126 @@ +/** + * prism.js default theme for JavaScript, CSS and HTML + * Based on dabblet (http://dabblet.com) + * @author Lea Verou + */ + +code[class*="language-"], +pre[class*="language-"] { + color: black; + text-shadow: 0 1px white; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { + text-shadow: none; + background: #b3d4fc; +} + +pre[class*="language-"]::selection, pre[class*="language-"] ::selection, +code[class*="language-"]::selection, code[class*="language-"] ::selection { + text-shadow: none; + background: #b3d4fc; +} + +@media print { + code[class*="language-"], + pre[class*="language-"] { + text-shadow: none; + } +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +:not(pre) > code[class*="language-"], +pre[class*="language-"] { + background: #f5f2f0; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: .1em; + border-radius: .3em; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: slategray; +} + +.token.punctuation { + color: #999; +} + +.namespace { + opacity: .7; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol { + color: #905; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.builtin { + color: #690; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string, +.token.variable { + color: #a67f59; + background: hsla(0,0%,100%,.5); +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: #07a; +} + + +.token.regex, +.token.important { + color: #e90; +} + +.token.important { + font-weight: bold; +} + +.token.entity { + cursor: help; +} + diff --git a/docs/layout/lib/prism/prism.js b/docs/layout/lib/prism/prism.js new file mode 100644 index 00000000..b7f84870 --- /dev/null +++ b/docs/layout/lib/prism/prism.js @@ -0,0 +1,9 @@ +/** + * Prism: Lightweight, robust, elegant syntax highlighting + * MIT license http://www.opensource.org/licenses/mit-license.php/ + * @author Lea Verou http://lea.verou.me + */(function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=self.Prism={util:{type:function(e){return Object.prototype.toString.call(e).match(/\[object (\w+)\]/)[1]},clone:function(e){var n=t.util.type(e);switch(n){case"Object":var r={};for(var i in e)e.hasOwnProperty(i)&&(r[i]=t.util.clone(e[i]));return r;case"Array":return e.slice()}return e}},languages:{extend:function(e,n){var r=t.util.clone(t.languages[e]);for(var i in n)r[i]=n[i];return r},insertBefore:function(e,n,r,i){i=i||t.languages;var s=i[e],o={};for(var u in s)if(s.hasOwnProperty(u)){if(u==n)for(var a in r)r.hasOwnProperty(a)&&(o[a]=r[a]);o[u]=s[u]}return i[e]=o},DFS:function(e,n){for(var r in e){n.call(e,r,e[r]);t.util.type(e)==="Object"&&t.languages.DFS(e[r],n)}}},highlightAll:function(e,n){var r=document.querySelectorAll('code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code');for(var i=0,s;s=r[i++];)t.highlightElement(s,e===!0,n)},highlightElement:function(r,i,s){var o,u,a=r;while(a&&!e.test(a.className))a=a.parentNode;if(a){o=(a.className.match(e)||[,""])[1];u=t.languages[o]}if(!u)return;r.className=r.className.replace(e,"").replace(/\s+/g," ")+" language-"+o;a=r.parentNode;/pre/i.test(a.nodeName)&&(a.className=a.className.replace(e,"").replace(/\s+/g," ")+" language-"+o);var f=r.textContent;if(!f)return;f=f.replace(/&/g,"&").replace(/e.length)break e;if(p instanceof i)continue;a.lastIndex=0;var d=a.exec(p);if(d){l&&(c=d[1].length);var v=d.index-1+c,d=d[0].slice(c),m=d.length,g=v+m,y=p.slice(0,v+1),b=p.slice(g+1),w=[h,1];y&&w.push(y);var E=new i(u,f?t.tokenize(d,f):d);w.push(E);b&&w.push(b);Array.prototype.splice.apply(s,w)}}}return s},hooks:{all:{},add:function(e,n){var r=t.hooks.all;r[e]=r[e]||[];r[e].push(n)},run:function(e,n){var r=t.hooks.all[e];if(!r||!r.length)return;for(var i=0,s;s=r[i++];)s(n)}}},n=t.Token=function(e,t){this.type=e;this.content=t};n.stringify=function(e,r,i){if(typeof e=="string")return e;if(Object.prototype.toString.call(e)=="[object Array]")return e.map(function(t){return n.stringify(t,r,e)}).join("");var s={type:e.type,content:n.stringify(e.content,r,i),tag:"span",classes:["token",e.type],attributes:{},language:r,parent:i};s.type=="comment"&&(s.attributes.spellcheck="true");t.hooks.run("wrap",s);var o="";for(var u in s.attributes)o+=u+'="'+(s.attributes[u]||"")+'"';return"<"+s.tag+' class="'+s.classes.join(" ")+'" '+o+">"+s.content+""};if(!self.document){self.addEventListener("message",function(e){var n=JSON.parse(e.data),r=n.language,i=n.code;self.postMessage(JSON.stringify(t.tokenize(i,t.languages[r])));self.close()},!1);return}var r=document.getElementsByTagName("script");r=r[r.length-1];if(r){t.filename=r.src;document.addEventListener&&!r.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)}})();; +Prism.languages.markup={comment:/<!--[\w\W]*?-->/g,prolog:/<\?.+?\?>/,doctype:/<!DOCTYPE.+?>/,cdata:/<!\[CDATA\[[\w\W]*?]]>/i,tag:{pattern:/<\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|\w+))?\s*)*\/?>/gi,inside:{tag:{pattern:/^<\/?[\w:-]+/i,inside:{punctuation:/^<\/?/,namespace:/^[\w-]+?:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/gi,inside:{punctuation:/=|>|"/g}},punctuation:/\/?>/g,"attr-name":{pattern:/[\w:-]+/g,inside:{namespace:/^[\w-]+?:/}}}},entity:/&#?[\da-z]{1,8};/gi};Prism.hooks.add("wrap",function(e){e.type==="entity"&&(e.attributes.title=e.content.replace(/&/,"&"))});; +Prism.languages.clike={comment:{pattern:/(^|[^\\])(\/\*[\w\W]*?\*\/|(^|[^:])\/\/.*?(\r?\n|$))/g,lookbehind:!0},string:/("|')(\\?.)*?\1/g,"class-name":{pattern:/((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/ig,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/g,"boolean":/\b(true|false)\b/g,"function":{pattern:/[a-z0-9_]+\(/ig,inside:{punctuation:/\(/}}, number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/g,operator:/[-+]{1,2}|!|<=?|>=?|={1,3}|(&){1,2}|\|?\||\?|\*|\/|\~|\^|\%/g,ignore:/&(lt|gt|amp);/gi,punctuation:/[{}[\];(),.:]/g}; +; +Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(var|let|if|else|while|do|for|return|in|instanceof|function|get|set|new|with|typeof|try|throw|catch|finally|null|break|continue)\b/g,number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?|NaN|-?Infinity)\b/g});Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/g,lookbehind:!0}});Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/(<|<)script[\w\W]*?(>|>)[\w\W]*?(<|<)\/script(>|>)/ig,inside:{tag:{pattern:/(<|<)script[\w\W]*?(>|>)|(<|<)\/script(>|>)/ig,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.javascript}}});; diff --git a/docs/layout/pages.json b/docs/layout/pages.json new file mode 100644 index 00000000..788db1c5 --- /dev/null +++ b/docs/layout/pages.json @@ -0,0 +1,4 @@ +[ +{"title": "Getting Started", "url": "getting-started.html"}, +{"title": "Documentation", "url": "mithril.html"} +] \ No newline at end of file diff --git a/docs/layout/style.css b/docs/layout/style.css new file mode 100644 index 00000000..81942c15 --- /dev/null +++ b/docs/layout/style.css @@ -0,0 +1,86 @@ +.container {margin:auto;max-width:1000px;padding:0 20px;position:relative;} +.container:after,.row:after {content:"";display:table;clear:both;} +.container,.row,[class*='col('] {-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;} +[class*='col('] {float:left;} +[class*='(3,'] {width:25%;} +[class*='(4,'] {width:33.33333%;} +[class*='(6,'] {width:50%;} +[class*='(8,'] {width:66.66667%;} +[class*='(9,'] {width:75%;} +@media (max-width:1000px) { +[class*=',3,'] {width:25%;} +[class*=',4,'] {width:33.33333%;} +[class*=',6,'] {width:50%;} +[class*=',8,'] {width:66.66667%;} +[class*=',9,'] {width:75%;} +} +@media (max-width:750px) { +[class*=',6)'] {width:50%;} +[class*=',12)'] {width:100%;} +} + +html {background:#999;color:#222;font:14px Helvetica;} +html,body {margin:0;padding:0;} +header,footer {background:#999;} +nav {text-align:right;} +nav a:first-child,nav a:first-child:visited {color:#fff;font-size:27px;float:left;line-height:1.3em;padding:0;text-decoration:none;} +nav a {color:#fff;display:inline-block;padding:10px;} +nav a:visited {color:#ddd;} +footer {text-align:center;padding:10px 0;} +footer,footer a,footer a:visited {color:#fff;} +h1,h2 {font-family:Palatino;margin:0 0 10px;} +h1 {font-size:3em;text-shadow:0.01em 0.01em #777, -0.01em -0.01em #fff;} +h1 span {animation:logo 2s;display:inline-block;} +h2 {color:#888;font-style:italic;} +h3 {margin:10px 0;} +p {margin:15px 0;} +ul {margin:15px 0;padding:0 0 0 1em;} +li {margin:0 0 10px;} +a {color:#161;} +a:visited {color:#383;} +a:hover {text-decoration:none;} +pre {background:#ffe;border:1px solid #ddd;overflow:auto;margin:0 0 15px;padding:5px 10px;white-space:pre;-webkit-overflow-scrolling:touch;} +pre[class*="language-"],code {background:#ffe;font:12px/15px Lucida Console,Monaco,monospace;} +hr {border-top:1px solid #ccc;border-width:1px 0 0;margin:20px 0;} +table {margin:0 0 10px;width:100%;} +.cta {padding:30px 0 20px;text-align:center;} +.cta { +background: +linear-gradient(27deg, #e5e5e5 5px, rgba(255,255,255,0) 5px) 0 5px, +linear-gradient(207deg, #e5e5e5 5px, rgba(255,255,255,0) 5px) 10px 0px, +linear-gradient(27deg, #f2f2f2 5px, rgba(255,255,255,0) 5px) 0px 10px, +linear-gradient(207deg, #f2f2f2 5px, rgba(255,255,255,0) 5px) 10px 5px, +linear-gradient(90deg, #ebebeb 10px, rgba(255,255,255,0) 10px), +linear-gradient(#ededed 25%, #eaeaea 25%, #eaeaea 50%, rgba(255,255,255,0) 50%, rgba(255,255,255,0) 75%, #f4f4f4 75%, #f4f4f4); +background-color: #e3e3e3; +background-size: 20px 20px; +} +.logo {color:#d3d3d3;font-family:Georgia;font-style:italic;} +.logo :before {content:"\25CB";position:absolute;margin:-0.17em 0 0 -0.10em;} +.logo :after {content:"\25CB";position:absolute;margin:-0.17em 0 0 -0.5em;} +.button,.button:visited {background:#5a5;border-radius:5px;box-shadow:1px 1px #777, -1px -1px #fff;color:#fff;display:inline-block;font:normal bold 16px Helvetica;margin:0 10px 10px;padding:10px 30px;text-decoration:none;} +.features {background:#fff;padding:30px 0 0;} +.feature {margin:0 0 30px;padding:0 20px 0 0;} +.sample {background:#f5f5f5;padding:30px 0 10px;} +.example {background:#ffe;border:1px solid #ddd;display:block;font:Courier New;margin-bottom:20px;padding:5px 10px;} +.example span {color:#383;font-weight:bold;} +.example small {color:#888;font-size:1em;} +.more {background:#ddd;padding:30px 0;} +.output a,.more a {display:block;margin:0 0 10px;} +.output a:after,.more a:after {content:" \bb";} +.performance {background:#fff;padding:30px 0;} +.performance td:first-child {text-align:right;width:1%;} +.bar {background:red;height:4px;float:left;margin:0.5em 1em 0 0;} +.security {background:#f5f5f5;padding:30px 0;} +.success {color:#383;} +.error {color:#f00;} +.content {background:#f5f5f5;padding:30px 0;} + +@media (min-width:750px) { +.sample pre {margin-right:20px;} +} + +@keyframes logo { + from {opacity:0;transform:scale(2) rotate(359deg);} + to {opacity:1;transform:scale(1) rotate(0deg);} +} \ No newline at end of file diff --git a/docs/layout/tools/template-compiler.sjs b/docs/layout/tools/template-compiler.sjs new file mode 100644 index 00000000..4d5924c0 --- /dev/null +++ b/docs/layout/tools/template-compiler.sjs @@ -0,0 +1,64 @@ +/* +Compiles Mithril templates + +Requires sweet.js (https://github.com/mozilla/sweet.js) +Installation: npm install -g sweet.js +Usage: sjs --module /mithril.compile.sjs --output .js .js +*/ + +macro m { + case { _ ($selector) } => { + return #{m($selector, {}, [])}; + } + case { _ ($selector, $partial) } => { + var partialSyntax = #{$partial}; + var partial = unwrapSyntax(partialSyntax); + return partial.value == "{}" ? #{m($selector, $partial, [])} : #{m($selector, {}, partial)}; + } + case { _ ($selector, $dynAttrs, $children) } => { + var selectorSyntax = #{$selector}; + var selector = unwrapSyntax(selectorSyntax); + + var dynAttrsSyntax = #{$dynAttrs}; + var dynAttrs = unwrapSyntax(dynAttrsSyntax); + + var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g; + var attrParser = /\[(.+?)=("|'|)(.+?)\2\]/; + var _match = null; + var classes = []; + var cell = {tag: "div", attrs: {}, children: []}; + + while (_match = parser.exec(selector)) { + 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]; + } + } + if (classes.length > 0) cell.attrs["class"] = classes.join(" "); + + var tag = makeValue(cell.tag, #{here}); + var attrsBody = Object.keys(cell.attrs).reduce(function(memo, attrName) { + return memo.concat([ + makeValue(attrName, #{here}), + makePunc(":", #{here}), + makeValue(cell.attrs[attrName], #{here}), + makePunc(",", #{here}) + ]); + }, []).concat(dynAttrs.inner); + var attrs = [makeDelim("{}", attrsBody, #{here})]; + var children = cell.children.map(function(child) { + return makeValue(child, #{here}); + }) + letstx $tag = [tag], $attrs = attrs; + + return #{ ({tag: $tag, attrs: $attrs , children: $children}) }; + } + case { _ } => { + return #{Mithril}; + } +} + +export m; \ No newline at end of file diff --git a/docs/layout/tools/template-converter.html b/docs/layout/tools/template-converter.html new file mode 100644 index 00000000..e3ec7de5 --- /dev/null +++ b/docs/layout/tools/template-converter.html @@ -0,0 +1,9 @@ +

If you already have your HTML written and want to convert it into a Mithril template, paste the HTML below and press the "Convert" button.

+ +
+ + + + \ No newline at end of file diff --git a/docs/layout/tools/template-converter.js b/docs/layout/tools/template-converter.js new file mode 100644 index 00000000..5501df8e --- /dev/null +++ b/docs/layout/tools/template-converter.js @@ -0,0 +1,89 @@ +var templateConverter = {}; + +templateConverter.DOMFragment = function(markup) { + if (markup.indexOf(" -1) return [new DOMParser().parseFromString(markup, "text/html").childNodes[1]] + var container = document.createElement("div"); + container.insertAdjacentHTML("beforeend", markup); + return container.childNodes; +} +templateConverter.VirtualFragment = function recurse(domFragment) { + var virtualFragment = []; + for (var i = 0, el; el = domFragment[i]; i++) { + if (el.nodeType == 3) { + virtualFragment.push(el.nodeValue); + } + else { + var attrs = {}; + for (var j = 0, attr; attr = el.attributes[j]; j++) { + attrs[attr.name] = attr.value; + } + + virtualFragment.push({tag: el.tagName.toLowerCase(), attrs: attrs, children: recurse(el.childNodes)}); + } + } + return virtualFragment; +} +templateConverter.Template = function recurse() { + if (Object.prototype.toString.call(arguments[0]) == "[object String]") { + return new recurse(new templateConverter.VirtualFragment(new templateConverter.DOMFragment(arguments[0]))); + } + + var virtualFragment = arguments[0], level = arguments[1] + if (!level) level = 1; + + var tab = "\n" + new Array(level + 1).join("\t"); + var virtuals = []; + for (var i = 0, el; el = virtualFragment[i]; i++) { + if (typeof el == "string") { + if (el.match(/\t| {2,}/g) && el.trim().length == 0) virtuals.indented = true; + else virtuals.push('"' + el.replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n") + '"'); + } + else { + var virtual = ""; + if (el.tag != "div") virtual += el.tag; + if (el.attrs["class"]) { + virtual += "." + el.attrs["class"].replace(/\t+/g, " ").split(" ").join("."); + delete el.attrs["class"]; + } + var attrNames = Object.keys(el.attrs).sort() + for (var j = 0, attrName; attrName = attrNames[j]; j++) { + if (attrName != "style") virtual += "[" + attrName + "='" + el.attrs[attrName].replace(/'/g, "\\'") + "']"; + } + virtual = '"' + virtual + '"'; + + var style = "" + if (el.attrs.style) { + virtual += ", {style: " + ("{\"" + el.attrs.style.replace(/:/g, "\": \"").replace(/;/g, "\", \"") + "}").replace(/, "}|"}/, "}") + "}" + } + + if (el.children.length > 0) { + virtual += ", " + recurse(el.children, level + 1); + } + virtual = "m(" + virtual + ")"; + virtuals.push(virtual); + } + } + if (!virtuals.indented) tab = ""; + + var isInline = virtuals.length == 1 && virtuals[0].charAt(0) == '"'; + var template = isInline ? virtuals.join(", ") : "[" + tab + virtuals.join("," + tab) + tab.slice(0, -1) + "]"; + return new String(template); +} + +templateConverter.controller = function() { + this.source = m.prop(""); + this.output = m.prop(""); + + this.convert = function() { + return this.output(new templateConverter.Template(this.source())); + }; + +}; + +templateConverter.view = function(ctrl) { + return [ + m("textarea", {autofocus: true, style: {width:"100%", height: "40%"}, onchange: m.withAttr("value", ctrl.source)}, ctrl.source()), + m("button", {onclick: ctrl.convert.bind(ctrl)}, "Convert"), + m("textarea", {style: {width:"100%", height: "40%"}}, ctrl.output()) + ]; +}; \ No newline at end of file diff --git a/docs/mithril.computation.md b/docs/mithril.computation.md new file mode 100644 index 00000000..36a4edb9 --- /dev/null +++ b/docs/mithril.computation.md @@ -0,0 +1,122 @@ +## m.startComputation / m.endComputation + +If you need to do custom asynchronous calls without using Mithril's API, and find that your views are not redrawing, or that you're being forced to call [`m.redraw`](mithril.redraw.md) manually, you should consider using `m.startComputation` / `m.endComputation` so that Mithril can intelligently auto-redraw once your custom code finishes running. + +In order to integrate an asynchronous code to Mithril's autoredrawing system, you should call `m.startComputation` BEFORE making an asynchronous call, and `m.endComputation` after the asynchronous callback completes. + +```javascript +//this service waits 1 second, logs "hello" and then notifies the view that +//it may start redrawing (if no other asynchronous operations are pending) +var doStuff = function() { + m.startComputation(); //call `startComputation` before the asynchronous `setTimeout` + + setTimeout(function() { + console.log("hello"); + + m.endComputation(); //call `endComputation` at the end of the callback + }, 1000); +}; +``` + +To integrate synchronous code, call `m.startComputation` at the beginning of the method, and `m.endComputation` at the end. + +```javascript +window.onfocus = function() { + m.startComputation(); //call before everything else in the event handler + + doStuff(); + + m.endComputation(); //call after everything else in the event handler +} +``` + +For each `m.startComputation` call a library makes, it MUST also make one and ONLY one corresponding `m.endComputation` call. + +You should not use these methods if your code is intended to run repeatedly (e.g. by using `setInterval`). If you want to repeatedly redraw the view without necessarily waiting for user input, you should manually call [`m.redraw`](mithril.redraw.md) within the repeatable context. + +--- + +### Integrating multiple execution threads + +When [integrating with third party libraries](integration.md), you might find that you need to call asynchronous methods from outside of Mithril's API. + +In order to integrate non-trivial asynchronous code to Mithril's auto-redrawing system, you need to ensure all execution threads call `m.startComputation` / `m.endComputation`. + +An execution thread is basically any amount of code that runs before other asynchronous threads start to run. + +Integrating multiple execution threads can be done in a two different ways: in a layered fashion or in comprehensive fashion + +#### Layered integration + +Layered integration is recommended for modular code where many different APIs may be put together at the application level. + +Below is an example where various methods implemented with a third party library can be integrated in layered fashion: any of the methods can be used in isolation or in combination. + +Notice how `doBoth` repeatedly calls `m.startComputation` since that method calls both `doSomething` and `doAnother`. This is perfectly valid: there are three asynchronous computations pending after the `jQuery.when` method is called, and therefore, three pairs of `m.startComputation` / `m.endComputation` in play. + +```javascript +var doSomething = function(callback) { + m.startComputation(); //call `startComputation` before the asynchronous AJAX request + + return jQuery.ajax("/something").done(function() { + if (callback) callback(); + + m.endComputation(); //call `endComputation` at the end of the callback + }); +}; +var doAnother = function(callback) { + m.startComputation(); //call `startComputation` before the asynchronous AJAX request + + return jQuery.ajax("/another").done(function() { + if (callback) callback(); + m.endComputation(); //call `endComputation` at the end of the callback + }); +}; +var doBoth = function(callback) { + m.startComputation(); //call `startComputation` before the asynchronous synchronization method + + jQuery.when(doSomething(), doAnother()).then(function() { + if (callback) callback(); + + m.endComputation(); //call `endComputation` at the end of the callback + }) +}; +``` + +#### Comprehensive integration + +Comprehensive integration is recommended if integrating a monolithic series of asynchronous operations. In contrast to layered integration, it minimizes the number of `m.startComputation` / `m.endComputation` to avoid clutter. + +The example below shows a convoluted series of AJAX requests implemented with a third party library. + +```javascript +var doSomething = function(callback) { + m.startComputation(); //call `startComputation` before everything else + + jQuery.ajax("/something").done(function() { + doStuff(); + jQuery.ajax("/another").done(function() { + doMoreStuff(); + jQuery.ajax("/more").done(function() { + if (callback) callback(); + + m.endComputation(); //call `endComputation` at the end of everything + }); + }); + }); +}; +``` + +--- + +### Signature + +[How to read signatures](how-to-read-signatures.md) + +```clike +void startComputation() +``` + +```clike +void endComputation() +``` \ No newline at end of file diff --git a/docs/mithril.deferred.md b/docs/mithril.deferred.md new file mode 100644 index 00000000..daa19b53 --- /dev/null +++ b/docs/mithril.deferred.md @@ -0,0 +1,85 @@ +## m.deferred + +This is a low-level method in Mithril. It's a modified version of the Thenable API. + +A deferred is an asynchrony monad. It exposes a `promise` property which can *bind* callbacks to build a computation tree. + +The deferred object can then *apply* a value by calling either `resolve` or `reject`, which then dispatches the value to be processed to the computation tree. + +Each computation function takes a value as a parameter and is expected to return another value, which in turns is forwarded along to the next computation function (or functions) in the tree. + +--- + +### Usage + +```javascript +//standalone usage +var greetAsync = function() { + var deferred = m.deferred(); + setTimeout(function() { + deferred.resolve("hello"); + }, 1000); + return deferred.promise; +}; + +greetAsync() + .then(function(value) {return value + " world"}) + .then(function(value) {console.log(value)}); //logs "hello world" after 1 second +``` + +--- + +### Signature + +[How to read signatures](how-to-read-signatures.md) + +```clike +Deferred deferred() + +where: + Deferred :: Object { Promise promise, void resolve(any value), void reject(any value) } + Promise :: GetterSetter { Promise then(any successCallback(any value), any errorCallback(any value)) } + GetterSetter :: any getterSetter([any value]) +``` + +- **GetterSetter { Promise then([any successCallback(any value) [, any errorCallback(any value)]]) } promise** + + A promise has a method called `then` which takes two computation callbacks as parameters. + + The `then` method returns another promise whose computations (if any) receive their inputs from the parent promise's computation. + + A promise is also a getter-setter (see [`m.prop`](mithril.prop)). After a call to either `resolve` or `reject`, it holds the result of the parent's computation (or the `resolve`/`reject` value, if the promise has no parent promises) + + - **Promise then([any successCallback(any value) [, any errorCallback(any value)]])** + + This method accepts two callbacks which process a value passed to the `resolve` and `reject` methods, respectively, and pass the processed value to the returned promise + + - **any successCallback(any value)** (optional) + + The `successCallback` is called if `resolve` is called in the root `deferred`. + + The default value (if this parameter is falsy) is the identity function `function(value) {return value}` + + If this function returns undefined, then it passes the `value` argument to the next step in the thennable queue, if any + + - **any errorCallback(any value)** (optional) + + The `errorCallback` is called if `reject` is called in the root `deferred`. + + The default value (if this parameter is falsy) is the identity function `function(value) {return value}` + + If this function returns undefined, then it passes the `value` argument to the next step in the thennable queue, if any + + - **returns Promise promise** + +- **void resolve(any value)** + + This method passes a value to the `successCallback` of the deferred object's child promise + +- **void reject(any value)** + + This method passes a value to the `errorCallback` of the deferred object's child promise + + + + \ No newline at end of file diff --git a/docs/mithril.md b/docs/mithril.md new file mode 100644 index 00000000..285c8215 --- /dev/null +++ b/docs/mithril.md @@ -0,0 +1,323 @@ +## m + +This is a convenience method to compose virtual elements that can be rendered via [`m.render()`](mithril.render). + +You are encouraged to use CSS selectors to define virtual elements. See "Signature" section for details. + +--- + +### Usage + +You can use simple tag selectors to make templates resemble HTML: + +```javascript +m("br"); //yields a virtual element that represents
+ +m("div", "Hello"); //yields
Hello
+ +m("div", {class: "container"}, "Hello"); //yields
Hello
+``` + +Note that the output value from `m()` is not an actual DOM element. In order to turn the virtual element into a real DOM element, you must call [`m.render()`](mithril.render). + +```javascript +m.render(document.body, m("br")); //puts a
in +``` + +You can also use more complex CSS selectors: + +```javascript +m(".container"); //yields
+ +m("#layout"); //yields
+ +m("a[name=top]"); //yields + +m("[contenteditable]"); //yields
+ +m("a#google.external[href='http://google.com']", "Google"); //yields Google +``` + +Each `m()` call creates a virtual DOM element, that is, a javascript object that represents a DOM element, and which is eventually converted into one. + +You can, of course, nest virtual elements: + +```javascript +m("ul", [ + m("li", "item 1"), + m("li", "item 2"), +]); + +/* +yields +
    +
  • item 1
  • +
  • item 2
  • +
+*/ +``` + +Be aware that when nesting virtual elements, the child elements must be in an Array. + +--- + +The CSS selector syntax (e.g. `a#google.external[href='http://google.com']`) is meant to be used for declaring static attributes in the element, i.e. attribute values that don't change dynamically when the user interacts with the app. + +The `attributes` argument (i.e. the second parameter in the `m("div", {class: "container"}, "Hello")` example) is meant to be used for attributes whose values we want to dynamically populate. + +For example, let's say that you're generating a link from an entry that comes from a web service: + +```javascript +//assume the variable `link` came from a web service +var link = {url: "http://google.com", title: "Google"} + +m("a", {href: link.url}, link.title); //yields Google +``` + +Here's a less trivial example: + +```javascript +var links = [ + {title: "item 1", url: "/item1"}, + {title: "item 2", url: "/item2"} + {title: "item 3", url: "/item3"} +]; + +m.render(document.body, [ + m("ul.nav", [ + m("li", links.map(function(link) { + return m("a", {href: link.url}, link.title) + }) + ]) +]); +``` + +yields: + +```markup + + + +``` + +As you can see, flow control is done with vanilla Javascript. This allows the developer to abstract away any aspect of the template at will. + +--- + +Note that you can use both javascript property names and HTML attribute names to set values in the `attributes` argument, but you should pass a value of appropriate type. If an attribute has the same name in Javascript and in HTML, then Mithril assumes you're setting the Javascript property. + +```javascript +m("div", {class: "widget"}); //yields
+ +m("div", {className: "widget"}); //yields
+ +m("input", {readonly: true}); //yields + +m("button", {onclick: alert}); //yields , which alerts its event argument when clicked +``` + +--- + +Note that you can use JSON syntax if the attribute name you are setting has non-alphanumeric characters: + +```javascript +m("div", {"data-index": 1}); //yields
+``` + +You can set inline styles like this: + +```javascript +m("div", {style: {border: "1px solid red"}}); //yields
+``` + +Note that in order to keep the framework lean, Mithril does not auto-append units like `px` or `%` to any values. Typically, you should not even be using inline styles to begin with (unless you are dynamically changing them). + +--- + +You can define a non-HTML-standard attribute called `config`. This special parameter allows you to call methods on the DOM element after it gets created. + +This is useful, for example, if you declare a `canvas` element and want to use the Javascript API to draw: + +```javascript +function draw(element, isInitialized) { + //don't redraw if we did once already + if (isInitialized) return; + + var ctx = element.getContext("2d"); + /* draws stuff */ +} + +var view = [ + m("canvas", {config: draw}) +] + +//this creates the canvas element, and therefore, `isInitialized` is false +m.render(document.body, view); + +//here, isInitialized is `true` +m.render(document.body, view); +``` + +One common way of using `config` is in conjunction with [`m.route`](mithril.route), which is an unobtrusive extension to links that allow Mithril's routing system to work transparently regardless of which routing mode is used. + +```javascript +//this link can use any of Mithril's routing system modes +//(i.e. it can use either the hash, the querystring or the pathname as the router implementation) +//without needing to hard-code any syntax (`#` or `?`) in the `href` attribute. +m("a[href='/dashboard']", {config: m.route}, "Dashboard"); +``` + +The `config` mechanism can also be used to put focus on form inputs, and call methods that would not be possible to execute via the regular attribute syntax. + +It is only meant to be used to call methods on DOM elements that cannot be called otherwise. + +It is NOT a "free out-of-jail card". You should not use this method to modify element properties that could be modified via the `attributes` argument, nor values outside of the DOM element in question. + +Also note that the `config` callback only runs after a rendering lifecycle is done. Therefore, you should not use `config` to modify controller and model values, if you expect these changes to render immediately. Changes to controller and model values in this fashion will only render on the next `m.render` or `m.module` call. + +You can use this mechanism to attach custom event listeners to controller methods (for example, when integrating with third party libraries), but you are responsible for making sure the integration with Mithril's autoredrawing system is in place. See the [integration guide](integration.md) for more information. + +--- + +### Signature + +[How to read signatures](how-to-read-signatures.md) + +```clike +VirtualElement m(String selector [, Attributes attributes] [, Children children]) + +where: + VirtualElement :: Object { String tag, Attributes attributes, Children children } + Attributes :: Object + Children :: String text | Array +``` + +- **String selector** + + This string should be a CSS rule that represents a DOM element. + + Only tag, id, class and attribute selectors are supported. + + If the tag selector is omitted, it defaults to `div`. + + Note that if the same attribute is defined in the both `selector` and `attributes` parameters, the value in `attributes` is used. + + For developer convenience, Mithril makes an exception for the `class` attribute: if there are classes defined in both parameters, they are concatenated as a space separated list. It does not, however, de-dupe classes if the same class is declared twice. + + *Examples:* + + `"div"` + + `"#container"` + + `".active"` + + `"[title='Application']"` + + `"div#container.active[title='Application']"` + + `".active#container"` + +- **Attributes attributes** (optional) + + This key-value map should define a list of HTML attributes and their respective values. + + You can use both HTML and Javascript attribute names. For example, both `class` and `className` are valid. + + Values' types should match the expected type for the respective attribute. + + For example, the value for `className` should be a string. + + When a attribute name expects different types for the value in HTML and Javascript, the Javascript type should be used. + + For example, the value for the `onclick` attribute should be a function. + + Similar, setting the value of attribute `readonly` to `false` is equivalent to removing the attribute in HTML. + + It's also possible to set values to Javascript-only properties, such as `hash` in a `` element. + + Note that if the same attribute is defined in the both `selector` and `attributes` parameters, the value in `attributes` is used. + + For developer convenience, Mithril makes an exception for the `class` attribute: if there are classes defined in both parameters, they are concatenated as a space separated list. It does not, however, de-dupe classes if the same class is declared twice. + + *Examples:* + + `{ title: "Application" }` + + `{ onclick: function(e) { /*do stuff*/ } }` + + `{ style: {border: "1px solid red"} }` + +- #### The `config` attribute + + **void config(DOMElement element, Boolean isNew)** (optional) + + You can define a non-HTML-standard attribute called `config`. This special parameter allows you to call methods on the DOM element after it gets created. + + This is useful, for example, if you declare a `canvas` element and want to use the Javascript API to draw: + + ```javascript + function draw(element, isInitialized) { + //don't redraw if we did once already + if (isInitialized) return; + + var ctx = element.getContext("2d"); + /* draws stuff */ + } + + var view = [ + m("canvas", {config: draw}) + ] + + //this creates the canvas element, and therefore, `isInitialized` is false + m.render(document.body, view); + + //here, isInitialized is `true` + m.render(document.body, view); + ``` + + One common way of using `config` is in conjunction with [`m.route`](mithril.route), which is an unobtrusive extension to links that allow Mithril's routing system to work transparently regardless of which routing mode is used. + + ```javascript + //this link can use any of Mithril's routing system modes + //(i.e. it can use either the hash, the querystring or the pathname as the router implementation) + //without needing to hard-code any syntax (`#` or `?`) in the `href` attribute. + m("a[href='/dashboard']", {config: m.route}, "Dashboard"); + ``` + + The `config` mechanism can also be used to put focus on form inputs, and call methods that would not be possible to execute via the regular attribute syntax. + + It is only meant to be used to call methods on DOM elements that cannot be called otherwise. + + It is NOT a "free out-of-jail card". You should not use this method to modify element properties that could be modified via the `attributes` argument, nor values outside of the DOM element in question. + + Also note that the `config` callback only runs after a rendering lifecycle is done. Therefore, you should not use `config` to modify controller and model values, if you expect these changes to render immediately. Changes to controller and model values in this fashion will only render on the next `m.render` or `m.module` call. + + You can use this mechanism to attach custom event listeners to controller methods (for example, when integrating with third party libraries), but you are responsible for making sure the integration with Mithril's autoredrawing system is in place. See the [integration guide](integration.md) for more information. + + - **DOMElement element** + + The DOM element that corresponds to virtual element defined by the `m()` call. + + - **Boolean isInitialized** + + Whether this is the first time we are running this function on this element. This flag is false the first time it runs on an element, and true on redraws that happen after the element has been created. + +- **Children children** (optional) + + If this argument is a string, it will be rendered as a text node. To render a string as HTML, see [`m.trust`](mithril.trust) + + If it's a VirtualElement, it will be rendered as a DOM Element. + + If it's a list, its contents will recursively be rendered as appropriate and appended as children of the element being created. + +- **returns** VirtualElement + + The returned VirtualElement is a javascript data structure that represents the DOM element to be rendered by [`m.render`](mithril.render) + diff --git a/docs/mithril.module.md b/docs/mithril.module.md new file mode 100644 index 00000000..4f686e5e --- /dev/null +++ b/docs/mithril.module.md @@ -0,0 +1,138 @@ +## m.module + +A module is an Object with two keys: `controller` and `view`. Each of those should point to a Javascript class constructor function. + +'m.module' activates a module by instantiating its controller, then instantiating its view and rendering it into a root DOM element. + +Conceptually, the easiest way to think of a module is as a logical namespace with which to organize applications. For example, an app might have a dashboard module, a userEditForm module, an autocompleter module, a date formatting module, etc + +In the context of single page applications (SPA), a module can often be thought of as the code for a single "page", i.e. a visual state that is bookmarkable. Module can, however, also represent *parts* of pages. + +Note that a module might have external dependencies and that the dependencies aren't considered part of the module. + +In more complex applications, modules can be nested in a [hierarchical MVC](http://en.wikipedia.org/wiki/Hierarchical_model%E2%80%93view%E2%80%93controller) pattern. Nested reusable modules that have views are called **Components**. + +Modules and namespaces are often used interchangeably, but namespaces that do not implement the module interface (that is, objects that do not have a property called `controller` and a property called `view`) cannot be activated with `m.module`. For example, a namespace for date formatting utilities could be labeled a "module" (in the generic sense of the word) but it would not contain a view class, and therefore attempting to initialize it via `m.module` would result in undefined behavior. + +--- + +### Usage + +You can make anonymous modules out of existing classes + +```javascript +//controller class +var dashboardController = function() { + this.greeting = "Hello"; +}; + +//view class +var dashboardView = function() { + return m("h1", ctrl.greeting); +}; + +//initialize an anonymous module +m.module(document.body, {controller: dashboardController, view: dashboardView}); +``` + +Typically, however, modules and namespaces are used interchangeably. + +```javascript +//`dashboard` is both a namespace and a module +var dashboard = {} + +//controller class +dashboard.controller = function() { + this.greeting = "Hello"; +}; + +//view class +dashboard.view = function() { + return m("h1", ctrl.greeting); +}; + +//initialize it +m.module(document.body, dashboard); +``` + +The example below shows a component module called `user` being included in a parent module `dashboard`. + +```javascript +//this is a sample module +var dashboard = { + controller: function() { + this.greeting = "Hello"; + + this.user = new user.controller(); + }, + view: function(controller) { + return [ + m("h1", controller.greeting), + + new user.view(controller.user) + ]; + } +}; + +//this module is being included as a component +var user = { + //model + User: function(name) { + this.name = name; + }, + //controller + controller: function() { + this.user = new user.User("John Doe"); + }, + //view + view: function(controller) { + return m("div", controller.user.name); + } +}; + +//activate the dashboard module +m.module(document.body, dashboard); +``` + +yields: + +```markup + +

Hello

+
John Doe
+ +``` + +--- + +### Signature + +[How to read signatures](how-to-read-signatures.md) + +```clike +void module(DOMElement rootElement, Module module) + +where: + Module :: Object { void controller(), void view(Object controllerInstance) } +``` + +- **DOMElement rootElement** + + A DOM element which will contain the view's template. + +- **Module module** + + A module is supposed to be an Object with two keys: `controller` and `view`. Each of those should point to a Javascript class constructor function + + The controller class is instantiated immediately upon calling `m.module`. + + Once the controller code finishes executing (and this may include waiting for AJAX requests to complete), the view class is instantiated, and the instance of the controller is passed as an argument to the view's constructor. + + Note that controllers can manually instantiate child controllers (since they are simply Javascript constructors), and likewise, views can instantiate child views and manually pass the child controller instances down the the child view constructors. + + This "turtles all the way down" approach is the heart of Mithril's component system. + + Components are nothing more than decoupled classes that can be dynamically brought together as required. This permits the swapping of implementations at a routing level (for example, if implementing widgetized versions of existing components) and class dependency hierarchies can be structurally organized to provide uniform interfaces (for unit tests, for example). + + + diff --git a/docs/mithril.prop.md b/docs/mithril.prop.md new file mode 100644 index 00000000..ec8b58b8 --- /dev/null +++ b/docs/mithril.prop.md @@ -0,0 +1,87 @@ +## m.prop + +This is a getter-setter factory utility. It returns a function that stores information + +--- + +### Usage + +```javascript +//define a getter-setter with initial value `John` +var name = m.prop("John"); + +//read the value +var a = name(); //a == "John" + +//set the value to `Mary` +name("Mary"); //Mary + +//read the value +var b = name(); //b == "Mary" +``` + +It can be used in conjunction with [`m.withAttr`](mithril.withattr) to implement data binding in the view-to-model direction and to provide uniform data access for model entity properties. + +```javascript +//a contrived example of bi-directional data binding +var user = { + model: function(name) { + this.name = m.prop(name); + }, + controller: function() { + this.user = new user.model("John Doe"); + }, + view: function(controller) { + m.render("body", [ + m("input", {onchange: m.withAttr("value", controller.user.name), value: controller.user.name()}) + ]); + } +}; +``` + +In the example above, the usage of `m.prop` allows the developer to change the implementation of the user name getter/setter without the need for code changes in the controller and view. + +`m.prop` can also be used in conjunction with [`m.request`](mithril.request) and [`m.deferred`](mithril.deferred) to bind data on completion of an asynchronous operation. + +```javascript +var users = m.prop([]); +var error = m.prop(""); + +m.request({method: "GET", url: "/users"}) + .then(users, error); //on success, `users` will be populated, otherwise `error` will be populated +//assuming the response contains the following data: `[{name: "John"}, {name: "Mary"}]` +//then when resolved (e.g. in a view), the `users` getter-setter will contain a list of User instances +//i.e. users()[0].name() == "John" +``` + +--- + +### Signature + +[How to read signatures](how-to-read-signatures.md) + +```clike +GetterSetter prop([any initialValue]) + +where: + GetterSetter :: any getterSetter([any value]) +``` + +- **any initialValue** (optional) + + An initialization value. If not provided, the value of the getter-setter's internal store defaults to `undefined`. + +- **returns any getterSetter([any value])** + + A getter-setter method. + + - **any value** (optional) + + If provided, it updates the getter-setter's internal store to the provided value. + + If not provided, return the current internally stored value. + + - **returns any value** + + This method always returns the value of the internal store, regardless of whether it was updated or not. + \ No newline at end of file diff --git a/docs/mithril.redraw.md b/docs/mithril.redraw.md new file mode 100644 index 00000000..7f775845 --- /dev/null +++ b/docs/mithril.redraw.md @@ -0,0 +1,27 @@ +## m.redraw + +Redraws the view for the currently active module. Use [`m.module()`](mithril.module) to activate a module. + +This method is called internally by Mithril's auto-redrawing system and is only documented for completeness; you should avoid calling it manually unless you explicitly want a multi-pass redraw cycle. + +A multi-pass redraw cycle is usually only useful if you need non-trivial UI metrics measurements. A multi-pass cycle may span multiple browser repaints and therefore could cause flash of unbehaviored content (FOUC) and performance degradation. + +By default, if you're using either [`m.route`](mithril.route.md) or [`m.module`](mithril.module.md), `m.redraw()` is called automatically by Mithril's auto-redrawing system once the controller finishes executing. + +`m.redraw` is also called automatically on event handlers defined in virtual elements. + +If there are pending [`m.request`](mithril.request.md) calls in either a controller constructor or event handler, the auto-redrawing system waits for all the AJAX requests to complete before calling `m.redraw`. + +This method may also be called manually from within a controller if more granular updates to the view are needed, however doing so is generally not recommended, as it may degrade performance. Model classes should never call this method. + +If you are developing an asynchronous model-level service and finding that Mithril is not redrawing the view after your code runs, you should use [`m.startComputation` and `m.endComputation`](mithril.computation.md) to integrate with Mithril's auto-redrawing system instead. + +--- + +### Signature + +[How to read signatures](how-to-read-signatures.md) + +```clike +void redraw() +``` \ No newline at end of file diff --git a/docs/mithril.render.md b/docs/mithril.render.md new file mode 100644 index 00000000..49d2cd2f --- /dev/null +++ b/docs/mithril.render.md @@ -0,0 +1,67 @@ +## m.render + +This method generates a DOM tree inside of a given HTML element. + +If the method is run more than once with the same root element, it diffs the new tree against the existing one and intelligently modifies only the portions that have changed. + +Note that, unlike many templating engines, this "smart diff" feature does not affect things like cursor placement in inputs and focus, and is therefore safe to call during user interactions. + +--- + +### Usage + +Assuming a document has an empty `` element, the code below: + +```javascript +var links = [ + {title: "item 1", url: "/item1"} +]; + +m.render(document.body, [ + m("ul.nav", [ + m("li", links.map(function(link) { + return m("a", {href: link.url, config: m.route}, link.title) + }) + ]) +]); +``` + +yields: + +```markup + +
+ +``` + +--- + +### Signature + +[How to read signatures](how-to-read-signatures.md) + +```clike +void render(DOMElement rootElement, Children children) + +where: + Children :: String text | Array + VirtualElement :: Object { String tag, Attributes attributes, Children children } + Attributes :: Object +``` + +- **DOMElement rootElement** + + A DOM element which will contain the template represented by `children`. + +- **Children children** + + If this argument is a string, it will be rendered as a text node. To render a string as HTML, see [`m.trust`](mithril.trust) + + If it's a VirtualElement, it will be rendered as a DOM Element. + + If it's a list, its contents will recursively be rendered as appropriate and appended as children of the `root` element. + diff --git a/docs/mithril.request.md b/docs/mithril.request.md new file mode 100644 index 00000000..7cdfee61 --- /dev/null +++ b/docs/mithril.request.md @@ -0,0 +1,371 @@ +## m.request + +This is a high-level utility for working with web services, which allows writing asynchronous code relatively procedurally. + +By default, it assumes server responses are in JSON format and optionally instantiates a class with the response data. + +It provides a number of useful features out of the box: + +- The ability to get an early reference to a container that will hold the asynchronous response +- The ability to queue operations to be performed after the asynchronous request completes +- The ability to "cast" the response to a class of your choice +- The ability to unwrap data in a response that includes metadata properties + +--- + +### Basic usage + +The basic usage pattern for `m.request` returns an [`m.prop`](mithril.prop.md) getter-setter, which is populated when the AJAX request completes. + +The returned getter-setter can be thought of as a box: you can pass this reference around cheaply, and you can "unwrap" its value when needed. + +```javascript +var users = m.request({method: "GET", url: "/user"}); + +//assuming the response contains the following data: `[{name: "John"}, {name: "Mary"}]` +//then when resolved (e.g. in a view), the `users` getter-setter will contain a list of users +//i.e. users() //[{name: "John"}, {name: "Mary"}] +``` + +Note that this getter-setter holds an *undefined* value until the AJAX request completes. Attempting to unwrap its value early will likely result in errors. + +The returned getter-setter also implements the [promise](mithril.deferred.md) interface (also known as a *thennable*): this is the mechanism you should always use to queue operations to be performed on the data from the web service. + +The simplest use case of this feature is to implement functional value assignment via `m.prop` (i.e. the same thing as above). You can bind a pre-existing getter-setter by passing it in as a parameter to a `.then` method: + +```javascript +var users = m.prop([]); //default value + +m.request({method: "GET", url: "/user"}).then(users) +//assuming the response contains the following data: `[{name: "John"}, {name: "Mary"}]` +//then when resolved (e.g. in a view), the `users` getter-setter will contain a list of users +//i.e. users() //[{name: "John"}, {name: "Mary"}] +``` + +This syntax allows you to bind intermediate results before piping them down for further processing, for example: + +```javascript +var users = m.prop([]); //default value +var doSomething = function() { /*...*/ } + +m.request({method: "GET", url: "/user"}).then(users).then(doSomething) +``` + +While both basic assignment syntax and thennable syntax can be used to the same effect, typically it's recommended that you use the assignment syntax whenever possible, as it's easier to read. + +The thennable mechanism is intended to be used in 3 ways: + +- in the model layer: to process web service data in transformative ways (e.g. filtering a list based on a parameter that the web service doesn't support) +- in the controller layer: to bind redirection code upon a condition +- in the controller layer: to bind error messages + +#### Processing web service data + +This step is meant to be done in the model layer. Doing it in the controller level is also possible, but philosophically not recommended, because by tying logic to a controller, the code becomes harder to reuse due to unrelated controller dependencies. + +In the example below, the `listEven` method returns a getter-setter that resolves to a list of users containing only users whose id is even. + +```javascript +//model +var User = {} + +User.listEven = function() { + return m.request({method: "GET", url: "/user"}).then(function(list) { + return list.filter(function(user) {return user.id % 2 == 0}); + }); +} + +//controller +var controller = function() { + this.users = User.listEven() +} +``` + +#### Bind redirection code + +This step is meant to be done in the controller layer. Doing it in the model level is also possible, but philosophically not recommended, because by tying redirection to the model, the code becomes harder to reuse due to overly tight coupling. + +In the example below, we use the previously defined `listEven` model method and queue a controller-level function that redirects to another page if the user list is empty. + +```javascript +//controller +var controller = function() { + this.users = User.listEven().then(function(users) { + if (users.length == 0) m.route("/add"); + }) +} +``` + +#### Binding errors + +Mithril thennables take two functions as optional parameters: the first parameter is called if the web service request completes successfully. The second one is called if it completes with an error. + +Error binding is meant to be done in the controller layer. Doing it in the model level is also possible, but generally leads to more code in order to connect all the dots. + +In the example below, we bind an error getter-setter to our previous controller so that the `error` variable gets populated if the server throws an error. + +```javascript +//controller +var controller = function() { + this.error = m.prop("") + + this.users = User.listEven().then(function(users) { + if (users.length == 0) m.route("/add"); + }, this.error) +} +``` + +If the controller doesn't already have a success callback to run after a request resolves, you can still bind errors like this: + +```javascript +//controller +var controller = function() { + this.error = m.prop("") + + this.users = User.listEven().then(null, this.error) +} +``` + +--- + +### Queuing Operations + +As you saw, you can chain operations that act on the response data. Typically this is required in three situations: + +- in model-level methods if client-side processing is needed to make the data useful for a controller or view. +- in the controller, to redirect after a model service resolves. +- in the controller, to bind error messages + +In the example below, we take advantage of queuing to debug the ajax response data prior to doing further processing on the user list + +```javascript +var users = m.request({method: "GET", url: "/user"}) + .then(console.log); + .then(function(users) { + //add one more user to the response + return users.concat({name: "Jane"}) + }) + +//assuming the response contains the following data: `[{name: "John"}, {name: "Mary"}]` +//then when resolved (e.g. in a view), the `users` getter-setter will contain a list of users +//i.e. users() //[{name: "John"}, {name: "Mary"}, {name: "Jane"}] +``` + +--- + +### Casting the Response Data to a Class + +It's possible to auto-cast a JSON response to a class. This is useful when we want to control access to certain properties in an object, as opposed to exposing all the fields in POJOs (plain old javascript objects) for arbitrary processing. + +In the example below, `User.list` returns a list of `User` instances. + +```javascript +var User = function(data) { + this.name = m.prop(data.name); +} + +User.list = function() { + return m.request({method: "GET", url: "/user", type: User}); +} + +var users = User.list(); +//assuming the response contains the following data: `[{name: "John"}, {name: "Mary"}]` +//then when resolved (e.g. in a view), `users` will contain a list of User instances +//i.e. users()[0].name() == "John" +``` + +--- + +### Unwrapping Response Data + +Often, web services return the relevant data wrapped in objects that contain metadata. + +Mithril allows you to unwrap the relevant data, by providing two callback hooks: `unwrapSuccess` and `unwrapError`. + +These hooks allow you to unwrap different parts of the response data depending on whether it succeed or failed. + +```javascript +var users = m.request({ + method: "GET", + url: "/user", + unwrapSuccess: function(response) { + return response.data; + }, + unwrapError: function(response) { + return response.error; + } +}); + +//assuming the response is: `{data: [{name: "John"}, {name: "Mary"}], count: 2}` +//then when resolved (e.g. in a view), the `users` getter-setter will contain a list of users +//i.e. users() //[{name: "John"}, {name: "Mary"}] +``` + +--- + +### Using Different Data Transfer Formats + +By default, `m.request` uses JSON to send and receive data to web services. You can override this by providing `serialize` and `deserialize` options: + +```javascript +var users = m.request({ + method: "GET", + url: "/user", + serialize: mySerializer, + deserialize: myDeserializer +}); +``` + +One typical way to override this is to receive as-is responses. The example below shows how to receive a plain string from a txt file. + +```javascript +var file = m.request({ + method: "GET", + url: "myfile.txt", + deserialize: function(value) {return value;} +}); +``` + +--- + +### Signature + +[How to read signatures](how-to-read-signatures.md) + +```clike +Promise request(XHROptions options) + +where: + Promise :: GetterSetter { Promise then(any successCallback(any value), any errorCallback(any value)) } + GetterSetter :: any getterSetter([any value]) + XHROptions :: Object { + String method, + String url, + [String user,] + [String password,] + [Object data,] + [Response unwrapSuccess(Response data),] + [Response unwrapError(Response data),] + [String serialize(any dataToSerialize),] + [any deserialize(String dataToDeserialize),] + [void type(Object data),] + [void config(XMLHttpRequest xhr, XHROptions options)] + } + Response :: Object | Array +``` + +- **XHROptions options** + + A map of options for the XMLHttpRequest + + - **String method** + + The HTTP method. Must be either `"GET"`, `"POST"`, `"PUT"`, `"DELETE"`, `"HEAD"` or `"OPTIONS"` + + - **String url** + + The URL to request. If the URL is not in the same domain as the application, the target server must be configured to accept cross-domain requests from the application's domain, i.e. its responses must include the header `Access-Control-Allow-Origin: *`. + + - **String user** (optional) + + A user for HTTP authentication. Defaults to `undefined` + + - **String password** (optional) + + A password for HTTP authentication. Defaults to `undefined` + + - **String password** (optional) + + A password for HTTP authentication. Defaults to `undefined` + + - **Object data** (optional) + + Data to be sent. It's automatically placed in the appropriate section of the request with the appropriate serialization based on `method` + + - **Response unwrapSuccess(Response data)** (optional) + + A preprocessor function to extract the data from a success response in case the response contains metadata wrapping the data. + + The default value (if this parameter is falsy) is the identity function `function(value) {return value}` + + For example, if the response is `{data: [{name: "John"}, {name: "Mary"}]}` and the unwrap function is `function(response) {return response.data}`, then the response will be considered to be `[{name: "John"}, {name: "Mary"}]` when processing the `type` parameter + + - **Object | Array data** + + The data to unwrap + + - **returns Object | Array unwrappedData** + + The unwrapped data + + - **String unwrapError(Response data)** (optional) + + A preprocessor function to extract the data from an error response in case the response contains metadata wrapping the data. + + The default value (if this parameter is falsy) is the identity function `function(value) {return value}` + + - **Object | Array data** + + The data to unwrap + + - **returns Object | Array unwrappedData** + + The unwrapped data + + - **String serialize(any dataToSerialize)** (optional) + + Method to use to serialize the request data + + The default value (if this parameter is falsy) is `JSON.stringify` + + - **any dataToSerialize** + + Data to be serialized + + - **returns String serializedData** + + - **any deserialize(String dataToDeserialize)** (optional) + + Method to use to deserialize the response data + + The default value (if this parameter is falsy) is `JSON.parse` + + - **String dataToDeserialize** + + Data to be deserialized + + - **returns any deserializedData** + + - **void type(Object data)** (optional) + + The response object (or the child items if this object is an Array) will be passed as a parameter to the class constructor defined by `type` + + If this parameter is falsy, the deserialized data will not be wrapped. + + For example, if `type` is the following class: + + ```javascript + var User = function(data) { + this.name = m.prop(data.name); + } + ``` + + And the data is `[{name: "John"}, {name: "Mary"}]`, then the response will contain an array of two User instances. + + - **void config(XMLHttpRequest xhr, XHROptions options)** (optional) + + An initialization function that runs after `open` and before `send`. Useful for adding request headers and when using XHR2 features, such as the XMLHttpRequest's `upload` property. + + - **XMLHttpRequest xhr** + + The XMLHttpRequest instance. + + - **XHROptions options** + + The `options` parameter that was passed into `m.request` call + +- **returns Promise promise** + + returns a promise that can bind callbacks which get called on completion of the AJAX request. + + diff --git a/docs/mithril.route.md b/docs/mithril.route.md new file mode 100644 index 00000000..4721c18d --- /dev/null +++ b/docs/mithril.route.md @@ -0,0 +1,225 @@ +## m.route + +Routing is a system that allows creating Single-Page-Applications (SPA), i.e. applications that can go from a page to another without causing a full browser refresh. + +It enables seamless navigability while preserving the ability to bookmark each page individually, and the ability to navigate the application via the browser's history mechanism. + +This method overloads 3 different units of functionality: + +- `m.route(rootElement, defaultRoute, routes)` - defines the available URLs in an application, and their respective modules + +- `m.route(path)` - redirects to another route + +- `m.route(element)` - an extension to link elements that unobtrusively abstracts away the routing mode + +Routing is single-page-application (SPA) friendly, and can be implemented using either `location.hash`, HTML5 URL rewriting or `location.querystring`. See [`m.route.mode`](#mode) for the caveats of each implementation. + +--- + + + +### Defining routes + +#### Usage + +To define a list of routes, you need to specify a host DOM element, a default route and a key-value map of possible routes and respective [modules](mithril.module.md) to be rendered. + +The example below defines 3 routes, to be rendered in ``. `home`, `login` and `dashboard` are modules. We'll see how to define a module in a bit. + +```javascript +m.route(document.body, "/", { + "/": home, + "/login": login, + "/dashboard": dashboard, +}); +``` + +Routes can take arguments, by prefixing words with a colon `:` + +The example below shows a route that takes an `userID` parameter + +```javascript +//a sample module +var dashboard = { + controller: function() { + this.id = m.route.param("userID"); + }, + view: function(controller) { + m.render("body", controller.id); + } +} + +//define a route +m.route(document.body, "/dashboard/johndoe", { + "/dashboard/:userID": dashboard +}); + +//setup routes to start w/ the `#` symbol +m.route.mode = "hash"; +``` + +This redirects to the URL `http://server/#/dashboard/johndoe` and yields: + +```markup +johndoe +``` + +Above, `dashboard` is a module. It contains a `controller` and a `view` properties. When the URL matches a route, the respective module's controller is instantiated and passed as a parameter to the view. + +In this case, since there's only route, the app redirects to the default route `"/dashboard/johndoe"`. + +The string `johndoe` is bound to the `:userID` parameter, which can be retrived programmatically in the controller via `m.route.param("userID")`. + +The `m.route.mode` defines which part of the URL to use for routing. + +--- + +#### Signature + +[How to read signatures](how-to-read-signatures.md) + +```clike +void route(DOMElement rootElement, String defaultRoute, Object routes) { String mode, String param(String key) } + +where: + Module :: Object { void controller(), void view(Object controllerInstance) } +``` + +- **DOMElement root** + + A DOM element which will contain the view's template. + +- **String defaultRoute** + + The route to redirect to if the current URL does not match any of the defined routes + +- **Object routes** + + A key-value map of possible routes and their respective modules. Keys are expected to be absolute pathnames, but can include dynamic parameters. Dynamic parameters are words preceded by a colon `:` + + `{'/path/to/page/': pageModule}` - a route with a basic pathname + + `{'/path/to/page/:id': pageModule}` - a route with a pathname that contains a dynamic parameter called `id`. This route would be selected if the URL was `/path/to/page/1`, `/path/to/page/test`, etc + + `{'/user/:userId/book/:bookId': userBookModule}` - a route with a pathname that contains two parameters + + Dynamic parameters are wild cards that allow selecting a module based on a URL pattern. The values that replace the dynamic parameters in a URL are available via `m.route.param()` + + Note that the URL component used to resolve routes is dependent on `m.route.mode`. By default, the querystring is considered the URL component to test against the routes collection + + If the current page URL matches a route, its respective module is activated. See `m.module` for information on modules. + +- + + #### m.route.mode + + **String mode** + + The `m.route.mode` property defines which URL portion is used to implement the routing mechanism. Its value can be set to either "search", "hash" or "pathname". Default value is "search" + + - `search` mode uses the querystring. This allows named anchors (i.e. `Back to top`, ``) to work on the page, but routing changes causes page refreshes in IE8, due to its lack of support for `history.pushState`. + + Example URL: `http://server/?/path/to/page` + + - `hash` mode uses the hash. It's the only mode in which routing changes do not cause page refreshes in any browser. However, this mode does not support named anchors. + + Example URL: `http://server/#/path/to/page` + + - `pathname` mode allows routing URLs that contains no special characters, however this mode requires server-side setup in order to support bookmarking and page refreshes. It also causes page refreshes in IE8. + + Example URL: `http://server/path/to/page` + + The simplest server-side setup possible to support pathname mode is to serve the same content regardless of what URL is requested. In Apache, this URL rewriting can be achieved using ModRewrite. + +- + + #### m.route.param + + **String param(String key)** + + Route parameters are dynamic values that can be extracted from the URL based on the signature of the currently active route. + + A route without parameters looks like this: + + `"/path/to/page/"` + + A route with parameters might look like this: + + `"/path/to/page/:id"` - here `id` is the name of the route parameter + + If the currently active route is `/dashboard/:userID` and the current URL is `/dashboard/johndoe`, then calling `m.route.param("userID")` returns `"johndoe"` + + - **String key** + + The name of a route parameter + + - **returns String value** + + The value that maps to the parameter specified by `key` + +--- + + + +### Redirecting + +#### Usage + +You can programmatically redirec to another page. Given the example in the "Defining Routes" section: + +```javascript +m.route("/dashboard/marysue"); +``` + +redirects to `http://server/#/dashboard/marysue` + +--- + +#### Signature + +[How to read signatures](how-to-read-signatures.md) + +```clike +void route(String path) +``` + +- **String path** + + The route to redirect to. Note that to redirect to a different page outside of the scope of Mithril's routing, you should use `window.location` + +--- + + + +### Mode abstraction + +#### Usage + +This method is meant to be used with a virtual element's `config` attribute. For example: + +```javascript +//Note that the '#' is not required in `href`, thanks to the `config` setting. +m("a[href='/dashboard/alicesmith']", {config: m.route}); +``` + +This makes the href behave correctly regardless of which `m.route.mode` is selected. It's a good practice to always use the idiom above, instead of hardcoding `?` or `#` in the href attribute. + +See [`m()`](mithril.md) for more information on virtual elements. + +--- + +#### Signature + +[How to read signatures](how-to-read-signatures.md) + +```clike +void route(DOMElement element, Boolean isNew) +``` + +- **DOMElement element** + + an anchor element `` with an `href` attribute that points to a route + +- **Boolean isInitialized** + + the method does not run if this flag is set to true. This is to make the method compatible with virtual DOM elements' `config` attribute (see [`m()`](mithril)) \ No newline at end of file diff --git a/docs/mithril.sync.md b/docs/mithril.sync.md new file mode 100644 index 00000000..e5d37042 --- /dev/null +++ b/docs/mithril.sync.md @@ -0,0 +1,48 @@ +## m.sync + +This method takes a list of promises and returns a promise that resolves when all promises in the input list have resolved. See [`m.deferred`](mithril.deferred) for more information on promises. + +--- + +### Usage + +```javascript +var greetAsync = function(delay) { + var deferred = m.deferred(); + setTimeout(function() { + deferred.resolve("hello"); + }, delay); + return deferred.promise; +}; + +m.sync([ + greetAsync(1000), + greetAsync(1500) +]).then(function(args) { + console.log(args); // ["hello", "hello"] +}); +``` + +--- + +### Signature + +[How to read signatures](how-to-read-signatures.md) + +```clike +Promise sync(Array promises) + +where: + Promise :: GetterSetter { Promise then(any successCallback(any value), any errorCallback(any value)) } + GetterSetter :: any getterSetter([any value]) +``` + +- **Array promises** + + A list of promises to synchronize + +- **return Promise promise** + + The promise of the deferred object that is resolved when all input promises have been resolved + + The callbacks for this promise receive as a parameter an Array containing the values of all the input promises \ No newline at end of file diff --git a/docs/mithril.trust.md b/docs/mithril.trust.md new file mode 100644 index 00000000..7b869298 --- /dev/null +++ b/docs/mithril.trust.md @@ -0,0 +1,68 @@ +## m.trust + +If you're writing a template for a view, use `m()` instead. + +This method flags a string as trusted HTML. + +Trusted HTML is allowed to render arbitrary, potentially invalid markup, as well as run arbitrary javascript, and therefore the developer is responsible for either: + +- sanitizing the markup contained in the string, or + +- acknowledging that the string is authorized to run any code that may be contained within it. + +Note that browsers ignore ` + + +``` + +--- + +## Usage of m.redraw + +`m.redraw` is a method that allows you to render a template outside the scope of Mithril's auto-redrawing system. + +Calling of this method while using `m.module` or `m.route` should only be done if you have recurring asynchronous view updates (i.e. something that uses setInterval). + +If you're integrating other non-recurring services (e.g. calling setTimeout), you should use [`m.startComputation` / `m.emdComputation`](mithril.computation.md) instead. + +This is the most potentially expensive method in Mithril and should not be used at a rate faster than the rate at which the native `requestAnimationFrame` method fires (i.e. the rate at which browsers are comfortable calling recurring rendering-intensive code). Typically, this rate is around 60 calls per second. + +If you call this method more often than that, Mithril may ignore calls or defer them to the next browser repaint cycle. + +If calls are more expensive than a repaint window, the browser may drop frames, resulting in choppy animations. It's your responsibility to make sure single iterations of animation rendering code don't take longer than 16ms (for a frequency of 60 frames-per-second). + +In addition, note that template performance, both in Mithril templates as well as in general, is dependent on markup complexity. You are responsible for ensuring that templates aren't too big to render efficiently. diff --git a/docs/refactoring.md b/docs/refactoring.md new file mode 100644 index 00000000..8746df6a --- /dev/null +++ b/docs/refactoring.md @@ -0,0 +1,6 @@ +## Refactoring + +Below are some common refactoring patterns: + +### Porting legacy code to Mithril + diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 00000000..bca5cad2 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,24 @@ +## Roadmap + +Things that would be useful to have (though likely not as part of Mithril core) + +### Utilities + +- Formatters / parsers + - i18n + - Date + - Absolute (e.g. Jan 1, 1970 12:00 AM) + - Relative (e.g. 10 days ago) + - Number (e.g. 1,234.5) + - Currency (e.g. $1,000.00) + - Word wrap +- Dependency management +- Functional / relational tools +- Animation + +### Components + +- Autocompleter +- Date/time picker +- Swipe-to-show panel +- Tree \ No newline at end of file diff --git a/docs/routing.md b/docs/routing.md new file mode 100644 index 00000000..1fba9e73 --- /dev/null +++ b/docs/routing.md @@ -0,0 +1,107 @@ +## Routing + +Routing is a system that allows creating Single-Page-Applications (SPA), i.e. applications that can go from a page to another without causing a full browser refresh. + +It enables seamless navigability while preserving the ability to bookmark each page individually, and the ability to navigate the application via the browser's history mechanism. + +Mithril provides utilities to handle three different aspect of routing: + +- defining a list of routes +- programmatically redirecting between routes +- making links in templates routed transparently and unobtrusively + +--- + +### Defining routes + +To define a list of routes, you need to specify a host DOM element, a default route and a key-value map of possible routes and respective [modules](mithril.module.md) to be rendered. + +The example below defines 3 routes, to be rendered in ``. `home`, `login` and `dashboard` are modules. We'll see how to define a module in a bit. + +```javascript +m.route(document.body, "/", { + "/": home, + "/login": login, + "/dashboard": dashboard, +}); +``` + +Routes can take arguments, by prefixing words with a colon `:` + +The example below shows a route that takes an `userID` parameter + +```javascript +//a sample module +var dashboard = { + controller: function() { + this.id = m.route.param("userID"); + }, + view: function(controller) { + m.render("body", controller.id); + } +} + +//define a route +m.route(document.body, "/dashboard/johndoe", { + "/dashboard/:userID": dashboard +}); + +//setup routes to start w/ the `#` symbol +m.route.mode = "hash"; +``` + +This redirects to the URL `http://server/#/dashboard/johndoe` and yields: + +```markup +johndoe +``` + +Above, `dashboard` is a module. It contains a `controller` and a `view` properties. When the URL matches a route, the respective module's controller is instantiated and passed as a parameter to the view. + +In this case, since there's only route, the app redirects to the default route `"/dashboard/johndoe"` and, under the hood, it calls `m.module(document.body, dashboard)`. + +The string `johndoe` is bound to the `:userID` parameter, which can be retrived programmatically in the controller via `m.route.param("userID")`. + +The `m.route.mode` property defines which URL portion is used to implement the routing mechanism. Its value can be set to either "search", "hash" or "pathname". The default value is "search" + +- `search` mode uses the querystring. This allows named anchors (i.e. `Back to top`, ``) to work on the page, but routing changes causes page refreshes in IE8, due to its lack of support for `history.pushState`. + + Example URL: `http://server/?/path/to/page` + +- `hash` mode uses the hash. It's the only mode in which routing changes do not cause page refreshes in any browser. However, this mode does not support named anchors and browser history lists. + + Example URL: `http://server/#/path/to/page` + +- `pathname` mode allows routing URLs that contains no special characters, however this mode requires server-side setup in order to support bookmarking and page refreshes. It also causes page refreshes in IE8. + + Example URL: `http://server/path/to/page` + + The simplest server-side setup possible to support pathname mode is to serve the same content regardless of what URL is requested. In Apache, this URL rewriting can be achieved using ModRewrite. + + +--- + +### Redirecting + +You can programmatically redirec to another page. Given the example in the "Defining Routes" section: + +```javascript +m.route("/dashboard/marysue"); +``` + +redirects to `http://server/#/dashboard/marysue` + +--- + +### Mode abstraction + +This method is meant to be used with a virtual element's `config` attribute. For example: + +```javascript +//Note that the '#' is not required in `href`, thanks to the `config` setting. +m("a[href='/dashboard/alicesmith']", {config: m.route}); +``` + +This makes the href behave correctly regardless of which `m.route.mode` is selected. It's a good practice to always use the idiom above, instead of hardcoding `?` or `#` in the href attribute. + +See [`m()`](mithril.md) for more information on virtual elements. diff --git a/docs/tools.md b/docs/tools.md new file mode 100644 index 00000000..202de480 --- /dev/null +++ b/docs/tools.md @@ -0,0 +1,31 @@ +## Tools + +### HTML to Mithril Template Converter + +If you already have your HTML written and want to convert it into a Mithril template, use the tool below. + +[Template Converter](tools/template-converter.html) + +--- + +### Mithril Template Compiler + +You can pre-compile Mithril templates to make them run faster. For more information see this page: + +[Compiling Templates](compiling-templates.md) + +--- + +### Internet Explorer Compatibility + +Mithril relies on some Ecmascript 5 features, namely: `Array::indexOf` and `Object::keys`, as well as the `JSON` object. + +You can use polyfill libraries to support these features in IE7. + +- [ES5 Shim](https://github.com/es-shims/es5-shim) or Mozilla.org's [Array::indexOf](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf) and [Object::keys](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys) polyfills + +- [JSON2.js](https://github.com/douglascrockford/JSON-js/blob/master/json2.js) + +Mithril also has a dependency on XMLHttpRequest. If you wish to support IE6, you'll need [a shim for it](https://gist.github.com/Contra/2709462). IE7 and lower do not support cross-domain AJAX requests. + +In addition, note that most `m.route` modes rely on `history.pushState` in order to allow moving from one page to another without a browser refresh. [IE9 and lower](http://caniuse.com/#search=pushstate) do not support this feature and will gracefully degrade to page refreshes instead. \ No newline at end of file diff --git a/docs/web-services.md b/docs/web-services.md new file mode 100644 index 00000000..e3bbdb66 --- /dev/null +++ b/docs/web-services.md @@ -0,0 +1,226 @@ +## Web Services + +Mithril provides a high-level utility for working with web services, which allows writing asynchronous code relatively procedurally. + +It provides a number of useful features out of the box: + +- The ability to get an early reference to a container that will hold the asynchronous response +- The ability to queue operations to be performed after the asynchronous request completes +- The ability to "cast" the response to a class of your choice +- The ability to unwrap data in a response that includes metadata properties + +--- + +### Basic usage + +The basic usage pattern for `m.request` returns an [`m.prop`](mithril.prop.md) getter-setter, which is populated when the AJAX request completes. + +The returned getter-setter can be thought of as a box: you can pass this reference around cheaply, and you can "unwrap" its value when needed. + +```javascript +var users = m.request({method: "GET", url: "/user"}); + +//assuming the response contains the following data: `[{name: "John"}, {name: "Mary"}]` +//then when resolved (e.g. in a view), the `users` getter-setter will contain a list of users +//i.e. users() //[{name: "John"}, {name: "Mary"}] +``` + +Note that this getter-setter holds an *undefined* value until the AJAX request completes. Attempting to unwrap its value early will likely result in errors. + +The returned getter-setter also implements the [promise](mithril.deferred.md) interface (also known as a *thennable*): this is the mechanism you should always use to queue operations to be performed on the data from the web service. + +The simplest use case of this feature is to implement functional value assignment via `m.prop` (i.e. the same thing as above). You can bind a pre-existing getter-setter by passing it in as a parameter to a `.then` method: + +```javascript +var users = m.prop([]); //default value + +m.request({method: "GET", url: "/user"}).then(users) +//assuming the response contains the following data: `[{name: "John"}, {name: "Mary"}]` +//then when resolved (e.g. in a view), the `users` getter-setter will contain a list of users +//i.e. users() //[{name: "John"}, {name: "Mary"}] +``` + +This syntax allows you to bind intermediate results before piping them down for further processing, for example: + +```javascript +var users = m.prop([]); //default value +var doSomething = function() { /*...*/ } + +m.request({method: "GET", url: "/user"}).then(users).then(doSomething) +``` + +While both basic assignment syntax and thennable syntax can be used to the same effect, typically it's recommended that you use the assignment syntax in the first example whenever possible, as it's easier to read. + +The thennable mechanism is intended to be used in 3 ways: + +- in the model layer: to process web service data in transformative ways (e.g. filtering a list based on a parameter that the web service doesn't support) +- in the controller layer: to bind redirection code upon a condition +- in the controller layer: to bind error messages + +#### Processing web service data + +This step is meant to be done in the model layer. Doing it in the controller level is also possible, but philosophically not recommended, because by tying logic to a controller, the code becomes harder to reuse due to unrelated controller dependencies. + +In the example below, the `listEven` method returns a getter-setter that resolves to a list of users containing only users whose id is even. + +```javascript +//model +var User = {} + +User.listEven = function() { + return m.request({method: "GET", url: "/user"}).then(function(list) { + return list.filter(function(user) {return user.id % 2 == 0}); + }); +} + +//controller +var controller = function() { + this.users = User.listEven() +} +``` + +#### Bind redirection code + +This step is meant to be done in the controller layer. Doing it in the model level is also possible, but philosophically not recommended, because by tying redirection to the model, the code becomes harder to reuse due to overly tight coupling. + +In the example below, we use the previously defined `listEven` model method and queue a controller-level function that redirects to another page if the user list is empty. + +```javascript +//controller +var controller = function() { + this.users = User.listEven().then(function(users) { + if (users.length == 0) m.route("/add"); + }) +} +``` + +#### Binding errors + +Mithril thennables take two functions as optional parameters: the first parameter is called if the web service request completes successfully. The second one is called if it completes with an error. + +Error binding is meant to be done in the controller layer. Doing it in the model level is also possible, but generally leads to more code in order to connect all the dots. + +In the example below, we bind an error getter-setter to our previous controller so that the `error` variable gets populated if the server throws an error. + +```javascript +//controller +var controller = function() { + this.error = m.prop("") + + this.users = User.listEven().then(function(users) { + if (users.length == 0) m.route("/add"); + }, this.error) +} +``` + +If the controller doesn't already have a success callback to run after a request resolves, you can still bind errors like this: + +```javascript +//controller +var controller = function() { + this.error = m.prop("") + + this.users = User.listEven().then(null, this.error) +} +``` + +--- + +### Queuing Operations + +As you saw, you can chain operations that act on the response data. Typically this is required in three situations: + +- in model-level methods if client-side processing is needed to make the data useful for a controller or view. +- in the controller, to redirect after a model service resolves. +- in the controller, to bind error messages + +In the example below, we take advantage of queuing to debug the ajax response data prior to doing further processing on the user list + +```javascript +var users = m.request({method: "GET", url: "/user"}) + .then(console.log); + .then(function(users) { + //add one more user to the response + return users.concat({name: "Jane"}) + }) + +//assuming the response contains the following data: `[{name: "John"}, {name: "Mary"}]` +//then when resolved (e.g. in a view), the `users` getter-setter will contain a list of users +//i.e. users() //[{name: "John"}, {name: "Mary"}, {name: "Jane"}] +``` + +--- + +### Casting the Response Data to a Class + +It's possible to auto-cast a JSON response to a class. This is useful when we want to control access to certain properties in an object, as opposed to exposing all the fields in POJOs (plain old javascript objects) for arbitrary processing. + +In the example below, `User.list` returns a list of `User` instances. + +```javascript +var User = function(data) { + this.name = m.prop(data.name); +} + +User.list = function() { + return m.request({method: "GET", url: "/user", type: User}); +} + +var users = User.list(); +//assuming the response contains the following data: `[{name: "John"}, {name: "Mary"}]` +//then when resolved (e.g. in a view), `users` will contain a list of User instances +//i.e. users()[0].name() == "John" +``` + +--- + +### Unwrapping Response Data + +Often, web services return the relevant data wrapped in objects that contain metadata. + +Mithril allows you to unwrap the relevant data, by providing two callback hooks: `unwrapSuccess` and `unwrapError`. + +These hooks allow you to unwrap different parts of the response data depending on whether it succeed or failed. + +```javascript +var users = m.request({ + method: "GET", + url: "/user", + unwrapSuccess: function(response) { + return response.data; + }, + unwrapError: function(response) { + return response.error; + } +}); + +//assuming the response is: `{data: [{name: "John"}, {name: "Mary"}], count: 2}` +//then when resolved (e.g. in a view), the `users` getter-setter will contain a list of users +//i.e. users() //[{name: "John"}, {name: "Mary"}] +``` + +--- + +### Using Different Data Transfer Formats + +By default, `m.request` uses JSON to send and receive data to web services. You can override this by providing `serialize` and `deserialize` options: + +```javascript +var users = m.request({ + method: "GET", + url: "/user", + serialize: mySerializer, + deserialize: myDeserializer +}); +``` + +One typical way to override this is to receive as-is responses. The example below shows how to receive a plain string from a txt file. + +```javascript +var file = m.request({ + method: "GET", + url: "myfile.txt", + deserialize: function(value) {return value;} +}); +``` + diff --git a/mithril.js b/mithril.js new file mode 100644 index 00000000..9fcc5e19 --- /dev/null +++ b/mithril.js @@ -0,0 +1,415 @@ +new function(window) { + var selectorCache = {} + var type = {}.toString + var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.+?)\2)?\]/ + + Mithril = m = function() { + var args = arguments + var hasAttrs = type.call(args[1]) == "[object Object]" + var attrs = hasAttrs ? args[1] : {} + var classAttrName = "class" in attrs ? "class" : "className" + var cell = selectorCache[args[0]] + if (cell === undefined) { + selectorCache[args[0]] = 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] || true + } + } + if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" ") + } + cell = clone(cell) + cell.attrs = clone(cell.attrs) + 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(parent, data, cached) { + if (data === null || data === undefined) return + + var cachedType = type.call(cached), dataType = type.call(data) + if (cachedType != dataType) { + if (cached !== null && cached !== undefined) clear(cached.nodes) + cached = new data.constructor + cached.nodes = [] + } + + if (dataType == "[object Array]") { + var nodes = [], intact = cached.length === data.length + for (var i = 0; i < data.length; i++) { + var item = build(parent, data[i], cached[i]) + if (!item.nodes.intact) intact = false + cached[i] = item + } + if (!intact) { + for (var i = 0; i < data.length; i++) nodes = nodes.concat(cached[i].nodes) + for (var i = nodes.length, node; node = cached.nodes[i]; i++) if (node.parentNode !== null) node.parentNode.removeChild(node) + for (var i = cached.nodes.length, node; node = nodes[i]; i++) if (node.parentNode === null) parent.appendChild(node) + cached.length = data.length + cached.nodes = nodes + } + } + else if (dataType == "[object Object]") { + if (data.tag != cached.tag || Object.keys(data.attrs).join() != Object.keys(cached.attrs).join()) clear(cached.nodes) + + var node, isNew = cached.nodes.length === 0 + if (isNew) { + node = window.document.createElement(data.tag) + cached = {tag: data.tag, attrs: setAttributes(node, data.attrs, {}), children: build(node, data.children, cached.children), nodes: [node]} + parent.appendChild(node) + } + else { + node = cached.nodes[0] + setAttributes(node, data.attrs, cached.attrs) + cached.children = build(node, data.children, cached.children) + cached.nodes.intact = true + } + if (type.call(data.attrs["config"]) == "[object Function]") data.attrs["config"](node, !isNew) + } + else { + var node + if (cached.nodes.length === 0) { + if (data.$trusted) { + var lastChild = parent.lastChild + parent.insertAdjacentHTML("beforeend", data) + node = lastChild ? lastChild.nextSibling : parent.firstChild + } + else { + node = window.document.createTextNode(data) + parent.appendChild(node) + } + cached = "string number boolean".indexOf(typeof data) > -1 ? new data.constructor(data) : data + cached.nodes = [node] + } + else if (cached.valueOf() !== data.valueOf()) { + if (data.$trusted) { + var current = cached.nodes[0], nodes = [current] + if (current) { + while (current = current.nextSibling) nodes.push(current) + clear(nodes) + var lastChild = parent.lastChild + parent.insertAdjacentHTML("beforeend", data) + node = lastChild ? lastChild.nextSibling : parent.firstChild + } + else parent.innerHTML = data + } + else { + node = cached.nodes[0] + parent.appendChild(node) + node.nodeValue = data + } + cached = new data.constructor(data) + cached.nodes = [node] + } + else cached.nodes.intact = true + } + + return cached + } + function setAttributes(node, dataAttrs, cachedAttrs) { + for (var attrName in dataAttrs) { + var dataAttr = dataAttrs[attrName] + if (!(attrName in cachedAttrs) || (cachedAttrs[attrName] !== dataAttr)) { + cachedAttrs[attrName] = dataAttr + if (attrName == "config") continue + if (attrName.indexOf("on") == 0 && typeof dataAttr == "function") dataAttr = autoredraw(dataAttr, node) + if (attrName == "style") for (var rule in dataAttr) node.style[rule] = dataAttr[rule] + else if (attrName in node) node[attrName] = dataAttr + else node.setAttribute(attrName, dataAttr) + } + } + return cachedAttrs + } + function clear(nodes) { + for (var i = 0; i < nodes.length; i++) nodes[i].parentNode.removeChild(nodes[i]) + nodes.length = 0 + } + function clone(object) { + var result = {} + for (var prop in object) result[prop] = object[prop] + return result + } + function autoredraw(callback, object) { + return function() { + m.startComputation() + var output = callback.apply(object || window, arguments) + m.endComputation() + return output + } + } + + 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 !== html) { + window.document.replaceChild(html, window.document.documentElement) + } + } + } + var nodeCache = [], cellCache = {} + m.render = function(root, cell) { + var index = nodeCache.indexOf(root) + var id = index < 0 ? nodeCache.push(root) - 1 : index + var node = root == window.document || root == window.document.documentElement ? documentNode : root + cellCache[id] = build(node, cell, cellCache[id]) + } + + m.trust = function(value) { + value = new String(value) + value.$trusted = true + return value + } + + var currentRoot, currentModule = {view: function() {}}, currentController = {}, now = 0, lastRedraw = 0, lastRedrawId = 0 + m.module = function(root, module) { + m.startComputation() + currentRoot = root + currentModule = module + currentController = new module.controller + m.endComputation() + } + m.redraw = function() { + m.render(currentRoot || window.document, currentModule.view(currentController)) + lastRedraw = now + } + function redraw() { + now = window.performance && window.performance.now ? window.performance.now() : new window.Date().getTime() + if (now - lastRedraw > 16) m.redraw() + else { + var cancel = window.cancelAnimationFrame || window.clearTimeout + var defer = window.requestAnimationFrame || window.setTimeout + cancel(lastRedrawId) + lastRedrawId = defer(m.redraw, 0) + } + } + + var pendingRequests = 0, computePostRedrawHook = null + m.startComputation = function() {pendingRequests++} + m.endComputation = function() { + pendingRequests = Math.max(pendingRequests - 1, 0) + if (pendingRequests == 0) { + redraw() + if (computePostRedrawHook) { + computePostRedrawHook() + computePostRedrawHook = null + } + } + } + + m.withAttr = function(prop, withAttrCallback) { + return function(e) {withAttrCallback(prop in e.currentTarget ? e.currentTarget[prop] : e.currentTarget.getAttribute(prop))} + } + + //routing + var modes = {pathname: "", hash: "#", search: "?"} + var redirect = function() {}, routeParams = {} + m.route = function() { + if (arguments.length == 3) { + var root = arguments[0], defaultRoute = arguments[1], router = arguments[2] + redirect = function(source) { + var path = source.slice(modes[m.route.mode].length) + if (!routeByValue(root, router, path)) { + m.route(defaultRoute, true) + } + } + var listener = m.route.mode == "hash" ? "onhashchange" : "onpopstate" + window[listener] = function() { + redirect(window.location[m.route.mode]) + } + computePostRedrawHook = scrollToHash + window[listener]() + } + else if (arguments[0].addEventListener) { + var element = arguments[0] + var isInitialized = arguments[1] + if (!isInitialized) { + element.removeEventListener("click", routeUnobtrusive) + element.addEventListener("click", routeUnobtrusive) + } + } + else if (typeof arguments[0] == "string") { + var route = arguments[0] + var shouldReplaceHistoryEntry = arguments[1] === true + if (window.history.pushState) { + computePostRedrawHook = function() { + window.history[shouldReplaceHistoryEntry ? "replaceState" : "pushState"](null, window.document.title, modes[m.route.mode] + route) + scrollToHash() + } + redirect(modes[m.route.mode] + route) + } + else window.location[m.route.mode] = route + } + } + m.route.param = function(key) {return routeParams[key]} + m.route.mode = "search" + function routeByValue(root, router, path) { + for (var route in router) { + if (route == path) return !void m.module(root, router[route]) + + var matcher = new RegExp("^" + route.replace(/:[^\/]+/g, "([^\\/]+)") + "$") + if (matcher.test(path)) { + return !void path.replace(matcher, function() { + var keys = route.match(/:[^\/]+/g) + var values = [].slice.call(arguments, 1, -2) + routeParams = {} + for (var i = 0; i < keys.length; i++) routeParams[keys[i].slice(1)] = values[i] + m.module(root, router[route]) + }) + } + } + } + function routeUnobtrusive(e) { + e.preventDefault() + m.route(e.currentTarget.getAttribute("href")) + } + function scrollToHash() { + if (m.route.mode != "hash" && window.location.hash) window.location.hash = window.location.hash + } + + //model + m.prop = function(store) { + return function() { + if (arguments.length) store = arguments[0] + return store + } + } + + m.deferred = function() { + var resolvers = [], rejecters = [] + var object = { + resolve: function(value) { + for (var i = 0; i < resolvers.length; i++) resolvers[i](value) + }, + reject: function(value) { + for (var i = 0; i < rejecters.length; i++) rejecters[i](value) + }, + promise: m.prop() + } + object.promise.resolvers = resolvers + object.promise.then = function(success, error) { + var next = m.deferred() + if (!success) success = identity + if (!error) error = identity + resolvers.push(function(value) { + var result = success(value) + next.resolve(result !== undefined ? result : value) + }) + rejecters.push(function(value) { + var result = error(value) + next.reject(result !== undefined ? result : value) + }) + return next.promise + } + return object + } + m.sync = function(args) { + var method = "resolve" + function synchronizer(resolved) { + return function(value) { + results.push(value) + if (!resolved) method = "reject" + if (results.length == args.length) { + deferred.promise(results) + deferred[method](results) + } + return value + } + } + + var deferred = m.deferred() + var results = [] + for (var i = 0; i < args.length; i++) { + args[i].then(synchronizer(true), synchronizer(false)) + } + return deferred.promise + } + function identity(value) {return value} + + function ajax(options) { + var xhr = window.XDomainRequest ? new window.XDomainRequest : new window.XMLHttpRequest + xhr.open(options.method, options.url, true, options.user, options.password) + xhr.onload = typeof options.onload == "function" ? options.onload : function() {} + xhr.onerror = typeof options.onerror == "function" ? options.onerror : function() {} + xhr.withCredentials = true + if (typeof options.config == "function") options.config(xhr, options) + xhr.send(options.data) + return xhr + } + function querystring(object, prefix) { + var str = [] + for(var prop in object) { + var key = prefix ? prefix + "[" + prop + "]" : prop, value = object[prop] + str.push(typeof value == "object" ? querystring(value, key) : encodeURIComponent(key) + "=" + encodeURIComponent(value)) + } + return str.join("&") + } + function bindData(xhrOptions, data, serialize) { + if (data && Object.keys(data).length > 0) { + if (xhrOptions.method == "GET") { + xhrOptions.url = xhrOptions.url + (xhrOptions.url.indexOf("?") < 0 ? "?" : "&") + querystring(data) + } + else xhrOptions.data = serialize(data) + } + return xhrOptions + } + function parameterizeUrl(url, data) { + var tokens = url.match(/:\w+/g) + 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) { + m.startComputation() + var deferred = m.deferred() + var serialize = xhrOptions.serialize || JSON.stringify + var deserialize = xhrOptions.deserialize || JSON.parse + xhrOptions.url = parameterizeUrl(xhrOptions.url, xhrOptions.data) + xhrOptions = bindData(xhrOptions, xhrOptions.data, serialize) + xhrOptions.onload = xhrOptions.onerror = function(e) { + var unwrap = (e.type == "load" ? xhrOptions.unwrapSuccess : xhrOptions.unwrapError) || identity + var response = unwrap(deserialize(e.target.responseText)) + if (response instanceof 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.promise(response) + deferred[e.type == "load" ? "resolve" : "reject"](response) + m.endComputation() + } + ajax(xhrOptions) + deferred.promise.then = propBinder(deferred.promise) + return deferred.promise + } + function propBinder(promise) { + var bind = promise.then + return function(success, error) { + var next = bind(function(value) {return next(success(value))}, function(value) {return next(error(value))}) + next.then = propBinder(next) + return next + } + } + + //testing API + m.deps = function(mock) {return window = mock} +}(this) \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..052f8b01 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "mithril", + "version": "0.1.0", + "scripts": { + "test": "grunt test" + }, + "devDependencies": { + "grunt-cli": "*", + "grunt-contrib-copy": "*", + "grunt-contrib-uglify": "*", + "grunt-contrib-clean": "*", + "grunt-contrib-concat": "*", + "grunt-contrib-watch": "*", + "grunt-execute": "*", + "grunt-md2html": "*", + "grunt-replace": "*", + "grunt-zip": "*" + } +} \ No newline at end of file diff --git a/tests/index.html b/tests/index.html new file mode 100644 index 00000000..0db72d14 --- /dev/null +++ b/tests/index.html @@ -0,0 +1,7 @@ + + + + + + +

Open the console to see the test report

\ No newline at end of file diff --git a/tests/mithril-tests.js b/tests/mithril-tests.js new file mode 100644 index 00000000..f8686fc6 --- /dev/null +++ b/tests/mithril-tests.js @@ -0,0 +1,226 @@ +function testMithril(mock) { + m.deps(mock) + + //m + test(function() {return m("div").tag === "div"}) + test(function() {return m(".foo").tag === "div"}) + test(function() {return m(".foo").attrs.className === "foo"}) + test(function() {return m("[title=bar]").tag === "div"}) + test(function() {return m("[title=bar]").attrs.title === "bar"}) + test(function() {return m("[title=\'bar\']").attrs.title === "bar"}) + test(function() {return m("[title=\"bar\"]").attrs.title === "bar"}) + test(function() {return m("div", "test").children === "test"}) + test(function() {return m("div", ["test"]).children[0] === "test"}) + test(function() {return m("div", {title: "bar"}, "test").attrs.title === "bar"}) + test(function() {return m("div", {title: "bar"}, "test").children === "test"}) + test(function() {return m("div", {title: "bar"}, ["test"]).children[0] === "test"}) + test(function() {return m("div", {title: "bar"}, m("div")).children.tag === "div"}) + test(function() {return m("div", {title: "bar"}, [m("div")]).children[0].tag === "div"}) + test(function() {return m("div", ["a", "b"]).children.length === 2}) + test(function() {return m("div", [m("div")]).children[0].tag === "div"}) + test(function() {return m("div", m("div")).attrs.tag === "div"}) //yes, this is expected behavior: see method signature + + //m.module + for (var i = 0; i < 2; i++) { + //first iteration tests immediate rendering + //second iteration tests deferred rendering + test(function() { + var root = mock.document.createElement("div") + m.module(root, { + controller: function() {this.value = "test"}, + view: function(ctrl) {return ctrl.value} + }) + return root.childNodes[0].nodeValue === "test" + }) + } + + //m.withAttr + test(function() { + var value + var handler = m.withAttr("test", function(data) {value = data}) + handler({currentTarget: {test: "foo"}}) + return value === "foo" + }) + + //m.trust + test(function() {return m.trust("test").valueOf() === "test"}) + + //m.render + test(function() { + var root = mock.document.createElement("div") + m.render(root, "test") + return root.childNodes[0].nodeValue === "test" + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("div", {id: "a"})) + var elementBefore = root.childNodes[0] + m.render(root, m("div", {id: "b"})) + var elementAfter = root.childNodes[0] + return elementBefore === elementAfter + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("#a")) + var elementBefore = root.childNodes[0] + m.render(root, m("#b")) + var elementAfter = root.childNodes[0] + return elementBefore === elementAfter + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("div", {id: "a"})) + var elementBefore = root.childNodes[0] + m.render(root, m("div", {title: "b"})) + var elementAfter = root.childNodes[0] + return elementBefore !== elementAfter + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("#a")) + var elementBefore = root.childNodes[0] + m.render(root, m("[title=b]")) + var elementAfter = root.childNodes[0] + return elementBefore !== elementAfter + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("#a")) + var elementBefore = root.childNodes[0] + m.render(root, "test") + var elementAfter = root.childNodes[0] + return elementBefore !== elementAfter + }) + + //m.redraw + test(function() { + var controller + var root = mock.document.createElement("div") + m.module(root, { + controller: function() {controller = this}, + view: function(ctrl) {return ctrl.value} + }) + controller.value = "foo" + m.redraw() + return root.childNodes[0].nodeValue === "foo" + }) + + //m.route + test(function() { + var root = mock.document.createElement("div") + m.route.mode = "search" + m.route(root, "/test1", { + "/test1": {controller: function() {}, view: function() {return "foo"}} + }) + return mock.location.search == "?/test1" && root.childNodes[0].nodeValue === "foo" + }) + test(function() { + var root = mock.document.createElement("div") + m.route.mode = "pathname" + m.route(root, "/test2", { + "/test2": {controller: function() {}, view: function() {return "foo"}} + }) + return mock.location.pathname == "/test2" && root.childNodes[0].nodeValue === "foo" + }) + test(function() { + var root = mock.document.createElement("div") + m.route.mode = "hash" + m.route(root, "/test3", { + "/test3": {controller: function() {}, view: function() {return "foo"}} + }) + return mock.location.hash == "#/test3" && root.childNodes[0].nodeValue === "foo" + }) + test(function() { + var root = mock.document.createElement("div") + m.route.mode = "search" + m.route(root, "/test4/foo", { + "/test4/:test": {controller: function() {}, view: function() {return m.route.param("test")}} + }) + return mock.location.search == "?/test4/foo" && root.childNodes[0].nodeValue === "foo" + }) + + //m.prop + test(function() { + var prop = m.prop("test") + return prop() === "test" + }) + test(function() { + var prop = m.prop("test") + prop("foo") + return prop() == "foo" + }) + + //m.request + test(function() { + var prop = m.request({method: "GET", url: "test"}) + var e = mock.XMLHttpRequest.$events.pop() + e.target.onload(e) + return prop().method === "GET" && prop().url === "test" + }) + test(function() { + var prop = m.request({method: "GET", url: "test"}).then(function(value) {return "foo"}) + var e = mock.XMLHttpRequest.$events.pop() + e.target.onload(e) + return prop() === "foo" + }) + + //m.deferred + test(function() { + var value + var deferred = m.deferred() + deferred.promise.then(function(data) {value = data}) + deferred.resolve("test") + return value === "test" + }) + test(function() { + var value + var deferred = m.deferred() + deferred.promise.then(function(value) {return "foo"}).then(function(data) {value = data}) + deferred.resolve("test") + return value === "foo" + }) + test(function() { + var value + var deferred = m.deferred() + deferred.promise.then(null, function(data) {value = data}) + deferred.reject("test") + return value === "test" + }) + test(function() { + var value + var deferred = m.deferred() + deferred.promise.then(null, function(value) {return "foo"}).then(null, function(data) {value = data}) + deferred.reject("test") + return value === "foo" + }) + + //m.sync + test(function() { + var value + var deferred1 = m.deferred() + var deferred2 = m.deferred() + m.sync([deferred1.promise, deferred2.promise]).then(function(data) {value = data}) + deferred1.resolve("test") + deferred2.resolve("foo") + return value[0] === "test" && value[1] === "foo" + }) + + //m.startComputation/m.endComputation + test(function() { + var controller + var root = mock.document.createElement("div") + m.module(root, { + controller: function() {controller = this}, + view: function(ctrl) {return ctrl.value} + }) + m.startComputation() + controller.value = "foo" + m.endComputation() + return root.childNodes[0].nodeValue === "foo" + }) +} + +//mocks +testMithril(mock.window) + +test.print(console.log) \ No newline at end of file diff --git a/tests/mock.js b/tests/mock.js new file mode 100644 index 00000000..8a5ef789 --- /dev/null +++ b/tests/mock.js @@ -0,0 +1,71 @@ +var mock = {} +mock.window = new function() { + var window = {} + window.document = {} + window.document.childNodes = [] + window.document.createElement = function(tag) { + return { + childNodes: [], + nodeName: tag.toUpperCase(), + appendChild: window.document.appendChild, + removeChild: window.document.removeChild, + replaceChild: window.document.replaceChild, + setAttribute: function(name, value) { + this[name] = value.toString() + }, + getAttribute: function(name, value) { + return this[name] + }, + } + } + window.document.createTextNode = function(text) { + return {nodeValue: text.toString()} + } + window.document.documentElement = null + window.document.replaceChild = function(newChild, oldChild) { + var index = this.childNodes.indexOf(oldChild) + if (index > -1) this.childNodes.splice(index, 1, newChild) + else this.childNodes.push(newChild) + newChild.parentNode = this + oldChild.parentNode = null + } + window.document.appendChild = function(child) { + this.childNodes.push(child) + child.parentNode = this + } + window.document.removeChild = function(child) { + var index = this.childNodes.indexOf(child) + this.childNodes.splice(index, 1) + child.parentNode = null + } + window.performance = new function () { + var timestamp = 50 + this.$elapse = function(amount) {timestamp = amount} + this.now = function() {return timestamp} + } + window.cancelAnimationFrame = function() {} + window.requestAnimationFrame = function(callback) {callback()} + window.XMLHttpRequest = new function() { + var request = function() { + this.open = function(method, url) { + this.method = method + this.url = url + } + this.send = function() { + this.responseText = JSON.stringify(this) + request.$events.push({type: "load", target: this}) + } + } + request.$events = [] + return request + } + window.location = {search: "", pathname: "", hash: ""}, + window.history = {} + window.history.pushState = function(data, title, url) { + window.location.pathname = window.location.search = window.location.hash = url + }, + window.history.replaceState = function(data, title, url) { + window.location.pathname = window.location.search = window.location.hash = url + } + return window +} \ No newline at end of file diff --git a/tests/test.js b/tests/test.js new file mode 100644 index 00000000..ba134c74 --- /dev/null +++ b/tests/test.js @@ -0,0 +1,13 @@ +function test(condition) { + try {if (!condition()) throw new Error} + catch (e) {test.failures.push(condition)} + test.total++ +} +test.total = 0 +test.failures = [] +test.print = function(print) { + for (var i = 0; i < test.failures.length; i++) { + print(test.failures[i].toString()) + } + print("tests: " + test.total + "\nfailures: " + test.failures.length) +} \ No newline at end of file