├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE.txt ├── README.md ├── bower.json ├── config ├── jshintReporter.js └── webpack.js ├── docs └── FAQ.md ├── gulpfile.js ├── package.json └── src ├── app.js ├── assets ├── apple-touch-icon-precomposed.png ├── browserconfig.xml ├── crossdomain.xml ├── favicon.ico ├── humans.txt ├── robots.txt ├── tile-wide.png └── tile.png ├── components ├── demos │ ├── accordion.js │ └── modal.js ├── experimental │ ├── occlusionScroller.js │ └── todosX │ │ ├── app.js │ │ ├── footer.msx │ │ ├── index.js │ │ ├── item.js │ │ └── model.js ├── mithril.bootstrap │ └── index.js └── todos │ ├── __tests__ │ ├── app-tests.js │ └── model-tests.js │ ├── app.js │ ├── footer.js │ ├── header.js │ ├── index.js │ ├── list-of-tasks.js │ ├── model.js │ ├── new-task.js │ ├── storage.js │ └── task.js ├── pages ├── 404.html └── index.html └── styles ├── .csscomb.json ├── .csslintrc ├── app.less ├── bootstrap.less ├── jumbotron.less ├── mixins.less ├── navbar.less ├── utilities.less └── variables.less /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # These attributes affect how the contents stored in the repository are copied 2 | # to the working tree files when commands such as git checkout and git merge run. 3 | # http://git-scm.com/docs/gitattributes 4 | 5 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Git uses this file to determine which files and directories to ignore 2 | # https://help.github.com/articles/ignoring-files 3 | 4 | build 5 | bower_components 6 | node_modules 7 | npm-debug.log 8 | .DS_Store -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "camelcase": true, 3 | "immed": true, 4 | "indent": 2, 5 | "latedef": true, 6 | "newcap": true, 7 | "quotmark": "single", 8 | 9 | "esnext": true, 10 | "globalstrict": true, 11 | 12 | "browser": true, 13 | "node": true, 14 | 15 | "globals": { 16 | "m": false, 17 | "require": false, 18 | "__dirname": false 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014 Phil Toms (phil.toms@hotmail.co.uk). 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mithril.Elements Starter Kit 2 | [Mithril.Elements] is a thin wrapper around the [Mithril] JavaScript framework that allows you to create composable custom element types: 3 | ```javascript 4 | m('greet', 'Bob') 5 | ``` 6 | becomes: 7 | ```html 8 |
9 | HiBob! 10 |
11 | ``` 12 | 13 | Custom elements are first class Mithril citizens and compose naturally with existing DOM elements: 14 | ```javascript 15 | m('accordion', [ 16 | m('.item', ['Title 1','item line one']), 17 | m('.item', ['Title 2','item line two']), 18 | m('.item', ['Title 3','item line three']), 19 | m('.item', ['Title 4','item line four']) 20 | ]) 21 | ``` 22 | [view in plunkr](http://embed.plnkr.co/FuSEJtlhvv4yqKN8Ohjd/preview) 23 | 24 | Application element types lend themselves to a feature oriented program structure: 25 | ```javascript 26 | m('#todoapp',[ 27 | m('header',[ 28 | m('new-task') 29 | ]), 30 | m('list-of-tasks', [ 31 | m('$task') 32 | ]), 33 | m('footer') 34 | ]) 35 | ``` 36 | [view in plunkr](http://embed.plnkr.co/WIOF43ObW3NL2nW2XPkr/preview) 37 | 38 | Overloading existing DOM tags works too. A huge table might be tamed this way: 39 | ```javascript 40 | m('table', [ 41 | m('thead', ['Name','Posts','Last Topic']), 42 | m('tbody',{state:{rows:12, content:hugeArray}}, function(content){return [ 43 | m('td', content.name), 44 | m('td', content.posts), 45 | m('td', content.lastTopic) 46 | ]}) 47 | ]) 48 | ``` 49 | compiles to: 50 | ```html 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
NamePostsLast Topic
Bob47About occlusion scrolling
66 | ``` 67 | [view in plunkr](http://embed.plnkr.co/TQuzWpBzP4AMN874gOfF/preview) 68 | 69 | 70 | ## Getting Started 71 | Three ways to use Mithril.Elements: 72 | 73 | 1. download [this project]( 74 | https://github.com/philtoms/mithril.elements/archive/master.zip 75 | ) and link to mithril and mithril.elements in the head of your app 76 | 77 | ```html 78 | 79 | 80 | 81 | 82 | 83 | ``` 84 | 85 | 2. easier - npm install mithril.elements into your current Mithril project and require in your app 86 | 87 | ```shell 88 | npm install --save mithril.elements 89 | ``` 90 | 91 | ```javascript 92 | // (Broswerify or WebPack) 93 | var m = require('mithril.elements'); 94 | ``` 95 | 96 | 3. easiest - [clone] or [fork] this repro and start hacking 97 | 98 | ```shell 99 | $ git clone -o upstream https://github.com/philtoms/mithril-starter-kit.git MyApp 100 | $ cd MyApp 101 | $ npm install -g gulp # Install Gulp task runner globally 102 | $ npm install # Install Node.js components listed in ./package.json 103 | bower install # only required for todomvc-common 104 | ``` 105 | shell commands: 106 | ``` 107 | gulp build --release # minify and build to release folder 108 | gulp serve # open browser on port 3000 109 | gulp jest # single pass test runner 110 | gulp tdd # watch + test runner 111 | npm test # run tests in CI (e.g. travis) 112 | npm run-script debug-test # run tests in node-inspector 113 | ``` 114 | 115 | ## Using Mithril.Elements 116 | Mithril.Elements are extended Mithril [components], bound to an element tag name and registered with the application, so that they can be used in-line with default element types in Mithril views. 117 | 118 | An element registration: 119 | ```javascript 120 | m.element('accordion', { 121 | controller: function() { 122 | this.toggle = function(id){ 123 | this.open=id; 124 | } 125 | }, 126 | view: function(ctrl, content) { 127 | display = function(id) { 128 | return 'display:'+(ctrl.open===id? 'block':'none') 129 | } 130 | return m('.accordion', content.map(function(line,id){ 131 | var title = line.children[0], content = line.children[1] 132 | return m(line,{ 133 | onclick:ctrl.toggle.bind(ctrl,id) 134 | },[ 135 | title, 136 | m('div',{style:display(id)},content) 137 | ]) 138 | })) 139 | } 140 | }) 141 | ``` 142 | In the view: 143 | ```javascript 144 | m('accordion', [ 145 | m('.item', ['Title 1','item line one']), 146 | m('.item', ['Title 2','item line two']), 147 | m('.item', ['Title 3','item line three']), 148 | m('.item', ['Title 4','item line four']) 149 | ]) 150 | ``` 151 | 152 | Sometimes you don't need to use the controller part of an element. In this situation you can leave it out of the definition and Mithril.Elements will provide a default controller: 153 | ```javascript 154 | m.element('jumbotron', { 155 | view: function(ctrl,inner) { 156 | return m('.jumbotron',[ 157 | m('.container',[ 158 | inner 159 | ]) 160 | ]) 161 | } 162 | }) 163 | ``` 164 | 165 | Note that the view can still receive the controller instance and can therefore access any state passed to the view through the controller.state property: 166 | ```javascript 167 | view: function(ctrl) { 168 | var count = ctrl.state.count; 169 | } 170 | ``` 171 | ### Element state and the Mithril page life-cycle 172 | All Mithril components have program state - encapsulated in controller logic and typically maintained hierarchically through [m.module] registration. A custom elements program state on the other hand is tied to the life-cycle of its own parent view. This is the main difference between a custom element component and a standard Mithril component: Mithril.Element life-cycle is consistent with DOM element life-cycle. 173 | 174 | Element state come into existence lazily when the element in the view is first created and is maintained until the view is discarded (on a route change for example). In Mithril terms, element state is tied to the ongoing [redraw strategy] so that: 175 | 176 | - **all** - creates element state via a *new* controller instance - *always* 177 | - **diff** - uses the state of the *current* controller instance 178 | - *if it exists* 179 | - *otherwise as* **all** 180 | 181 | ### Element identity 182 | In most cases, this extended state management strategy is silently implemented by Mithril.Elements. In all of the examples presented so far, explicit reference to state management is not mentioned. However there are some programming scenarios where this strategy will fail. 183 | 184 | Mithril.Elements does not attempt to track element state through dynamically changing page layouts and relies instead on view generated identity using the following logical sequence: 185 | 186 | - use the virtual Element key attribute if it exists: 187 | 188 | ```javascript 189 | m('greet',{key:'bob1'}, 'Bob') // component identity is bob1 190 | ``` 191 | 192 | - use the virtual Element id attribute if it exists: 193 | 194 | ```javascript 195 | m('greet#bob2', 'Bob') // component identity is bob2 196 | m('greet',{id:'bob2'}, 'Bob') // component identity is bob2 197 | ``` 198 | - use the element state.id attribute if it exists: 199 | 200 | ```javascript 201 | m('greet',{state:{id='bob3'}}) // component identity is bob3 202 | ``` 203 | 204 | - Default: generate a sequential id, keyed on page refresh. This option is not suitable 205 | for sortable lists or for pages that are composed logically: 206 | 207 | ```javascript 208 | m('greet', 'Bob') // component identity is greet1 209 | ``` 210 | 211 | ### Creating element singletons 212 | Mithril.Elements are designed to be composed in-line with the current view life-cycle. Nevertheless, there are situations where it can be useful to create an instance outside of the view life-cycle and feed the instance into the view directly: 213 | 214 | ```javascript 215 | { 216 | controller: function(){ 217 | var page1 = page.instance('one') 218 | var page2 = page.instance('two') 219 | }, 220 | view: function(){ 221 | return m('tabset', {}, 222 | function(){ return [ 223 | m('tab', ['Page 1', m(page1)]), 224 | m('tab', ['Page 2', m(page2)]) 225 | ]} 226 | ) 227 | } 228 | } 229 | ``` 230 | 231 | Note that this pattern effectivey emulates the standard Mithril component pattern, with the semantic difference being that the singleton instance can be composed directly and interchangebly with other element types. 232 | 233 | Element singletons are also a useful pattern to use when you need to expose an extended API: 234 | 235 | ```javascript 236 | var launcherFactory = m.element('unicornLauncher', { 237 | controller: function(){ 238 | // borrowed from https://docs.angularjs.org/guide/providers 239 | var useTinfoilShielding=false 240 | this.useTinfoilShielding = function(value) { 241 | useTinfoilShielding = !!value; 242 | } 243 | this.launch = function(useTinfoilShielding){...} 244 | }, 245 | view: function(){ 246 | return m('button', {onclick:ctrl.launch}) 247 | } 248 | } 249 | // tally ho!! 250 | launcherFactory.instance().useTinfoilShielding(true); 251 | ``` 252 | 253 | ### Escaping element tag names 254 | Element tag names can be escaped by preceeding them with the **$** sign to prevent them from being compiled into components. There are two situations where this can be useful: 255 | 256 | Using custom elements as templates in a parent-child relationship: 257 | ```javascript 258 | m('#todoapp',[ 259 | m('header',[ 260 | m('new-task') 261 | ]), 262 | m('list-of-tasks', [ 263 | m('$task') // use task as a template 264 | ]), 265 | m('footer') 266 | ]) 267 | ``` 268 | 269 | Preventing recursion when overriding native elements: 270 | ```javascript 271 | view:function(ctrl){ 272 | return m('$table',{style:{ // escape table to prevent recursion 273 | display:'block', 274 | overflow:'scroll', 275 | height:ctrl.height 276 | }, 277 | config:ctrl.setup}) 278 | } 279 | ``` 280 | 281 | ## Composability 282 | Mithril.Elements supports two composability patterns: lexical and parent-child. 283 | 284 | Lexical composability (the standard mithril pattern) means that sibling elements are compiled in order of definition, and child elements are compiled before parents: 285 | ```javascript 286 | m('.main', [ // order of compilation --> 287 | m('sib-1'), // sib-1 : : 288 | m('sib-2',[ // : : sib-2 289 | m('child-1') // : child-1 : 290 | ]) 291 | ]) 292 | ``` 293 | Normally this does not matter because the elements are [orthogonal] and they all end up being compiled before the DOM build phase. However, when creating higher order custom elements, compilation order becomes an issue for parent-child relationships. 294 | 295 | Parent-child composibility uses the factory pattern to invert the compilation order so that the child is compiled in the context of the parent: 296 | ```javascript 297 | m('table', [ // : : : : table 298 | m('tbody', function(content){return [ // tbody : : : : 299 | m('td', content.name), // : td : : : 300 | m('td', content.posts), // : : td : : 301 | m('td', content.lastTopic) // : : : td : 302 | ]} 303 | ]) 304 | ``` 305 | In the parent-child pattern, the parent component is responsible for compiling the child. Given this pattern, the parent has the opportunity to pass context into the child: 306 | ```javascript 307 | view: function(ctrl,child) { 308 | return ctrl.data.map(function(rowData){ 309 | return child(rowData) 310 | } 311 | } 312 | ``` 313 | 314 | ## Mithril API extensions 315 | 316 | ### m.element 317 | Use the m.element API to register mithril components as custom element types: 318 | ```javascript 319 | m.element('accordion', { 320 | controller: function() { 321 | this.toggle = function(id){ 322 | this.open=id; 323 | } 324 | }, 325 | view: function(ctrl, content) { 326 | display = function(id) { 327 | return 'display:'+(ctrl.open===id? 'block':'none') 328 | } 329 | return m('.accordion', content.map(function(line,id){ 330 | return m(line,{ 331 | onclick:ctrl.toggle.bind(ctrl,id) 332 | },[ 333 | line.children[0], 334 | m('div',{style:display(id)},line.children[1]) 335 | ]) 336 | })) 337 | } 338 | }) 339 | ``` 340 | 341 | The Mithril component signature has been modified for semantic components in the following ways: 342 | 343 | - **Controller** - the controller accepts an optional state argument. The state can be any valid JavaScript type and will be passed on to the controller constructor function at the start of the current page life-cycle. 344 | 345 | - **View** - the view accepts an optional inner argument. The inner argument can be one of: 346 | - Functor - a function callback that is used to provide context to complex element compositions. 347 | - Template - a virtual DOM element that will provide the component element structure. 348 | - Content - an array of virtual Elements that form the children of the component. 349 | 350 | - **Instance** - an additional component method that can be used to programatically create a component instance. The method returns a new element Controller instance that can be used inline in view composition. 351 | 352 | Signature: 353 | 354 | ```clike 355 | Module element(string elementName, Module module) 356 | 357 | where: 358 | Module :: Object { Controller, View, Instance } 359 | Controller :: void controller([State state]) 360 | { prototype: void unload(UnloadEvent e) } 361 | State :: Object | Array | Literal | undefined 362 | View :: void view(Object controllerInstance [, Inner inner]) 363 | Inner :: Functor | VirtualElement | Array | undefined 364 | UnloadEvent :: Object {void preventDefault()} 365 | Instance :: Controller instance(State state) 366 | 367 | ``` 368 | 369 | ### m 370 | A thin wrapper around the mithril [m()] signature function that lets Mithril.Elements intercept semantically registered element tags and integrate components bound to these tags with the current Mithril page life-cycle. 371 | 372 | The signature has been modified in the following ways: 373 | 374 | - **tag** - the tag argument can be any HTML5 tag name or a semantically registered component name, or a pre-compiled component instance: 375 | 376 | ```javascript 377 | m('greet', 'Bob') // hi Bob! 378 | 379 | var greeter = greet.instance('hola') 380 | m(greeter, 'Bob') // hola Bob! 381 | ``` 382 | 383 | Mithril.Elements will use the tag name to look up registered components. If a component has been registered under a tag name, one of two behaviours will occur depending on the current redraw strategy: 384 | 385 | - **all** - creates element state via a *new* controller instance. Optional initial state will be passed on to the controller constructor function. 386 | 387 | - **diff** - uses the state of the *current* controller instance. Therefore does not pass state. 388 | 389 | In both cases, the component view will be called on every redraw, but only after the controller has been invoked. 390 | 391 | - **attrs** - The attrs argument accepts a special property named **state**. The value of the state property will be passed unchanged to the controller constructor: 392 | 393 | ```javascript 394 | m('tbody', {state:{rows:12, content:hugeArray}}, function(content){return [ 395 | m('td', content.name), 396 | m('td', content.posts), 397 | m('td', content.lastTopic) 398 | ]}) 399 | ``` 400 | 401 | - **children** - the children argument can be overloaded with a functor that provides a context for composing complex elements. 402 | 403 | ```javascript 404 | m('shopping-cart-item',function(ctx){ return 405 | m('item', ctx.name), 406 | m('price', ctx.price), 407 | m('qnty', ctx.quantity]) 408 | ]}) 409 | ``` 410 | 411 | Signature: 412 | 413 | ```clike 414 | VirtualElement m(String selector [, Attributes attributes] [, Children... children]) 415 | 416 | where: 417 | VirtualElement :: Object { String tag, Attributes attributes, Children children } 418 | Attributes :: Object 419 | Children :: String text | VirtualElement virtualElement | SubtreeDirective directive | Functor | Array 420 | SubtreeDirective :: Object { String subtree } 421 | Functor :: Function definition 422 | ``` 423 | 424 | ## and finally 425 | A special thanks to: 426 | 427 | - Konstantin Tarkus - This starter kit owes a lot to [React Starter Kit](https://github.com/kriasoft/react-starter-kit) 428 | - Sean Adkinson - [npm-debug / node-inspector integration](http://stackoverflow.com/a/26415442/2708419) 429 | - Barney Carroll - [Ideas and encouragement for this project](https://groups.google.com/forum/#!topic/mithriljs/kt3JburQb1o) 430 | 431 | ### Copyright 432 | 433 | Source code is licensed under the MIT License (MIT). See [LICENSE.txt](./LICENSE.txt) 434 | file in the project root. Documentation to the project is licensed under the 435 | [CC BY 4.0](http://creativecommons.org/licenses/by/4.0/) license. 436 | 437 | 438 | [Mithril]: http://lhorie.github.io/mithril/index.html 439 | [Mithril.Elements]:https://github.com/philtoms/mithril.elements 440 | [m()]:http://lhorie.github.io/mithril/mithril.html 441 | [components]: http://lhorie.github.io/mithril/components.html 442 | [redraw strategy]:http://lhorie.github.io/mithril/mithril.redraw.html#strategy 443 | [m.module]:http://lhorie.github.io/mithril/mithril.module.html 444 | [referential integrity]:http://lhorie.github.io/mithril/mithril.html#dealing-with-sorting-and-deleting-in-lists 445 | [semantic]: http://html5doctor.com/lets-talk-about-semantics/ 446 | [orthogonal]:http://stackoverflow.com/questions/1527393/what-is-orthogonality 447 | [custom elements]: http://w3c.github.io/webcomponents/spec/custom/ 448 | [idiomatically scripted]: http://lhorie.github.io/mithril/getting-started.html 449 | [clone]: github-windows://openRepo/https://github.com/philtoms/mithril-starter-kit 450 | [fork]: https://github.com/philtoms/mithril-starter-kit/fork 451 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mithril-starter-kit", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "todomvc-common": "~0.1.4" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /config/jshintReporter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JSHint-Loader reporter function 3 | * 4 | * @param {Array} data Array of JSHint error objects. 5 | */ 6 | var chalk = require('chalk'); 7 | 8 | module.exports = function (errors) { 9 | 10 | var emitErrors = this.options.jshint.emitErrors; 11 | 12 | var hints = []; 13 | if(errors) errors.forEach(function(error) { 14 | if(!error) return; 15 | var message = chalk.gray(' line ') + chalk.blue(error.line) + chalk.gray(' char ') + chalk.blue(error.character) + ': ' + chalk.red(error.reason) + "\n " + chalk.gray(error.evidence); 16 | hints.push(message); 17 | }, this); 18 | var message = hints.join("\n\n"); 19 | var emitter = emitErrors ? this.emitError : this.emitWarning; 20 | if(emitter) 21 | emitter("jshint results in errors\n" + message); 22 | else 23 | throw new Error("Your module system doesn't support emitWarning. Update availible? \n" + message); 24 | }; -------------------------------------------------------------------------------- /config/webpack.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Mithril.Elements Starter Kit | https://github.com/philtoms/mithril-starter-kit 3 | * Copyright (c) Phil Toms, LLC. All rights reserved. See LICENSE.txt 4 | */ 5 | 6 | 'use strict'; 7 | 8 | var webpack = require('webpack'); 9 | 10 | /** 11 | * Get configuration for Webpack 12 | * 13 | * @see http://webpack.github.io/docs/configuration 14 | * https://github.com/petehunt/webpack-howto 15 | * 16 | * @param {boolean} release True if configuration is intended to be used in 17 | * a release mode, false otherwise 18 | * @return {object} Webpack configuration 19 | */ 20 | module.exports = function(release,watch) { 21 | return { 22 | cache: !release, 23 | debug: !release, 24 | devtool: 'eval', 25 | watch:watch, 26 | 27 | output: { 28 | filename: "bundle.js" 29 | }, 30 | 31 | stats: { 32 | colors: true, 33 | reasons: !release 34 | }, 35 | 36 | plugins: release ? [ 37 | new webpack.DefinePlugin({'process.env.NODE_ENV': '"production"'}), 38 | new webpack.optimize.DedupePlugin(), 39 | new webpack.optimize.UglifyJsPlugin(), 40 | new webpack.optimize.OccurenceOrderPlugin(), 41 | new webpack.optimize.AggressiveMergingPlugin() 42 | ] : [], 43 | 44 | resolve: { 45 | modulesDirectories: [ 46 | 'node_modules', 47 | 'bower_components' 48 | ], 49 | // alias: { 50 | // "mithril": "../../node_modules/mithril/mithril.js", 51 | // "mithril.elements": "../node_modules/mithril.elements/mithril.elements.js" 52 | // }, 53 | extensions: ['', '.webpack.js', '.web.js', '.js', '.msx'] 54 | }, 55 | 56 | module: { 57 | preLoaders: [ 58 | { 59 | test: /\.js$/, 60 | exclude: /node_modules/, 61 | loader: 'jshint' 62 | } 63 | ], 64 | 65 | loaders: [ 66 | { 67 | test: /\.msx$/, 68 | loader: 'sweetjs?modules[]=msx-reader/macros/msx-macro,readers[]=msx-reader' 69 | }, 70 | { 71 | test: /\.css$/, 72 | loader: 'style!css' 73 | }, 74 | { 75 | test: /\.less$/, 76 | loader: 'style!css!less' 77 | }, 78 | { 79 | test: /\.gif/, 80 | loader: 'url-loader?limit=10000&mimetype=image/gif' 81 | }, 82 | { 83 | test: /\.jpg/, 84 | loader: 'url-loader?limit=10000&mimetype=image/jpg' 85 | }, 86 | { 87 | test: /\.png/, 88 | loader: 'url-loader?limit=10000&mimetype=image/png' 89 | } 90 | ] 91 | }, 92 | 93 | // more options in the optional jshint object 94 | // see: http://jshint.com/docs/ for more details 95 | jshint: { 96 | // any jshint option http://www.jshint.com/docs/options/ 97 | // i. e. 98 | camelcase: true, 99 | 100 | // any globals that should be suppressed 101 | globals: ['m'], 102 | 103 | // jshint errors are displayed by default as warnings 104 | // set emitErrors to true to display them as errors 105 | emitErrors: false, 106 | 107 | // jshint to not interrupt the compilation 108 | // if you want any file with jshint errors to fail 109 | // set failOnHint to true 110 | failOnHint: true, 111 | 112 | // custom reporter function 113 | reporter: require('./jshintReporter.js') 114 | } 115 | }; 116 | }; 117 | 118 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | FAQ 2 | === 3 | 4 | ... -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Mithril.js Starter Kit 3 | * Copyright (c) 2014 Phil Toms (@philtoms) 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE.txt file in the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | // Include Gulp and other build automation tools and utilities 12 | // See: https://github.com/gulpjs/gulp/blob/master/docs/API.md 13 | 14 | var gulp = require('gulp'); 15 | var $ = require('gulp-load-plugins')(); 16 | var del = require('del'); 17 | var path = require('path'); 18 | var merge = require('merge-stream'); 19 | var runSequence = require('run-sequence'); 20 | var browserSync = require('browser-sync'); 21 | var pagespeed = require('psi'); 22 | var extend = require('extend'); 23 | var fs = require('fs'); 24 | var url = require('url'); 25 | var argv = require('minimist')(process.argv.slice(2)); 26 | 27 | // Settings 28 | var DEST = './build'; // The build output folder 29 | var RELEASE = !!argv.release; // Minimize and optimize during a build? 30 | var GOOGLE_ANALYTICS_ID = 'UA-XXXXX-X'; // https://www.google.com/analytics/web/ 31 | var AUTOPREFIXER_BROWSERS = [ // https://github.com/ai/autoprefixer 32 | 'ie >= 10', 33 | 'ie_mob >= 10', 34 | 'ff >= 30', 35 | 'chrome >= 34', 36 | 'safari >= 7', 37 | 'opera >= 23', 38 | 'ios >= 7', 39 | 'android >= 4.4', 40 | 'bb >= 10' 41 | ]; 42 | 43 | var src = {}; 44 | var watch = false; 45 | var pkgs = (function() { 46 | var pkgs = {}; 47 | var map = function(source) { 48 | for (var key in source) { 49 | pkgs[key.replace(/[^a-z0-9]/gi, '')] = source[key].substring(1); 50 | } 51 | }; 52 | map(require('./package.json').dependencies); 53 | return pkgs; 54 | }()); 55 | 56 | // The default task 57 | gulp.task('default', ['serve']); 58 | 59 | // Clean up 60 | gulp.task('clean', del.bind(null, [DEST])); 61 | 62 | // 3rd party libraries 63 | gulp.task('vendor', function() { 64 | src.vendor = [ 65 | 'bower_components/todomvc-common/base.{js,css}', 66 | 'bower_components/todomvc-common/bg.png' 67 | ]; 68 | return merge( 69 | gulp.src(src.vendor) 70 | .pipe(gulp.dest(DEST + '/vendor')), 71 | gulp.src('./node_modules/bootstrap/dist/fonts/**') 72 | .pipe(gulp.dest(DEST + '/fonts')) 73 | ); 74 | }); 75 | 76 | // Static files 77 | gulp.task('assets', function() { 78 | src.assets = [ 79 | 'src/assets/**', 80 | 'src/pages/index.html' 81 | ]; 82 | return gulp.src(src.assets) 83 | .pipe($.changed(DEST)) 84 | .pipe(gulp.dest(DEST)) 85 | .pipe($.size({title: 'assets'})); 86 | }); 87 | 88 | // Images 89 | gulp.task('images', function() { 90 | src.images = 'src/images/**'; 91 | return gulp.src(src.images) 92 | .pipe($.changed(DEST + '/images')) 93 | .pipe($.imagemin({ 94 | progressive: true, 95 | interlaced: true 96 | })) 97 | .pipe(gulp.dest(DEST + '/images')) 98 | .pipe($.size({title: 'images'})); 99 | }); 100 | 101 | // HTML pages 102 | gulp.task('pages', function() { 103 | src.pages = ['src/pages/**/*.js', 'src/pages/index.html', 'src/pages/404.html']; 104 | 105 | return gulp.src(src.pages) 106 | .pipe($.changed(DEST, {extension: '.html'})) 107 | .pipe($.replace('UA-XXXXX-X', GOOGLE_ANALYTICS_ID)) 108 | .pipe($.if(!RELEASE, $.replace('.min.js', '.js'))) 109 | .pipe($.if(RELEASE, $.htmlmin({ 110 | removeComments: true, 111 | collapseWhitespace: true, 112 | minifyJS: true 113 | }), $.jsbeautifier())) 114 | .pipe(gulp.dest(DEST)) 115 | .pipe($.size({title: 'pages'})); 116 | }); 117 | 118 | // CSS style sheets 119 | gulp.task('styles', function() { 120 | src.styles = 'src/styles/**/*.{css,less}'; 121 | return gulp.src('src/styles/app.less') 122 | .pipe($.plumber()) 123 | .pipe($.less({ 124 | sourceMap: !RELEASE, 125 | sourceMapBasepath: __dirname 126 | })) 127 | .on('error', console.error.bind(console)) 128 | .pipe($.autoprefixer({browsers: AUTOPREFIXER_BROWSERS})) 129 | .pipe($.if(RELEASE, $.minifyCss())) 130 | .pipe(gulp.dest(DEST + '/css')) 131 | .pipe($.size({title: 'styles'})); 132 | }); 133 | 134 | // Bundle 135 | gulp.task('bundle', function(cb) { 136 | var options = require('./config/webpack.js')(RELEASE,watch); 137 | gulp.src('./src/app.js') 138 | .pipe($.webpack(options)) 139 | .pipe(gulp.dest('./build/')); 140 | cb(null); 141 | }); 142 | 143 | // Build the app from source code 144 | gulp.task('build', ['clean'], function(cb) { 145 | runSequence(['vendor', 'assets', 'images', 'styles', 'bundle'], cb); 146 | }); 147 | 148 | // Launch a lightweight HTTP Server 149 | gulp.task('serve', function(cb) { 150 | 151 | watch = true; 152 | 153 | runSequence('build', function() { 154 | browserSync({ 155 | notify: false, 156 | // Customize the BrowserSync console logging prefix 157 | logPrefix: 'MSK', 158 | // Run as an https by uncommenting 'https: true' 159 | // Note: this uses an unsigned certificate which on first access 160 | // will present a certificate warning in the browser. 161 | // https: true, 162 | server: { 163 | baseDir: DEST, 164 | // Allow web page requests without .html file extension in URLs 165 | middleware: function(req, res, cb) { 166 | var uri = url.parse(req.url); 167 | if (uri.pathname.length > 1 && 168 | path.extname(uri.pathname) === '' && 169 | fs.existsSync(DEST + uri.pathname + '.html')) { 170 | req.url = uri.pathname + '.html' + (uri.search || ''); 171 | } 172 | cb(); 173 | } 174 | } 175 | }); 176 | 177 | gulp.watch(src.vendor, ['vendor']); 178 | gulp.watch(src.assets, ['assets']); 179 | gulp.watch(src.images, ['images']); 180 | gulp.watch(src.pages, ['pages']); 181 | gulp.watch(src.styles, ['styles']); 182 | gulp.watch(DEST + '/**/*.*', function(file) { 183 | browserSync.reload(path.relative(__dirname, file.path)); 184 | }); 185 | cb(); 186 | }); 187 | }); 188 | 189 | // run jest tests 190 | // gulp.task('jest', function () { 191 | // return gulp.src('src/**/__tests__').pipe($.jest({ 192 | // unmockedModulePathPatterns: [ 193 | // ], 194 | // testDirectoryName: "src", 195 | // testPathIgnorePatterns: [ 196 | // "node_modules", 197 | // "spec/support" 198 | // ], 199 | // moduleFileExtensions: [ 200 | // "js", 201 | // "json", 202 | // "msx" 203 | // ] 204 | // })); 205 | // }); 206 | 207 | var jest = require('jest-cli'); 208 | var chalk = require('chalk'); 209 | gulp.task('jest', function (callback) { 210 | var onComplete = function (result) { 211 | // if (result) { 212 | // } else { 213 | // console.log(chalk.bgYellow('!!! Jest tests failed! You should fix them soon. !!!')); 214 | // } 215 | callback(); 216 | } 217 | jest.runCLI({}, __dirname, onComplete); 218 | }); 219 | 220 | gulp.task('tdd', function () { 221 | gulp.watch('src/**/*.js', ['jest']); 222 | }); 223 | 224 | gulp.task('bdd', function () { 225 | gulp.watch('src/**/*.js', ['jest']); 226 | }); 227 | 228 | // Deploy to GitHub Pages 229 | gulp.task('deploy', function() { 230 | 231 | // Remove temp folder 232 | if (argv.clean) { 233 | var os = require('os'); 234 | var path = require('path'); 235 | var repoPath = path.join(os.tmpdir(), 'tmpRepo'); 236 | $.util.log('Delete ' + $.util.colors.magenta(repoPath)); 237 | del.sync(repoPath, {force: true}); 238 | } 239 | 240 | return gulp.src(DEST + '/**/*') 241 | .pipe($.if('**/robots.txt', !argv.production ? $.replace('Disallow:', 'Disallow: /') : $.util.noop())) 242 | .pipe($.ghPages({ 243 | remoteUrl: 'https://github.com/{name}/{name}.github.io.git', 244 | branch: 'master' 245 | })); 246 | }); 247 | 248 | // Run PageSpeed Insights 249 | // Update `url` below to the public URL for your site 250 | gulp.task('pagespeed', pagespeed.bind(null, { 251 | // By default, we use the PageSpeed Insights 252 | // free (no API key) tier. You can use a Google 253 | // Developer API key if you have one. See 254 | // http://goo.gl/RkN0vE for info key: 'YOUR_API_KEY' 255 | url: 'https://example.com', 256 | strategy: 'mobile' 257 | })); 258 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mithril-starter-kit", 3 | "private": true, 4 | "version": "0.1.0", 5 | "description": "Mithril Starter Kit", 6 | "repository": "https://github.com/philtoms/mithril-starter-kit", 7 | "license": "MIT", 8 | "dependencies": { 9 | "bootstrap": "^3.3.1", 10 | "mithril": "^0.1.28", 11 | "mithril.elements": "^0.1.1", 12 | "object-assign": "^1.0.0" 13 | }, 14 | "devDependencies": { 15 | "browser-sync": "^1.6.5", 16 | "del": "^0.1.3", 17 | "gulp": "^3.8.10", 18 | "gulp-autoprefixer": "^1.0.1", 19 | "gulp-cache": "^0.2.4", 20 | "gulp-changed": "^1.0.0", 21 | "gulp-csscomb": "^3.0.3", 22 | "gulp-gh-pages": "^0.4.0", 23 | "gulp-htmlmin": "^0.2.0", 24 | "gulp-if": "^1.2.5", 25 | "gulp-imagemin": "^1.2.1", 26 | "gulp-jsbeautifier": "^0.0.3", 27 | "gulp-jshint": "^1.9.0", 28 | "gulp-less": "^1.3.6", 29 | "gulp-load-plugins": "^0.7.1", 30 | "gulp-minify-css": "^0.3.11", 31 | "gulp-plumber": "^0.6.6", 32 | "gulp-render": "^0.2.0", 33 | "gulp-replace": "^0.5.0", 34 | "gulp-size": "^1.1.0", 35 | "gulp-uglify": "^1.0.1", 36 | "gulp-util": "^3.0.1", 37 | "jest-cli": "~0.2.0", 38 | "jshint": "^2.5.10", 39 | "jshint-loader": "^0.8.0", 40 | "jshint-stylish": "^1.0.0", 41 | "jsx-loader": "^0.12.1", 42 | "merge-stream": "^0.1.6", 43 | "minimist": "^1.1.0", 44 | "protractor": "^1.4.0", 45 | "psi": "^0.1.5", 46 | "run-sequence": "^1.0.1", 47 | "url-loader": "^0.5.5", 48 | "webpack": "^1.4.13", 49 | "webpack-dev-server": "^1.6.5", 50 | "sweetjs-loader": "~0.1.0", 51 | "sweet.js": "~0.7.2", 52 | "msx-reader": "git+https://github.com/sdemjanenko/msx-reader.git", 53 | "gulp-webpack": "~1.1.0", 54 | "colors": "~1.0.3", 55 | "chalk": "~0.5.1", 56 | "extend": "~2.0.0", 57 | "css-loader": "~0.9.0", 58 | "less": "~1.7.5", 59 | "less-loader": "~0.7.8", 60 | "style-loader": "~0.8.2", 61 | "gulp-jest": "~0.2.2" 62 | }, 63 | "jest": { 64 | "rootDir": "src", 65 | "unmockedModulePathPatterns": [] 66 | }, 67 | "scripts": { 68 | "start": "gulp", 69 | "test": "jest", 70 | "test-debug": "node-debug --nodejs --harmony ./node_modules/jest-cli/bin/jest.js" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Mithril.Elements Starter Kit 3 | * Copyright (c) 2014 Phil Toms (@PhilToms3). 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE.txt file in the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | // global mithril.elements (alternatively, local require in each module) 12 | window.m = require('mithril.elements'); 13 | 14 | // experimental - will probably be npm'd in next version 15 | require('./components/mithril.bootstrap'); 16 | 17 | // tab routes 18 | var ACCORDION1 = 0; 19 | var ACCORDION2 = 1; 20 | var MODAL = 2; 21 | var TODOS = 3; 22 | var XP = 4; 23 | 24 | var app = function(tabNumber){ 25 | return { 26 | controller: function() { 27 | // initialize the pages as singletons 28 | this.todos = require('./components/todos').instance(); 29 | this.accordion1 = require('./components/demos/accordion').instance(); 30 | this.accordion2 = require('./components/demos/accordion').instance({toggle:true}); 31 | this.modal = require('./components/demos/modal').instance(); 32 | this.experimental= require('./components/experimental/todosX').instance(); 33 | }, 34 | 35 | view: function(ctrl) { 36 | return [ 37 | m('jumbotron',[ 38 | m('h1','Mithril Starter Kit'), 39 | m('h3','with Mithril.Elements v0.1.1') 40 | ]), 41 | m('h2.text-center', 'Click on any of the tab pills below'), 42 | m('h4.text-center','to reveal some custom elements in action'), 43 | m('tabset', {state:{active:tabNumber, style:'pills'}}, 44 | // provide routng to the tabs to engage route history 45 | function(){ return [ 46 | m('tab', {state:{href:'/accordion-1'}}, ['Accordion 1', m(ctrl.accordion1)]), 47 | m('tab', {state:{href:'/accordion-2'}}, ['Accordion 2', m(ctrl.accordion2)]), 48 | m('tab', {state:{href:'/modal'}}, ['Modal dialog', m(ctrl.modal)]), 49 | m('tab', {state:{href:'/todos'}}, ['Todo List', m(ctrl.todos)]), 50 | m('tab', {state:{href:'/todos-xp'}}, ['Experimental', m(ctrl.experimental)]) 51 | ]; 52 | }) 53 | ]; 54 | } 55 | }; 56 | }; 57 | 58 | m.route(document.getElementById('app'), '/', { 59 | '/': app(), 60 | '/accordion-1': app(ACCORDION1), 61 | '/accordion-2': app(ACCORDION2), 62 | '/modal': app(MODAL), 63 | '/todos': app(TODOS), 64 | '/todos/:filter': app(TODOS), 65 | '/todos-xp': app(XP), 66 | '/todos-xp/:filter': app(XP) 67 | }); 68 | -------------------------------------------------------------------------------- /src/assets/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philtoms/mithril-starter-kit/1dfab67887e2bac74b3202afa1e475b4342cf9bf/src/assets/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /src/assets/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philtoms/mithril-starter-kit/1dfab67887e2bac74b3202afa1e475b4342cf9bf/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/humans.txt: -------------------------------------------------------------------------------- 1 | # humanstxt.org/ 2 | # The humans responsible & technology colophon 3 | 4 | # TEAM 5 | 6 | -- -- 7 | 8 | # THANKS 9 | 10 | 11 | 12 | # TECHNOLOGY COLOPHON 13 | 14 | HTML5, CSS3, JavaScript 15 | Mithril, Bootstrap 16 | -------------------------------------------------------------------------------- /src/assets/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | 3 | # Allow crawling of all content 4 | User-agent: * 5 | Disallow: 6 | -------------------------------------------------------------------------------- /src/assets/tile-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philtoms/mithril-starter-kit/1dfab67887e2bac74b3202afa1e475b4342cf9bf/src/assets/tile-wide.png -------------------------------------------------------------------------------- /src/assets/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philtoms/mithril-starter-kit/1dfab67887e2bac74b3202afa1e475b4342cf9bf/src/assets/tile.png -------------------------------------------------------------------------------- /src/components/demos/accordion.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = m.element('accordion-demo', { 4 | controller: function(options) { 5 | this.options=options||{}; 6 | }, 7 | view: function(ctrl, content) { 8 | return [ 9 | m('h3', ctrl.options.toggle? 'Accordion with toggle state':'Single item accordion'), 10 | m('accordion', {state:ctrl.options}, [ 11 | m('.item', ['Title 1','item line one']), 12 | m('.item', ['Title 2','item line two']), 13 | m('.item', ['Title 3','item line three']), 14 | m('.item', ['Title 4','item line four']) 15 | ]) 16 | ]; 17 | } 18 | }); -------------------------------------------------------------------------------- /src/components/demos/modal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = m.element('modal-demo', { 4 | controller: function() { 5 | // provide a boolean trigger for the dialog to 6 | // read open / closed state 7 | this.trigger = m.prop(false); 8 | 9 | this.save = function(){ 10 | setTimeout(function(){window.alert('saved');},100); 11 | }; 12 | }, 13 | view: function(ctrl, content) { 14 | return [ 15 | m('button.btn.btn-primary.btn-lg[type="button"]', {onclick:ctrl.trigger.bind(ctrl,true)}, 'Launch demo modal'), 16 | m('modal', {state:{trigger:ctrl.trigger}}, function(){ return { 17 | title:'A Modal Title', 18 | body: ['Another fine example...', 19 | m('p',' of a work in progress') 20 | ], 21 | cancel: 'Cancel', 22 | ok: m('.save', {onclick:ctrl.save.bind(ctrl)}, 'Save Changes') 23 | }; 24 | })]; 25 | } 26 | }); -------------------------------------------------------------------------------- /src/components/experimental/occlusionScroller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = m.element('occlusionScroller',{ 4 | 5 | // Component controllers are instanced with optional data. 6 | // This data is part of the interface definition that users 7 | // of a component are expected to comply with. In this case 8 | // the data must contain: 9 | // items: function returning a set of scrollable items 10 | // page: the number of line to display on a page 11 | controller: function(model) { 12 | 13 | this.items = model.items; 14 | this.page = model.page; 15 | this.itemHeight=58; 16 | var scroller={scrollTop:0}; 17 | 18 | this.setup = function(element,done){ 19 | if (!done){ 20 | scroller = element; 21 | element.addEventListener('scroll', function(e) { 22 | m.redraw(); //notify view 23 | }); 24 | } 25 | }; 26 | 27 | this.pageY = function(){ 28 | return scroller.scrollTop; 29 | }; 30 | 31 | }, 32 | 33 | // Component views accept a controller and an optional inner argument. 34 | // 35 | // 36 | view: function(ctrl, template) { 37 | 38 | // fetch the item list into local scope. Typically this 39 | // controller method will be bound to the component controller 40 | // throuth the model interface and will return a reference to an 41 | // external list. 42 | var items = typeof ctrl.items === 'function'? ctrl.items():ctrl.items; 43 | 44 | // calculate the begin and end indicies of the scrollable section 45 | var begin = ctrl.pageY() / ctrl.itemHeight | 0; 46 | 47 | // Add 2 so that the top and bottom of the page are filled with 48 | // next/prev item, not just whitespace if item not in full view 49 | var end = begin + ctrl.page + 2; 50 | 51 | var offset = ctrl.pageY % ctrl.itemHeight; 52 | var height = Math.min(items.length,ctrl.page) * ctrl.itemHeight + 'px'; 53 | 54 | // add our own identity and style to the element. Note that any values 55 | // created here may be overridden by the component instance 56 | return m('.occlusionScroller', {style:{overflow:'scroll', height: height},config:ctrl.setup}, [ 57 | 58 | m('.list', {style: {height: items.length * ctrl.itemHeight + 'px', position: 'relative', top: -offset + 'px'}}, [ 59 | m('ul', {style: {paddingTop: ctrl.pageY() + 'px'}}, [ 60 | 61 | // merge the page content into the flow with a standard map 62 | items.slice(begin, end).map(function(item, idx) { 63 | 64 | // register the child template. Notice that we 65 | // are passing it as an object and not as a string tagname. 66 | // Mithril.Element can distinguish between compiled components 67 | // and precompiled cells 68 | // 69 | return m(template, {id:idx+begin,state:item}); 70 | }) 71 | 72 | ]) 73 | ]) 74 | ]); 75 | } 76 | }); 77 | -------------------------------------------------------------------------------- /src/components/experimental/todosX/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ENTER_KEY = 13; 4 | var ESC_KEY = 27; 5 | 6 | module.exports = { 7 | todoCount: 0, 8 | watchInput: function watchInput(ontype, onenter, onescape) { 9 | return function(e) { 10 | ontype(e); 11 | if (e.keyCode === ENTER_KEY) onenter(); 12 | if (e.keyCode === ESC_KEY) onescape(); 13 | }; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/experimental/todosX/footer.msx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = require('./app'); 4 | 5 | module.exports = function(ctrl) { 6 | 7 | var completed = ctrl.amountCompleted(); 8 | 9 | return <$footer id="footer"> 10 | {app.todoCount} item{app.todoCount > 1 ? 's ' : ' '}left 11 | 31 | {completed == 0 ? '' : } 34 | 35 | } -------------------------------------------------------------------------------- /src/components/experimental/todosX/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // todo modules 4 | var app = require('./app'); 5 | var model = require('./model'); 6 | var item = require('./item'); 7 | var footer = require('./footer'); 8 | 9 | // mithril.elements 10 | require('../occlusionScroller'); 11 | 12 | module.exports = m.element('todosX-demo',{ 13 | 14 | controller: function() { 15 | 16 | this.title = m.prop(''); // Temp title placeholder 17 | this.filter = m.prop(m.route.param('filter') || ''); // TodoList filter 18 | this.completeAll = model.completeAll; 19 | this.allCompleted = model.allCompleted; 20 | this.clearCompleted = model.clearCompleted; 21 | 22 | // Add a Todo 23 | this.add = function(title) { 24 | if(this.title()) { 25 | model.add(title()); 26 | this.title(''); 27 | } 28 | }; 29 | 30 | //check whether a todo is visible 31 | this.isVisible = function(filter,todo) { 32 | if(filter === '') 33 | return true; 34 | if (filter === 'active') 35 | return !todo.completed(); 36 | if (filter === 'completed') 37 | return todo.completed(); 38 | }.bind(this,this.filter()); 39 | 40 | this.clearTitle = function() { 41 | this.title(''); 42 | }; 43 | 44 | // Total amount of Todos completed 45 | this.amountCompleted = function() { 46 | var amount = 0; 47 | for(var i = 0, len=list.length; i < len; i++) 48 | if(list[i].completed()) 49 | amount++; 50 | 51 | return amount; 52 | }; 53 | 54 | // Todo collection - lazily filtered 55 | var list = [], filtered=[]; 56 | app.todoCount = -1; 57 | this.list = function(){ 58 | list = model.TodoList(); 59 | if (app.todoCount !== list.length) { 60 | app.todoCount = list.length; 61 | filtered = list.filter(this.isVisible); 62 | } 63 | return filtered; 64 | }.bind(this); 65 | }, 66 | 67 | view: function(ctrl) { 68 | return m('section#todoapp',[ 69 | m('$header#header', [ 70 | m('h1', 'too many todos'), 71 | m('input#new-todo[placeholder="What needs to be done?"]', { 72 | onkeypress: app.watchInput( 73 | m.withAttr('value', ctrl.title), 74 | ctrl.add.bind(ctrl, ctrl.title), 75 | ctrl.clearTitle.bind(ctrl) 76 | ), 77 | value: ctrl.title() 78 | }) 79 | ]), 80 | m('section#main', [ 81 | m('input#toggle-all[type=checkbox]',{ 82 | onclick: ctrl.completeAll, 83 | checked: ctrl.allCompleted() 84 | }), 85 | m('occlusionScroller#todo-list', {state:{items:ctrl.list,page:6}},[ 86 | m('$todosX-item') 87 | ]) 88 | ]), 89 | app.todoCount === 0 ? '' : footer(ctrl) 90 | ]); 91 | } 92 | }); 93 | -------------------------------------------------------------------------------- /src/components/experimental/todosX/item.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = require('./app'); 4 | 5 | module.exports = m.element('todosX-item',{ 6 | 7 | controller: function(task) { 8 | 9 | var state = { 10 | editing: false, 11 | task: task, 12 | setClass: function() { 13 | var cls = '' + (task.completed() ? 'completed ' : '') + (state.editing ? 'editing': ''); 14 | return cls? { class: cls}:''; 15 | }, 16 | 17 | setEdit: function() { 18 | state.editing=task.title(); 19 | }, 20 | 21 | update: function() { 22 | state.editing=false; 23 | }, 24 | 25 | remove: function() { 26 | task.remove(); 27 | m.redraw.strategy('all'); 28 | }, 29 | 30 | reset: function() { 31 | task.title(state.editing); 32 | state.editing=false; 33 | } 34 | }; 35 | 36 | return state; 37 | }, 38 | 39 | view: function(ctrl) { 40 | var task = ctrl.task; 41 | return m('li', ctrl.setClass(), [ 42 | m('.view', [ 43 | m('input.toggle[type=checkbox]', { 44 | onclick: m.withAttr('checked', task.completed), 45 | checked: task.completed() 46 | }), 47 | m('label', {ondblclick:ctrl.setEdit}, task.title()), 48 | m('button.destroy', { onclick: ctrl.remove}) 49 | ]), 50 | m('input.edit', { 51 | value:task.title(), 52 | onkeyup: app.watchInput( 53 | m.withAttr('value', task.title), 54 | ctrl.update, 55 | ctrl.reset 56 | ), 57 | onblur: ctrl.update, 58 | config: function (element) { 59 | if (ctrl.editing){ 60 | element.focus(); 61 | } 62 | } 63 | }) 64 | ]); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /src/components/experimental/todosX/model.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var list = []; 4 | 5 | 6 | // Todo Model 7 | function Todo(data){ 8 | this.title = m.prop(data.title); 9 | this.completed = m.prop(false); 10 | this.remove = function(){ 11 | var _item=this; 12 | list = list.filter(function(item){ 13 | return (item!==_item); 14 | }); 15 | }; 16 | } 17 | 18 | var model = { 19 | 20 | // List of Todos 21 | TodoList: function () { 22 | return list; 23 | }, 24 | 25 | // Add a Todo 26 | add: function(title) { 27 | list.push(new Todo({title: title })); 28 | }, 29 | 30 | // Remove all Todos where Completed == true 31 | clearCompleted: function() { 32 | for(var i = 0; i < list.length; i++) { 33 | if(list[i].completed()) 34 | list.splice(i, 1); 35 | } 36 | }, 37 | 38 | completeAll: function () { 39 | var complete = model.allCompleted(); 40 | for (var i = 0; i < list.length; i++) { 41 | list[i].completed(!complete); 42 | } 43 | }, 44 | 45 | allCompleted: function () { 46 | for (var i = 0; i < list.length; i++) { 47 | if (!list[i].completed()) { 48 | return false; 49 | } 50 | } 51 | return true; 52 | } 53 | 54 | }; 55 | 56 | for (var i = 0; i < 50000; i++){ 57 | model.add('List item no ' + (i+1)); 58 | } 59 | 60 | module.exports = model; 61 | -------------------------------------------------------------------------------- /src/components/mithril.bootstrap/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var scrollbarWidth = (function () { 4 | var scrollbarWidth; 5 | return function(){ 6 | if (scrollbarWidth===undefined){ 7 | var scrollDiv = document.createElement('div'); 8 | scrollDiv.className = 'modal-scrollbar-measure'; 9 | document.body.appendChild(scrollDiv); 10 | scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth; 11 | document.body.removeChild(scrollDiv); 12 | } 13 | return scrollbarWidth; 14 | }; 15 | })(); 16 | 17 | var accordion = m.element('accordion', { 18 | controller: function(options) { 19 | options = options || {}; 20 | var open=[]; 21 | this.toggle = function(id){ 22 | if (options.toggle){ 23 | open[id]=!open[id]; 24 | } 25 | else { 26 | open = id; 27 | } 28 | }; 29 | this.isOpen = function(id){ 30 | return id === open || options.toggle && open[id]; 31 | }; 32 | }, 33 | view: function(ctrl, content) { 34 | return m('.accordian.panel.panel-default', content.map(function(line,id){ 35 | var title=line.children[0],content=line.children[1]; 36 | return [ 37 | m(line,{ 38 | class:'panel-heading', 39 | onclick:ctrl.toggle.bind(ctrl,id) 40 | }, 41 | m('.panel-title',title)), 42 | m('div.panel-body',{style:'display:'+(ctrl.isOpen(id)? 'block':'none')},content) 43 | ]; 44 | })); 45 | } 46 | }); 47 | 48 | 49 | var jumbotron = m.element('jumbotron', { 50 | 51 | view: function(ctrl,inner) { 52 | return m('.jumbotron',[ 53 | m('.container',[ 54 | inner 55 | ]) 56 | ]); 57 | } 58 | }); 59 | 60 | var modal = m.element('modal', { 61 | 62 | controller: function(options) { 63 | var open, backdrop, saveBodyClass=''; 64 | function close(e){ 65 | open = false; 66 | options.trigger(false); 67 | document.body.className=saveBodyClass; 68 | if (e) m.redraw(); 69 | } 70 | this.close = {onclick:function(){close();}}; 71 | this.state = options.trigger; 72 | this.bind = function(element){ 73 | if (!open && options.trigger()){ 74 | open=element; 75 | saveBodyClass = document.body.className; 76 | document.body.className += ' modal-open'; 77 | backdrop = element.getElementsByClassName('modal-backdrop')[0]; 78 | backdrop.setAttribute('style', 'height:'+document.documentElement.clientHeight+'px'); 79 | backdrop.addEventListener('click', close); 80 | } 81 | }; 82 | }, 83 | 84 | view: function(ctrl,inner) { 85 | inner = inner(); 86 | var isOpen = ctrl.state(); 87 | return m((isOpen? '.is-open':'.modal.fade'), {config:ctrl.bind}, [ 88 | (isOpen? m('.modal-backdrop.fade.in'):''), 89 | m('.modal-dialog', [ 90 | m('.modal-content', [ 91 | m('.modal-header', [ 92 | m('button.close[type="button" data-dismiss="modal" aria-label="Close"]', 93 | m('span[aria-hidden=true]', ctrl.close, m.trust('×'))), 94 | m('h4.modal-title', inner.title) 95 | ]), 96 | m('.modal-body', inner.body), 97 | m('.modal-footer', [ 98 | m('button.btn.btn-default[type="button" data-dismiss="modal"]', ctrl.close, inner.cancel || 'Close'), 99 | inner.ok? m('button.btn.btn-primary[type="button"]', ctrl.close, inner.ok):'' 100 | ]) 101 | ]) 102 | ]) 103 | ]); 104 | } 105 | }); 106 | 107 | // tabset based on bootstrap navs markup. 108 | // Options = 109 | // active: current (default) tab 110 | // style: 'tabs' | 'pills' 111 | 112 | var tabset = m.element('tabset', { 113 | 114 | controller: function(options){ 115 | 116 | var currentTab = options.active; 117 | var count = 0; 118 | var tabs = this.tabs = []; 119 | var content = this.content = []; 120 | 121 | this.style=options.style || 'tabs'; 122 | 123 | function Select(){ 124 | currentTab = this.tabIdx; 125 | } 126 | 127 | function active(tabIdx) { 128 | return tabIdx===currentTab? 'active':''; 129 | } 130 | 131 | function display(tabIdx) { 132 | return {display: (tabIdx===currentTab? 'block':'none')}; 133 | } 134 | 135 | m.element('tab', { 136 | controller: function(options){ 137 | this.tabIdx=count++; 138 | this.href=function(){ 139 | return options.href? {config: m.route,href:options.href}:{href:'#'}; 140 | }; 141 | }, 142 | 143 | view: function(ctrl,inner) { 144 | var tabName=inner[0], tabContent=inner[1]; 145 | tabs[ctrl.tabIdx] = m('li.tab', {onclick:Select.bind(ctrl),class:active(ctrl.tabIdx)}, m('a', ctrl.href(), tabName)); 146 | content[ctrl.tabIdx] = m('.tabcontent', {style:display(ctrl.tabIdx)}, tabContent); 147 | } 148 | }); 149 | 150 | }, 151 | 152 | view: function(ctrl,tabs) { 153 | // tabs needs to be a factory in order to compile 154 | // directly into ctrl (ie parent / child) context 155 | tabs(); 156 | return m('.tabset',[ 157 | m('ul.nav.nav-'+ctrl.style, ctrl.tabs), 158 | m('div',ctrl.content) 159 | ]); 160 | } 161 | 162 | }); 163 | 164 | module.exports = { 165 | accordion:accordion, 166 | jumbotron:jumbotron, 167 | modal:modal, 168 | tabset:tabset 169 | }; 170 | -------------------------------------------------------------------------------- /src/components/todos/__tests__/app-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Mithral Starter Kit 3 | * Copyright (c) 2014 Phil Toms (@PhilToms3). 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE.txt file in the root directory of this source tree. 7 | */ 8 | 9 | /* global jest, describe, it, expect */ 10 | 11 | 'use strict'; 12 | 13 | jest.dontMock('../app'); 14 | 15 | describe('app.watchInput', function() { 16 | 17 | var appstate = require('../app'); 18 | 19 | var onType; 20 | var onEnter; 21 | var onEscape; 22 | 23 | var watchInput; 24 | var ENTER_KEY = 13; 25 | var ESC_KEY = 27; 26 | 27 | beforeEach(function(){ 28 | onEnter = jasmine.createSpy('onEnter'); 29 | onEscape = jasmine.createSpy('onEscape'); 30 | 31 | watchInput = appstate.watchInput(onEnter,onEscape); 32 | 33 | }); 34 | 35 | it('calls event handler on CR', function() { 36 | watchInput({keyCode:ENTER_KEY}); 37 | expect(onEnter).toHaveBeenCalled(); 38 | }); 39 | 40 | it('calls escape handler on ESC', function() { 41 | watchInput({keyCode:ESC_KEY}); 42 | expect(onEscape).toHaveBeenCalled(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/components/todos/__tests__/model-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Mithral Starter Kit 3 | * Copyright (c) 2014 Phil Toms (@PhilToms3). 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE.txt file in the root directory of this source tree. 7 | */ 8 | 9 | /* global jest, describe, it, expect */ 10 | 11 | 'use strict'; 12 | 13 | jest.dontMock('../model'); 14 | 15 | describe('model.persistence', function() { 16 | 17 | var model = require('../model'); 18 | var storage = require('../storage'); 19 | 20 | beforeEach(function(){ 21 | }); 22 | 23 | it('saves to local storage', function() { 24 | var task = new model.Todo({title:'a task'}) 25 | expect (storage.put).toBeCalled(); 26 | }); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /src/components/todos/app.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var ENTER_KEY = 13; 5 | var ESC_KEY = 27; 6 | 7 | var app = { 8 | watchInput: function (onenter, onescape) { 9 | return function(e) { 10 | if (e.keyCode === ENTER_KEY) onenter(); 11 | if (e.keyCode === ESC_KEY) onescape(); 12 | }; 13 | }, 14 | isVisible: function (todo) { 15 | switch (app.filter()) { 16 | case 'active': 17 | return !todo.completed(); 18 | case 'completed': 19 | return todo.completed(); 20 | default: 21 | return true; 22 | } 23 | } 24 | }; 25 | 26 | module.exports = app; 27 | -------------------------------------------------------------------------------- /src/components/todos/footer.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var app = require('./app'); 5 | 6 | module.exports = m.element('footer', { 7 | 8 | controller:function(){ 9 | 10 | this.clearCompleted = app.todos.clearCompleted; 11 | this.amountCompleted = app.todos.amountCompleted; 12 | 13 | }, 14 | 15 | view: function(ctrl){ 16 | if (app.todos.list.length===0){ 17 | return ''; 18 | } 19 | var amountCompleted = ctrl.amountCompleted(); 20 | var amountActive = app.todos.list.length - amountCompleted; 21 | 22 | return m('$footer#footer', [ 23 | m('span#todo-count', [ 24 | m('strong', amountActive), ' item' + (amountActive !== 1 ? 's' : '') + ' left' 25 | ]), 26 | m('ul#filters', [ 27 | m('li', [ 28 | m('a[href=/todos]', { 29 | config: m.route, 30 | class: app.filter() === '' ? 'selected' : '' 31 | }, 'All') 32 | ]), 33 | m('li', [ 34 | m('a[href=/todos/active]', { 35 | config: m.route, 36 | class: app.filter() === 'active' ? 'selected' : '' 37 | }, 'Active') 38 | ]), 39 | m('li', [ 40 | m('a[href=/todos/completed]', { 41 | config: m.route, 42 | class: app.filter() === 'completed' ? 'selected' : '' 43 | }, 'Completed') 44 | ]) 45 | ]), amountCompleted === 0 ? '' : m('button#clear-completed', { 46 | onclick: ctrl.clearCompleted 47 | }, 'Clear completed (' + amountCompleted + ')') 48 | ]); 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /src/components/todos/header.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | module.exports = m.element('header',{ 5 | view: function(ctrl,content){ 6 | return m('$header#header', [ 7 | m('h1', 'todos'), 8 | content 9 | ]); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/todos/index.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | /*global m */ 4 | 5 | // todo modules 6 | var app = require('./app'); 7 | var model = require('./model'); 8 | 9 | require('./header'); 10 | require('./new-task'); 11 | require('./list-of-tasks'); 12 | require('./task'); 13 | require('./footer'); 14 | 15 | module.exports = m.element('todos-demo', { 16 | controller: function(){ 17 | 18 | // Todo collection 19 | app.todos = new model.Todos(); 20 | 21 | // Todo list filter 22 | app.filter = m.prop(m.route.param('filter') || ''); 23 | 24 | }, 25 | view: function(){ 26 | return m('#todoapp',[ 27 | m('header',[ 28 | m('new-task') 29 | ]), 30 | m('list-of-tasks', [ 31 | m('$task') 32 | ]), 33 | m('footer') 34 | ]); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /src/components/todos/list-of-tasks.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var app = require('./app'); 5 | 6 | module.exports = m.element('list-of-tasks',{ 7 | controller:function(){ 8 | this.completeAll = app.todos.completeAll; 9 | this.allCompleted = app.todos.allCompleted; 10 | }, 11 | view: function(ctrl,template){ 12 | return m('section#main', { 13 | style: { 14 | display: app.todos.list.length ? '' : 'none' 15 | } 16 | }, [ 17 | m('input#toggle-all[type=checkbox]', { 18 | onclick: ctrl.completeAll, 19 | checked: ctrl.allCompleted() 20 | }), 21 | m('ul#todo-list', [ 22 | app.todos.list.filter(app.isVisible).map(function (task) { 23 | return m(template,{id:task.id,state:task}); 24 | }) 25 | ]) 26 | ]); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /src/components/todos/model.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var storage = require('./storage'); 5 | 6 | var list = []; 7 | var topId = 0; 8 | 9 | // prop utility 10 | function gettersetter(store,cb) { 11 | var prop = function() { 12 | if (arguments.length) { 13 | store = arguments[0]; 14 | if (cb){ 15 | cb.call(null,store); 16 | } 17 | } 18 | return store; 19 | }; 20 | 21 | prop.toJSON = function() { 22 | return store; 23 | }; 24 | 25 | if (cb){ 26 | cb.call(null,store); 27 | } 28 | return prop; 29 | } 30 | 31 | function prop(store,cb) { 32 | return gettersetter(store,cb); 33 | } 34 | 35 | var model = { 36 | Todo: function (data) { 37 | 38 | var that = this; 39 | 40 | this.id = data.id || ++topId; 41 | this.title = prop(data.title); 42 | 43 | this.completed = prop(data.completed || false,function(){ 44 | storage.put(list); 45 | }); 46 | 47 | this.editing = prop(data.editing || false, function(){ 48 | that.title(that.title().trim()); 49 | if (!that.title()) { 50 | that.remove(); 51 | } 52 | storage.put(list); 53 | }); 54 | 55 | this.remove = function () { 56 | list.splice(list.indexOf(that), 1); 57 | storage.put(list); 58 | }; 59 | 60 | }, 61 | 62 | Todos: function(){ 63 | 64 | list = storage.get(); 65 | 66 | // Update with props 67 | list = list.map(function(item) { 68 | topId = Math.max(item.id,topId); 69 | return new model.Todo(item); 70 | }); 71 | 72 | this.add = function(title){ 73 | list.push(new model.Todo({title: title})); 74 | storage.put(list); 75 | }; 76 | 77 | this.completeAll = function () { 78 | var complete = allCompleted(); 79 | for (var i = 0; i < list.length; i++) { 80 | list[i].completed(!complete); 81 | } 82 | storage.put(list); 83 | }; 84 | 85 | var allCompleted = this.allCompleted = function () { 86 | for (var i = 0; i < list.length; i++) { 87 | if (!list[i].completed()) { 88 | return false; 89 | } 90 | } 91 | return true; 92 | }; 93 | 94 | this.clearCompleted = function () { 95 | for (var i = list.length - 1; i >= 0; i--) { 96 | if (list[i].completed()) { 97 | list.splice(i, 1); 98 | } 99 | } 100 | storage.put(list); 101 | }; 102 | 103 | this.amountCompleted = function () { 104 | var amount = 0; 105 | for (var i = 0; i < list.length; i++) { 106 | if (list[i].completed()) { 107 | amount++; 108 | } 109 | } 110 | return amount; 111 | }; 112 | 113 | this.list = list; 114 | 115 | } 116 | }; 117 | 118 | module.exports = model; 119 | -------------------------------------------------------------------------------- /src/components/todos/new-task.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var app = require('./app'); 5 | 6 | module.exports = m.element('new-task',{ 7 | controller:function(){ 8 | 9 | // Temp title placeholder 10 | this.title = m.prop(''); 11 | 12 | this.add = function () { 13 | var title = this.title().trim(); 14 | if (title) { 15 | app.todos.add(title); 16 | } 17 | this.title(''); 18 | }; 19 | 20 | this.clearTitle = function () { 21 | this.title(''); 22 | }; 23 | 24 | this.editing=false; 25 | }, 26 | view: function(ctrl){ 27 | return m('input#new-todo[placeholder="What needs to be done?"]', { 28 | onkeyup: app.watchInput(ctrl.add.bind(ctrl), 29 | ctrl.clearTitle.bind(ctrl)), 30 | value: ctrl.title(), 31 | oninput: m.withAttr('value', ctrl.title), 32 | config:function(element){ 33 | if (!ctrl.editing){ 34 | ctrl.editing = true; 35 | element.focus(); 36 | } 37 | }}); 38 | } 39 | }); 40 | 41 | -------------------------------------------------------------------------------- /src/components/todos/storage.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var STORAGE_ID = 'todos-mithril'; 5 | 6 | module.exports = { 7 | get: function () { 8 | return JSON.parse(localStorage.getItem(STORAGE_ID) || '[]'); 9 | }, 10 | put: function (todos) { 11 | localStorage.setItem(STORAGE_ID, JSON.stringify(todos)); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/todos/task.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var app = require('./app'); 5 | 6 | module.exports = m.element('task',{ 7 | controller:function(task){ 8 | 9 | this.classes = function(){ 10 | var classes = ''; 11 | classes += task.completed() ? 'completed' : ''; 12 | classes += task.editing() ? ' editing' : ''; 13 | return classes; 14 | }; 15 | 16 | var previousTitle; 17 | this.title = task.title; 18 | this.completed = task.completed.bind(task); 19 | this.remove = task.remove.bind(task); 20 | this.editing = task.editing.bind(task); 21 | 22 | this.edit = function () { 23 | previousTitle = task.title(); 24 | task.editing(true); 25 | }; 26 | 27 | this.complete = function() { 28 | var state = !task.completed(); 29 | task.completed(state); 30 | }; 31 | 32 | this.doneEditing = function () { 33 | task.editing(false); 34 | }; 35 | 36 | this.cancelEditing = function () { 37 | task.title(previousTitle); 38 | task.editing(false); 39 | }; 40 | 41 | }, 42 | view: function(ctrl){ 43 | return m('li', { class: ctrl.classes()}, [ 44 | m('.view', [ 45 | m('input.toggle[type=checkbox]', { 46 | onclick: m.withAttr('checked', ctrl.complete), 47 | checked: ctrl.completed() 48 | }), 49 | m('label', { 50 | ondblclick: ctrl.edit 51 | }, ctrl.title()), 52 | m('button.destroy', { 53 | onclick: ctrl.remove 54 | }) 55 | ]), 56 | m('input.edit', { 57 | value: ctrl.title(), 58 | onkeyup: app.watchInput(ctrl.doneEditing, ctrl.cancelEditing), 59 | oninput: m.withAttr('value', ctrl.title), 60 | config: function (element) { 61 | if (ctrl.editing()) { 62 | element.focus(); 63 | } 64 | }, 65 | onblur: ctrl.doneEditing 66 | }) 67 | ]); 68 | } 69 | }); 70 | 71 | -------------------------------------------------------------------------------- /src/pages/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found 6 | 7 | 53 | 54 | 55 |

