├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── dist └── bundle.js ├── examples └── index.html ├── package.json ├── rollup.config.js └── src ├── app ├── controller.js ├── index.js ├── model.js └── view.js └── framework └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", {"modules": false}] 4 | ], 5 | "plugins": ["external-helpers"] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Yifeng Wang 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nano-mvc 2 | Demo MVC framework in 40 lines, 1KB 3 | 4 | ## Run Example 5 | `npm install && npm run example`, then visit `localhost:10008/examples` in browser. 6 | -------------------------------------------------------------------------------- /dist/bundle.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";function t(t){return'\n
\n \n \n
'+t.todos.map(function(t){return'\n
\n \n '+t.text+'\n \n \n\n \n \n \n \n Finish\n \n \n Redo\n \n \n
\n '}).join("")+"
\n
\n "}var e=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},n=function(){function t(t,e){for(var n=0;n 2 | 3 | 4 | Nano MVC Demo 5 | 16 | 17 | 18 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nano-mvc", 3 | "version": "0.1.0", 4 | "description": "Demo MVC framework in 40 lines, 1KB", 5 | "main": "src/framework/index.js", 6 | "scripts": { 7 | "dev": "cross-env NODE_ENV=dev rollup -c --watch", 8 | "build": "cross-env NODE_ENV=production rollup -c", 9 | "example": "http-server . -s -p 10008" 10 | }, 11 | "keywords": [ 12 | "mvc", 13 | "framework" 14 | ], 15 | "author": "doodlewind", 16 | "repository": "https://github.com/doodlewind/nano-mvc", 17 | "devDependencies": { 18 | "babel-core": "^6.0.0", 19 | "babel-plugin-external-helpers": "^6.0.0", 20 | "babel-preset-es2015": "^6.0.0", 21 | "cross-env": "^3.0.0", 22 | "http-server": "^0.10.0", 23 | "rollup": "^0.41.4", 24 | "rollup-plugin-babel": "^2.7.1", 25 | "rollup-plugin-uglify": "^1.0.1", 26 | "rollup-watch": "^3.2.2" 27 | }, 28 | "license": "MIT" 29 | } 30 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import uglify from 'rollup-plugin-uglify' 3 | 4 | const plugins = [babel()] 5 | if (process.env.NODE_ENV === 'production') plugins.push(uglify()) 6 | 7 | export default({ 8 | entry: 'src/app/index.js', 9 | dest: 'dist/bundle.js', 10 | format: 'iife', 11 | moduleName: 'nanoMVC', 12 | plugins 13 | }) 14 | -------------------------------------------------------------------------------- /src/app/controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from '../framework/index' 2 | 3 | export class TodoController extends Controller { 4 | constructor (model, view) { 5 | super({ 6 | model, 7 | view, 8 | el: '#app', 9 | onClick: { 10 | '.btn-add' () { 11 | // 新增 Todo 时对数据全量赋值 12 | this.model.todos = this.model.todos.concat([{ 13 | id: new Date().getTime().toString(), 14 | // 使用 done 属性标识是否完成 15 | done: false, 16 | // 使用 getter 获取绑定至 DOM 元素的数据 17 | text: this.addInputText 18 | }]) 19 | }, 20 | '.btn-delete' (e) { 21 | const id = this.getTargetAttr(e, 'data-id') 22 | // 根据 id 过滤掉待删除元素 23 | this.model.todos = this.model.todos.filter( 24 | todo => todo.id !== id 25 | ) 26 | }, 27 | '.btn-update' (e) { 28 | const id = this.getTargetAttr(e, 'data-id') 29 | const text = this.getUpdateText(id) 30 | // 根据 id 更新元素 31 | this.model.todos = this.model.todos.map( 32 | todo => ({ 33 | id: todo.id, 34 | done: todo.done, 35 | text: todo.id === id ? text : todo.text 36 | }) 37 | ) 38 | }, 39 | // 点击 redo 按钮时将对应 id 元素 done 设为 false 40 | '.btn-redo' (e) { 41 | const id = this.getTargetAttr(e, 'data-id') 42 | this.model.todos = this.model.todos.map( 43 | todo => ({ 44 | id: todo.id, 45 | done: todo.id === id ? false : todo.done, 46 | text: todo.text 47 | }) 48 | ) 49 | }, 50 | // 点击 finish 按钮时将对应 id 元素 done 设为 true 51 | '.btn-finish' (e) { 52 | const id = this.getTargetAttr(e, 'data-id') 53 | this.model.todos = this.model.todos.map( 54 | todo => ({ 55 | id: todo.id, 56 | done: todo.id === id ? true : todo.done, 57 | text: todo.text 58 | }) 59 | ) 60 | } 61 | } 62 | }) 63 | // 订阅 Model 更新事件 64 | this.model.subscribers.push(this.render) 65 | } 66 | getUpdateText (id) { 67 | return super.getChild(`input[data-id="${id}"]`).value 68 | } 69 | get addInputText () { 70 | return super.getChild('.input-add').value 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/app/index.js: -------------------------------------------------------------------------------- 1 | import { TodoModel } from './model' 2 | import { TodoController } from './controller' 3 | import { TodoView as view } from './view' 4 | 5 | const model = new TodoModel() 6 | const controller = new TodoController(model, view) 7 | controller.render() 8 | -------------------------------------------------------------------------------- /src/app/model.js: -------------------------------------------------------------------------------- 1 | import { Model } from '../framework/index' 2 | 3 | export class TodoModel extends Model { 4 | constructor () { 5 | super({ todos: [] }) 6 | } 7 | get todos () { 8 | return this.data.todos 9 | } 10 | set todos (todos) { 11 | this.data.todos = todos 12 | this.publish(todos) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/view.js: -------------------------------------------------------------------------------- 1 | export function TodoView ({ todos }) { 2 | const todosList = todos.map(todo => ` 3 |
4 | 5 | ${todo.text} 6 | 7 | 10 | 11 | 12 | 13 | 16 | 22 | 28 | 29 |
30 | `).join('') 31 | 32 | return (` 33 |
34 | 35 | 36 |
${todosList}
37 |
38 | `) 39 | } 40 | -------------------------------------------------------------------------------- /src/framework/index.js: -------------------------------------------------------------------------------- 1 | export class Model { 2 | constructor (data) { 3 | this.data = data 4 | this.subscribers = [] 5 | } 6 | publish (data) { 7 | this.subscribers.forEach(callback => callback(data)) 8 | } 9 | } 10 | 11 | export class Controller { 12 | constructor (conf) { 13 | this.el = document.querySelector(conf.el) 14 | this.model = conf.model 15 | this.view = conf.view 16 | this.render = this.render.bind(this) 17 | this.el.addEventListener('click', (e) => { 18 | e.stopPropagation() 19 | const rules = Object.keys(conf.onClick || {}) 20 | rules.forEach((rule) => { 21 | if (e.path[0].matches(rule)) conf.onClick[rule].call(this, e) 22 | }) 23 | }) 24 | } 25 | getTargetAttr (e, attr) { 26 | return e.path[0].getAttribute(attr) 27 | } 28 | getChild (selector) { 29 | return this.el.querySelector(selector) 30 | } 31 | render () { 32 | this.el.innerHTML = this.view(this.model) 33 | } 34 | } 35 | --------------------------------------------------------------------------------