Page Not Found

56 |

Sorry, but the page you were trying to view does not exist.

57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/pages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Mithril.Elements • Starter Kit 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/styles/.csscomb.json: -------------------------------------------------------------------------------- 1 | { 2 | "always-semicolon": true, 3 | "block-indent": 2, 4 | "colon-space": [0, 1], 5 | "color-case": "lower", 6 | "color-shorthand": true, 7 | "combinator-space": true, 8 | "element-case": "lower", 9 | "eof-newline": true, 10 | "leading-zero": false, 11 | "remove-empty-rulesets": true, 12 | "rule-indent": 2, 13 | "stick-brace": " ", 14 | "strip-spaces": true, 15 | "unitless-zero": true, 16 | "vendor-prefix-align": true, 17 | "sort-order": [ 18 | [ 19 | "position", 20 | "top", 21 | "right", 22 | "bottom", 23 | "left", 24 | "z-index", 25 | "display", 26 | "float", 27 | "width", 28 | "min-width", 29 | "max-width", 30 | "height", 31 | "min-height", 32 | "max-height", 33 | "-webkit-box-sizing", 34 | "-moz-box-sizing", 35 | "box-sizing", 36 | "-webkit-appearance", 37 | "padding", 38 | "padding-top", 39 | "padding-right", 40 | "padding-bottom", 41 | "padding-left", 42 | "margin", 43 | "margin-top", 44 | "margin-right", 45 | "margin-bottom", 46 | "margin-left", 47 | "overflow", 48 | "overflow-x", 49 | "overflow-y", 50 | "-webkit-overflow-scrolling", 51 | "-ms-overflow-x", 52 | "-ms-overflow-y", 53 | "-ms-overflow-style", 54 | "clip", 55 | "clear", 56 | "font", 57 | "font-family", 58 | "font-size", 59 | "font-style", 60 | "font-weight", 61 | "font-variant", 62 | "font-size-adjust", 63 | "font-stretch", 64 | "font-effect", 65 | "font-emphasize", 66 | "font-emphasize-position", 67 | "font-emphasize-style", 68 | "font-smooth", 69 | "-webkit-hyphens", 70 | "-moz-hyphens", 71 | "hyphens", 72 | "line-height", 73 | "color", 74 | "text-align", 75 | "-webkit-text-align-last", 76 | "-moz-text-align-last", 77 | "-ms-text-align-last", 78 | "text-align-last", 79 | "text-emphasis", 80 | "text-emphasis-color", 81 | "text-emphasis-style", 82 | "text-emphasis-position", 83 | "text-decoration", 84 | "text-indent", 85 | "text-justify", 86 | "text-outline", 87 | "-ms-text-overflow", 88 | "text-overflow", 89 | "text-overflow-ellipsis", 90 | "text-overflow-mode", 91 | "text-shadow", 92 | "text-transform", 93 | "text-wrap", 94 | "-webkit-text-size-adjust", 95 | "-ms-text-size-adjust", 96 | "letter-spacing", 97 | "-ms-word-break", 98 | "word-break", 99 | "word-spacing", 100 | "-ms-word-wrap", 101 | "word-wrap", 102 | "-moz-tab-size", 103 | "-o-tab-size", 104 | "tab-size", 105 | "white-space", 106 | "vertical-align", 107 | "list-style", 108 | "list-style-position", 109 | "list-style-type", 110 | "list-style-image", 111 | "pointer-events", 112 | "cursor", 113 | "visibility", 114 | "zoom", 115 | "flex-direction", 116 | "flex-order", 117 | "flex-pack", 118 | "flex-align", 119 | "table-layout", 120 | "empty-cells", 121 | "caption-side", 122 | "border-spacing", 123 | "border-collapse", 124 | "content", 125 | "quotes", 126 | "counter-reset", 127 | "counter-increment", 128 | "resize", 129 | "-webkit-user-select", 130 | "-moz-user-select", 131 | "-ms-user-select", 132 | "-o-user-select", 133 | "user-select", 134 | "nav-index", 135 | "nav-up", 136 | "nav-right", 137 | "nav-down", 138 | "nav-left", 139 | "background", 140 | "background-color", 141 | "background-image", 142 | "-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient", 143 | "filter:progid:DXImageTransform.Microsoft.gradient", 144 | "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader", 145 | "filter", 146 | "background-repeat", 147 | "background-attachment", 148 | "background-position", 149 | "background-position-x", 150 | "background-position-y", 151 | "-webkit-background-clip", 152 | "-moz-background-clip", 153 | "background-clip", 154 | "background-origin", 155 | "-webkit-background-size", 156 | "-moz-background-size", 157 | "-o-background-size", 158 | "background-size", 159 | "border", 160 | "border-color", 161 | "border-style", 162 | "border-width", 163 | "border-top", 164 | "border-top-color", 165 | "border-top-style", 166 | "border-top-width", 167 | "border-right", 168 | "border-right-color", 169 | "border-right-style", 170 | "border-right-width", 171 | "border-bottom", 172 | "border-bottom-color", 173 | "border-bottom-style", 174 | "border-bottom-width", 175 | "border-left", 176 | "border-left-color", 177 | "border-left-style", 178 | "border-left-width", 179 | "border-radius", 180 | "border-top-left-radius", 181 | "border-top-right-radius", 182 | "border-bottom-right-radius", 183 | "border-bottom-left-radius", 184 | "-webkit-border-image", 185 | "-moz-border-image", 186 | "-o-border-image", 187 | "border-image", 188 | "-webkit-border-image-source", 189 | "-moz-border-image-source", 190 | "-o-border-image-source", 191 | "border-image-source", 192 | "-webkit-border-image-slice", 193 | "-moz-border-image-slice", 194 | "-o-border-image-slice", 195 | "border-image-slice", 196 | "-webkit-border-image-width", 197 | "-moz-border-image-width", 198 | "-o-border-image-width", 199 | "border-image-width", 200 | "-webkit-border-image-outset", 201 | "-moz-border-image-outset", 202 | "-o-border-image-outset", 203 | "border-image-outset", 204 | "-webkit-border-image-repeat", 205 | "-moz-border-image-repeat", 206 | "-o-border-image-repeat", 207 | "border-image-repeat", 208 | "outline", 209 | "outline-width", 210 | "outline-style", 211 | "outline-color", 212 | "outline-offset", 213 | "-webkit-box-shadow", 214 | "-moz-box-shadow", 215 | "box-shadow", 216 | "filter:progid:DXImageTransform.Microsoft.Alpha(Opacity", 217 | "-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha", 218 | "opacity", 219 | "-ms-interpolation-mode", 220 | "-webkit-transition", 221 | "-moz-transition", 222 | "-ms-transition", 223 | "-o-transition", 224 | "transition", 225 | "-webkit-transition-delay", 226 | "-moz-transition-delay", 227 | "-ms-transition-delay", 228 | "-o-transition-delay", 229 | "transition-delay", 230 | "-webkit-transition-timing-function", 231 | "-moz-transition-timing-function", 232 | "-ms-transition-timing-function", 233 | "-o-transition-timing-function", 234 | "transition-timing-function", 235 | "-webkit-transition-duration", 236 | "-moz-transition-duration", 237 | "-ms-transition-duration", 238 | "-o-transition-duration", 239 | "transition-duration", 240 | "-webkit-transition-property", 241 | "-moz-transition-property", 242 | "-ms-transition-property", 243 | "-o-transition-property", 244 | "transition-property", 245 | "-webkit-transform", 246 | "-moz-transform", 247 | "-ms-transform", 248 | "-o-transform", 249 | "transform", 250 | "-webkit-transform-origin", 251 | "-moz-transform-origin", 252 | "-ms-transform-origin", 253 | "-o-transform-origin", 254 | "transform-origin", 255 | "-webkit-animation", 256 | "-moz-animation", 257 | "-ms-animation", 258 | "-o-animation", 259 | "animation", 260 | "-webkit-animation-name", 261 | "-moz-animation-name", 262 | "-ms-animation-name", 263 | "-o-animation-name", 264 | "animation-name", 265 | "-webkit-animation-duration", 266 | "-moz-animation-duration", 267 | "-ms-animation-duration", 268 | "-o-animation-duration", 269 | "animation-duration", 270 | "-webkit-animation-play-state", 271 | "-moz-animation-play-state", 272 | "-ms-animation-play-state", 273 | "-o-animation-play-state", 274 | "animation-play-state", 275 | "-webkit-animation-timing-function", 276 | "-moz-animation-timing-function", 277 | "-ms-animation-timing-function", 278 | "-o-animation-timing-function", 279 | "animation-timing-function", 280 | "-webkit-animation-delay", 281 | "-moz-animation-delay", 282 | "-ms-animation-delay", 283 | "-o-animation-delay", 284 | "animation-delay", 285 | "-webkit-animation-iteration-count", 286 | "-moz-animation-iteration-count", 287 | "-ms-animation-iteration-count", 288 | "-o-animation-iteration-count", 289 | "animation-iteration-count", 290 | "-webkit-animation-direction", 291 | "-moz-animation-direction", 292 | "-ms-animation-direction", 293 | "-o-animation-direction", 294 | "animation-direction" 295 | ] 296 | ] 297 | } 298 | -------------------------------------------------------------------------------- /src/styles/.csslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "adjoining-classes": false, 3 | "box-sizing": false, 4 | "box-model": false, 5 | "compatible-vendor-prefixes": false, 6 | "floats": false, 7 | "font-sizes": false, 8 | "gradients": false, 9 | "important": false, 10 | "known-properties": false, 11 | "outline-none": false, 12 | "qualified-headings": false, 13 | "regex-selectors": false, 14 | "shorthand": false, 15 | "text-indent": false, 16 | "unique-headings": false, 17 | "universal-selector": false, 18 | "unqualified-attributes": false 19 | } 20 | -------------------------------------------------------------------------------- /src/styles/app.less: -------------------------------------------------------------------------------- 1 | @import "bootstrap.less"; 2 | 3 | .jumbotron .container { 4 | text-align:center; 5 | } 6 | 7 | /* todo-mvc overrides */ 8 | 9 | body { 10 | width:768px !important; 11 | } 12 | 13 | .tabset { 14 | width:550px; 15 | margin: 0 auto; 16 | } 17 | 18 | #todoapp { 19 | label { 20 | font-weight:normal; 21 | margin-bottom:0; 22 | } 23 | 24 | #todo-list { 25 | margin: 0; 26 | padding: 0; 27 | list-style: none; 28 | } 29 | } -------------------------------------------------------------------------------- /src/styles/bootstrap.less: -------------------------------------------------------------------------------- 1 | // ============================================================================= 2 | // Bootstrap CSS + Custom styles and overrides 3 | // ============================================================================= 4 | 5 | // Core variables and mixins 6 | @import "variables.less"; 7 | @import "mixins.less"; 8 | 9 | // Reset and dependencies 10 | @import "../../node_modules/bootstrap/less/normalize.less"; 11 | @import "../../node_modules/bootstrap/less/print.less"; 12 | @import "../../node_modules/bootstrap/less/glyphicons.less"; 13 | 14 | // Core CSS 15 | @import "../../node_modules/bootstrap/less/scaffolding.less"; 16 | @import "../../node_modules/bootstrap/less/type.less"; 17 | @import "../../node_modules/bootstrap/less/code.less"; 18 | @import "../../node_modules/bootstrap/less/grid.less"; 19 | @import "../../node_modules/bootstrap/less/tables.less"; 20 | @import "../../node_modules/bootstrap/less/forms.less"; 21 | @import "../../node_modules/bootstrap/less/buttons.less"; 22 | 23 | // Components 24 | @import "../../node_modules/bootstrap/less/component-animations.less"; 25 | @import "../../node_modules/bootstrap/less/dropdowns.less"; 26 | @import "../../node_modules/bootstrap/less/button-groups.less"; 27 | @import "../../node_modules/bootstrap/less/input-groups.less"; 28 | @import "../../node_modules/bootstrap/less/navs.less"; 29 | @import "navbar.less"; 30 | @import "../../node_modules/bootstrap/less/breadcrumbs.less"; 31 | @import "../../node_modules/bootstrap/less/pagination.less"; 32 | @import "../../node_modules/bootstrap/less/pager.less"; 33 | @import "../../node_modules/bootstrap/less/labels.less"; 34 | @import "../../node_modules/bootstrap/less/badges.less"; 35 | @import "jumbotron.less"; 36 | @import "../../node_modules/bootstrap/less/thumbnails.less"; 37 | @import "../../node_modules/bootstrap/less/alerts.less"; 38 | @import "../../node_modules/bootstrap/less/progress-bars.less"; 39 | @import "../../node_modules/bootstrap/less/media.less"; 40 | @import "../../node_modules/bootstrap/less/list-group.less"; 41 | @import "../../node_modules/bootstrap/less/panels.less"; 42 | @import "../../node_modules/bootstrap/less/responsive-embed.less"; 43 | @import "../../node_modules/bootstrap/less/wells.less"; 44 | @import "../../node_modules/bootstrap/less/close.less"; 45 | 46 | // Components w/ JavaScript 47 | @import "../../node_modules/bootstrap/less/modals.less"; 48 | @import "../../node_modules/bootstrap/less/tooltip.less"; 49 | @import "../../node_modules/bootstrap/less/popovers.less"; 50 | @import "../../node_modules/bootstrap/less/carousel.less"; 51 | 52 | // Utility classes 53 | @import "utilities.less"; 54 | @import "../../node_modules/bootstrap/less/responsive-utilities.less"; 55 | -------------------------------------------------------------------------------- /src/styles/jumbotron.less: -------------------------------------------------------------------------------- 1 | // ============================================================================= 2 | // Jumbotron 3 | // ============================================================================= 4 | 5 | @import "../../node_modules/bootstrap/less/jumbotron.less"; 6 | 7 | .jumbotron { 8 | p { 9 | text-transform: uppercase; 10 | letter-spacing: 1px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/styles/mixins.less: -------------------------------------------------------------------------------- 1 | // ============================================================================= 2 | // Mixins 3 | // ============================================================================= 4 | 5 | @import "../../node_modules/bootstrap/less/mixins.less"; 6 | -------------------------------------------------------------------------------- /src/styles/navbar.less: -------------------------------------------------------------------------------- 1 | // ============================================================================= 2 | // Navigation Bar 3 | // ============================================================================= 4 | 5 | @import "../../node_modules/bootstrap/less/navbar.less"; 6 | 7 | body { 8 | padding: @navbar-height 0; 9 | overflow-y: scroll; 10 | } 11 | 12 | .navbar-top { 13 | &:extend(.navbar); 14 | &:extend(.navbar-inverse); 15 | &:extend(.navbar-fixed-top); 16 | } 17 | 18 | .navbar-brand { 19 | padding-top: 8px; 20 | padding-bottom: 8px; 21 | color: #00d8ff !important; 22 | font-size: 24px; 23 | 24 | img { 25 | margin-right: 10px; 26 | display: inline; 27 | } 28 | } 29 | 30 | // 31 | // Navigation Footer 32 | // ----------------------------------------------------------------------------- 33 | 34 | .navbar-footer { 35 | &:extend(.navbar); 36 | &:extend(.navbar-fixed-bottom); 37 | 38 | background-color: darken(@body-bg, 2%); 39 | 40 | .text-muted { 41 | margin-top: 1em; 42 | 43 | span + span:before { 44 | content: "\2022\00a0"; // Unicode space added since inline-block means non-collapsing white-space 45 | padding: 0 3px 0 7px; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/styles/utilities.less: -------------------------------------------------------------------------------- 1 | // ============================================================================= 2 | // Utility classes 3 | // ============================================================================= 4 | 5 | @import "../../node_modules/bootstrap/less/utilities.less"; 6 | 7 | // 8 | // Browse Happy prompt 9 | // ----------------------------------------------------------------------------- 10 | 11 | .browsehappy { 12 | margin: 0.2em 0; 13 | background: #ccc; 14 | color: #000; 15 | padding: 0.2em 0; 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/variables.less: -------------------------------------------------------------------------------- 1 | // ============================================================================= 2 | // Variables 3 | // ============================================================================= 4 | 5 | @import "../../node_modules/bootstrap/less/variables.less"; 6 | 7 | // 8 | // Scaffolding 9 | // ----------------------------------------------------------------------------- 10 | 11 | // Background color for `` 12 | @body-bg: #f9f9f9; 13 | // Global text color on `` 14 | @text-color: #484848; 15 | // Global textual link color 16 | @link-color: #c05b4d; 17 | 18 | // 19 | // Jumbotron 20 | // ----------------------------------------------------------------------------- 21 | 22 | @jumbotron-heading-color: #61DAFB; 23 | @jumbotron-color: #E9E9E9; 24 | @jumbotron-bg: #2d2d2d; 25 | @jumbotron-font-size: @font-size-base; 26 | --------------------------------------------------------------------------------