├── app
├── assets
│ ├── css
│ │ └── app.css
│ └── images
│ │ └── favicon.ico
├── index.html
└── js
│ ├── actions
│ └── AppActionCreator.js
│ ├── boot.js
│ ├── constants
│ └── AppConstants.js
│ ├── dispatcher
│ └── AppDispatcher.js
│ ├── fixtures
│ └── _readme.js
│ ├── mixins
│ ├── DirtyCheck.js
│ ├── MixinManager.js
│ └── Pushstate.js
│ ├── stores
│ └── TodoStore.js
│ └── views
│ ├── Detail.jsx
│ ├── Footer.jsx
│ ├── Header.jsx
│ ├── InputBox.jsx
│ ├── List.jsx
│ ├── ListItem.jsx
│ └── MainApp.jsx
├── gulpfile.js
├── package.json
├── readme.md
└── server.js
/app/assets/css/app.css:
--------------------------------------------------------------------------------
1 |
2 | .wrapper {
3 | background-color: #aadca8;
4 | width: 100%;
5 | position: absolute;
6 | left: 0;
7 | top: 0;
8 | right: 0;
9 | bottom: 0;
10 | }
11 |
12 | .logo {
13 | line-height: 48px;
14 | width: auto;
15 | display: inline-block;
16 | font-size: 20px;
17 | font-weight: bold;
18 | color: #F5F5F5;
19 | }
20 |
21 | .header {
22 | width: 100%;
23 | height: 48px;
24 | background-color: #A48163;
25 | position: absolute;
26 | top: 0;
27 | left: 0;
28 | padding: 0 10px;
29 | }
30 |
31 | .footer {
32 | position: absolute;
33 | left: 0;
34 | bottom: 0;
35 | width:100%;
36 | height: 36px;
37 | background-color: #A48163;
38 | }
39 |
40 | .footer span {
41 | color: white;
42 | display: block;
43 | text-align: center;
44 | line-height: 36px;
45 | }
46 |
47 | .search-box {
48 | /* line-height: 48px; */
49 | margin-right: 10px;
50 | margin-top: 8px;
51 | }
52 |
53 | .main-box {
54 | width: 80%;
55 | margin: 80px auto;
56 | padding: 10px;
57 | background-color: #F0E68C
58 | }
59 |
60 | .input-box {
61 | width: 100%;
62 | height:34px;
63 | margin-bottom: 10px;
64 | }
65 |
66 | .input-box .search-input {
67 | width: 82%;
68 | height: 100%;
69 | }
70 |
71 | .save-button {
72 | width: 16%;
73 | height: 100%;
74 | }
75 |
76 | .todo-list {
77 | border: 1px solid gray;
78 | height: 200px;
79 | overflow: scroll;
80 | }
81 |
82 | .list-item {
83 | padding: 0px 14px;
84 | line-height: 40px;
85 | height: 40px;
86 | border-top: 1px solid gray;
87 | border-bottom: 1px solid gray;
88 | margin-top: -1px;
89 | }
90 |
91 |
92 |
93 | .list-item span {
94 | line-height: 36px;
95 | outline: 0;
96 | }
97 |
98 | .selected {
99 | background-color: #C2DAC2;
100 | }
101 |
102 | .boxo {
103 | border: 1px dashed red;
104 | }
105 | .hide {
106 | display: none;
107 | }
108 |
109 | .right {
110 | float: right;
111 | }
112 |
113 | /*
114 | Tablet
115 | 480-720px
116 | */
117 | @media (max-width: 480px) {
118 |
119 | .main-box {
120 | position: absolute;
121 | top: 48px;
122 | left: 0;
123 | right: 0;
124 | bottom: 36px;
125 | width: 100%;
126 | margin: 0;
127 | padding: 10px;
128 | background-color: #F0E68C
129 | }
130 |
131 | .todo-list {
132 | border: 2px solid gray;
133 | height: auto;
134 | overflow: scroll;
135 | position: absolute;
136 | top: 55px;
137 | bottom: 10px;
138 | left: 10px;
139 | right: 10px;
140 | }
141 |
142 | }
143 |
144 | /*
145 | Desktop
146 | 一般應該是 1024+ 才進入此段
147 | 但為了示範方便,先寫 720px
148 | */
149 | @media (min-width: 720px) {
150 |
151 | .todo-list {
152 | border: 2px solid gray;
153 | width: 120px;
154 | height: 200px;
155 | overflow: scroll;
156 | display: inline-block;
157 | }
158 |
159 | .item-detail {
160 | float: right;
161 | width: calc(100% - 140px);
162 | height: 200px;
163 | display: inline-block;
164 | }
165 |
166 | .item-detail button {
167 | margin-top: -45px;
168 | float: right;
169 | }
170 |
171 | }
--------------------------------------------------------------------------------
/app/assets/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coodoo/react-meetup-1/0854faf17e5fac6e41acb7b42016823a6bcf9a6b/app/assets/images/favicon.ico
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React/Flux 入門起手式範例程式
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/js/actions/AppActionCreator.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | */
4 | var AppDispatcher = require('../dispatcher/AppDispatcher');
5 | var AppConstants = require('../constants/AppConstants');
6 | var Promise = require('es6-promise').Promise;
7 |
8 | /**
9 | * 這是一個 singleton 物件
10 | */
11 | var AppActionCreators = {
12 |
13 | /**
14 | * app 啟動後,第一次載入資料
15 | */
16 | load: function(){
17 | //
18 | },
19 |
20 | /**
21 | *
22 | */
23 | createTodo: function( item ) {
24 |
25 | // 1. 廣播給 store 知道去 optimistic 更新 view
26 | AppDispatcher.handleViewAction({
27 |
28 | // type 是為了方便將來所有 Store 內部判斷是否要處理這個 action
29 | actionType: AppConstants.TODO_CREATE,
30 |
31 | // 這裏是真正要傳出去的值
32 | item: item
33 | });
34 |
35 | },
36 |
37 | /**
38 | *
39 | */
40 | selectTodo: function( item ) {
41 |
42 | AppDispatcher.handleViewAction({
43 | actionType: AppConstants.TODO_SELECT,
44 | item: item
45 | });
46 |
47 | },
48 |
49 | /**
50 | *
51 | */
52 | removeTodo: function( item ) {
53 |
54 | AppDispatcher.handleViewAction({
55 | actionType: AppConstants.TODO_REMOVE,
56 | item: item
57 | });
58 |
59 | },
60 |
61 | /**
62 | *
63 | */
64 | updateTodo: function( item, newVal ) {
65 |
66 | AppDispatcher.handleViewAction({
67 | actionType: AppConstants.TODO_UPDATE,
68 | item: item,
69 | newVal: newVal
70 | });
71 |
72 | },
73 |
74 | /**
75 | *
76 | */
77 | doSearch: function( val ) {
78 |
79 | AppDispatcher.handleViewAction({
80 | actionType: AppConstants.TODO_FILTER,
81 | val: val
82 | });
83 |
84 | },
85 |
86 | // dummy
87 | noop: function(){}
88 | };
89 |
90 | module.exports = AppActionCreators;
91 |
--------------------------------------------------------------------------------
/app/js/boot.js:
--------------------------------------------------------------------------------
1 | /*
2 | * 這裏是整支程式的進入點,它負責建立 root view,
3 | * 也就是 MainApp 元件,將它建立起來放到畫面上
4 | *
5 | * boot.js 存在的目地,是因為通常 app 啟動時有許多先期工作要完成,
6 | * 例如預載資料到 store 內、檢查本地端 db 狀態、切換不同語系字串、
7 | * 這些工作都先在 boot.js 內做完,再啟動 root view 是比較理想的流程
8 | *
9 | */
10 |
11 | // v0.12 開始要用 createFactory 包一次才能使用元件
12 | // 如果不希望這麼麻煩,只要在每份 js 裏都加下面這句即可,但它有缺點
13 | // var React = require('react');
14 | //
15 | // 因為 require('...') 只是拿到一份元件定義檔,無法直接使用
16 | // 要用它建立一個 factory,之後才能產出 instance,下面 createFactory() 就是在建立工廠
17 | var MainApp = React.createFactory(require('./views/MainApp.jsx'));
18 |
19 | $(function(){
20 |
21 | //
22 | React.render( MainApp(), document.getElementById('container') );
23 |
24 | })
25 |
--------------------------------------------------------------------------------
/app/js/constants/AppConstants.js:
--------------------------------------------------------------------------------
1 | /**
2 | * TodoConstants
3 | */
4 | var keyMirror = function(obj) {
5 | var ret = {};
6 | var key;
7 | for (key in obj) {
8 | if (!obj.hasOwnProperty(key)) {
9 | continue;
10 | }
11 | ret[key] = key;
12 | }
13 | return ret;
14 | };
15 |
16 | // Constructs an enumeration with keys equal to their value.
17 | // 也就是讓 hash 的 key 與 value 值一樣
18 | // 不然原本 value 都是 null
19 | // 不過既然如此,為何不乾脆用 set 之類只有key 的就好
20 | module.exports = keyMirror({
21 |
22 | SOURCE_VIEW_ACTION: null,
23 | SOURCE_SERVER_ACTION: null,
24 | SOURCE_ROUTER_ACTION: null,
25 |
26 | CHANGE_EVENT: null,
27 |
28 | TODO_CREATE: null,
29 |
30 | TODO_REMOVE: null,
31 |
32 | TODO_UPDATE: null,
33 |
34 | TODO_SELECT: null,
35 |
36 | TODO_FILTER: null,
37 |
38 | noop: null
39 | });
40 |
41 |
--------------------------------------------------------------------------------
/app/js/dispatcher/AppDispatcher.js:
--------------------------------------------------------------------------------
1 |
2 | var AppConstants = require('../constants/AppConstants');
3 |
4 | var Dispatcher = require('flux').Dispatcher;
5 |
6 |
7 | /**
8 | * flux-chat 內最新的 dispatcher
9 | */
10 | var AppDispatcher = new Dispatcher();
11 |
12 | // 注意:這裏等於是繼承 Dispatcher class 身上所有指令,目地是讓此物件俱有廣播能功
13 | // 同樣功能也可用 underscore.extend 或 Object.assign() 做到
14 | // 今天因為有用 jquery 就請它代勞了
15 | $.extend( AppDispatcher, {
16 |
17 | /**
18 | * @param {object} action The details of the action, including the action's
19 | * type and additional data coming from the server.
20 | */
21 | handleServerAction: function(action) {
22 | var payload = {
23 | source: AppConstants.SOURCE_SERVER_ACTION,
24 | action: action
25 | };
26 |
27 | this.dispatch(payload);
28 | },
29 |
30 | /**
31 | *
32 | */
33 | handleViewAction: function(action) {
34 | var payload = {
35 | source: AppConstants.SOURCE_VIEW_ACTION,
36 | action: action
37 | };
38 |
39 | this.dispatch(payload);
40 | },
41 |
42 | /**
43 | * 將來啟用 router 時,這裏處理所有 router event
44 | */
45 | handleRouterAction: function(path) {
46 | this.dispatch({
47 | source: AppConstants.SOURCE_ROUTER_ACTION,
48 | action: path
49 | });
50 | }
51 |
52 | });
53 |
54 | module.exports = AppDispatcher;
55 |
--------------------------------------------------------------------------------
/app/js/fixtures/_readme.js:
--------------------------------------------------------------------------------
1 | // 如果需要假資料供開發用,放這裏
2 |
3 | /*jshint maxlen:200 */
4 | // 這是為了建立假資料,方便程式能跑起來,跟測試無關
5 | (function () {
6 | 'use strict';
7 |
8 | App.Fixtures || (App.Fixtures = {});
9 |
10 | App.Fixtures.Notes = [
11 | {
12 | "createdAt": Date.UTC(2013, 1, 23, 0, 0, 0),
13 | "text": "## Recreation\n* Seattle Bouldering Project\n* Burke-Gilman Trail\n* Ballard Locks\n\n## Dining\n* Pies and Pints\n* Señor Moose's\n* Delancey\n* Dahlia Lounge",
14 | "title": "Things to do in Seattle."
15 | },
16 | {
17 | "createdAt": Date.UTC(2013, 1, 23, 0, 0, 1),
18 | "text": "## Sights\n* Tidal Basin\n* National Mall\n* Rock Creek Trail\n\n## Cultural\n* National Portrait Gallery\n* National Gallery of Art\n* Hirshhorn Museum\n* The Phillips Collection",
19 | "title": "Things to do in Washington, DC."
20 | },
21 | {
22 | "createdAt": Date.UTC(2013, 1, 23, 0, 0, 2),
23 | "text": "## Europe\n* Scotland\n* Ireland\n\n## South America\n* Patagonia\n* Peru",
24 | "title": "Places I want to go."
25 | }
26 | ];
27 |
28 | }());
29 |
--------------------------------------------------------------------------------
/app/js/mixins/DirtyCheck.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | monitor: function( target, flag) {
4 | this.isDirty = false;
5 |
6 | // dbg( 'mooooo: ', target, flag );
7 |
8 | Object.observe( target, function(changes) {
9 | // dbg( '\n\n\n有改 = ', changes );
10 | if (changes.length > 0)
11 | this[flag] = true;
12 | }.bind(this));
13 | }
14 |
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/app/js/mixins/MixinManager.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * react-mixin-manager v0.4.0
3 | * https://github.com/jhudson8/react-mixin-manager
4 | *
5 | *
6 | * Copyright (c) 2014 Joe Hudson
7 | *
8 | * Permission is hereby granted, free of charge, to any person obtaining a copy
9 | * of this software and associated documentation files (the "Software"), to deal
10 | * in the Software without restriction, including without limitation the rights
11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | * copies of the Software, and to permit persons to whom the Software is
13 | * furnished to do so, subject to the following conditions:
14 | *
15 | * The above copyright notice and this permission notice shall be included in
16 | * all copies or substantial portions of the Software.
17 | *
18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | * THE SOFTWARE.
25 | */
26 | (function(main) {
27 | if (typeof define === 'function' && define.amd) {
28 | define(['react'], main);
29 | } else if (typeof exports !== 'undefined' && typeof require !== 'undefined') {
30 | module.exports = function(React) {
31 | main(React);
32 | };
33 | } else {
34 | main(React);
35 | }
36 | })(function(React) {
37 |
38 | /**
39 | * return the normalized mixin list
40 | * @param values {Array} list of mixin entries
41 | * @param index {Object} hash which contains a truthy value for all named mixins that have been added
42 | * @param rtn {Array} the normalized return array
43 | */
44 | function get(values, index, rtn) {
45 |
46 | /**
47 | * add the named mixin and all un-added dependencies to the return array
48 | * @param the mixin name
49 | */
50 | function addTo(name) {
51 | if (!index[name]) {
52 | var mixin = React.mixins._mixins[name],
53 | checkAgain = false;
54 | if (mixin) {
55 | if (typeof mixin === 'function') {
56 | mixin = mixin();
57 | checkAgain = true;
58 | }
59 | get(React.mixins._dependsOn[name], index, rtn);
60 | get(React.mixins._dependsInjected[name], index, rtn);
61 |
62 | index[name] = true;
63 | if (checkAgain) {
64 | get([mixin], index, rtn);
65 | } else {
66 | rtn.push(mixin);
67 | }
68 |
69 | } else {
70 | throw 'invalid mixin "' + name + '"';
71 | }
72 | }
73 | }
74 |
75 | function handleMixin(mixin) {
76 | if (mixin) {
77 | if (Array.isArray(mixin)) {
78 | // flatten it out
79 | get(mixin, index, rtn);
80 | } else if (typeof mixin === 'string') {
81 | // add the named mixin and all of it's dependencies
82 | addTo(mixin);
83 | } else {
84 | // just add the mixin normally
85 | rtn.push(mixin);
86 | }
87 | }
88 | }
89 |
90 | if (Array.isArray(values)) {
91 | for (var i=0; i
40 |
41 |
42 |
43 |
48 |
49 |
50 |
51 |
52 |
{ date }
53 |
54 |
55 |
56 |
57 |
{this.state.selectedItem.uid}
58 |
59 |
60 |
62 |
63 |
64 | );
65 |
66 | },
67 |
68 | /**
69 | * 大部份 ui 操作最終都是直接轉手給 actions 去處理
70 | */
71 | handleClick: function( evt ){
72 | evt.preventDefault();
73 | // console.log( '\n\nsave button click: ', this.state.selectedItem.name );
74 | actions.updateTodo(this.state.selectedItem, this.state.selectedItem.name);
75 | },
76 |
77 |
78 | /**
79 | * input 是 controlled component
80 | * 它的值是綁定在 this.state.selectedItem 身上
81 | * 因此要在 change 時將新值設回 selectedItem 內,才會顯示在畫面上
82 | */
83 | handleChange: function(field, evt){
84 | this.state.selectedItem[field] = evt.target.value;
85 | this.setState( {selectedItem: this.state.selectedItem} );
86 | },
87 |
88 | //
89 | noop: function(){
90 | console.log( 'd: ', new Date(this.state.selectedItem.created) );
91 | }
92 |
93 | });
94 |
95 | module.exports = comp;
--------------------------------------------------------------------------------
/app/js/views/Footer.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | */
4 | var actions = require('../actions/AppActionCreator');
5 |
6 | /**
7 | *
8 | */
9 | var Footer = React.createClass({
10 |
11 |
12 | /**
13 | *
14 | */
15 | render: function() {
16 |
17 | return (
18 |
23 | );
24 | },
25 |
26 |
27 | });
28 |
29 | module.exports = Footer;
30 |
--------------------------------------------------------------------------------
/app/js/views/Header.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | */
4 | var actions = require('../actions/AppActionCreator');
5 |
6 | /**
7 | *
8 | */
9 | var Header = React.createClass({
10 |
11 | /**
12 | *
13 | */
14 | render: function() {
15 |
16 |
17 | return (
18 |
19 |
29 | );
30 |
31 | },
32 |
33 | /**
34 | *
35 | */
36 | handleChange: function(evt){
37 | var val = evt.target.value.trim();
38 | actions.doSearch(val);
39 | },
40 |
41 | //
42 | noop: function(){
43 | }
44 |
45 | });
46 |
47 | module.exports = Header;
--------------------------------------------------------------------------------
/app/js/views/InputBox.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | */
4 | var shortId = require('shortid');
5 | var actions = require('../actions/AppActionCreator');
6 |
7 | /**
8 | *
9 | */
10 | var comp = React.createClass({
11 |
12 | componentDidMount: function(){
13 | this.$input = $('#todo-input');
14 | },
15 |
16 | /**
17 | * supported events
18 | * http://facebook.github.io/react/docs/events.html
19 | */
20 | render: function() {
21 |
22 | return (
23 |
24 |
25 |
26 |
33 |
34 |
35 |
36 |
37 | );
38 |
39 | },
40 |
41 |
42 | /**
43 | * 按下 enter 就存檔
44 | */
45 | handleKeyDown: function(evt){
46 | if( evt.keyCode == 13){
47 | this.handleSave();
48 | }
49 | },
50 |
51 | /**
52 | * 按下 save 鈕就存檔
53 | */
54 | handleSave: function(evt){
55 |
56 | var val = this.$input.val();
57 |
58 | // 未輸入文字的話就擋掉
59 | if( val.trim().length == 0 ) return;
60 |
61 | var item = {};
62 | item.name = val;
63 | item.uid = shortId.generate();
64 | item.created = Date.now();
65 |
66 | actions.createTodo( item );
67 |
68 | // 清空輸入框,等待下一次的輸入
69 | this.$input.val('');
70 | },
71 |
72 | noop: function(){}
73 |
74 | });
75 |
76 | module.exports = comp;
--------------------------------------------------------------------------------
/app/js/views/List.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | */
4 | var actions = require('../actions/AppActionCreator');
5 | var ListItem = React.createFactory(require('./ListItem.jsx'));
6 |
7 | /**
8 | *
9 | */
10 | var comp = React.createClass({
11 |
12 | /**
13 | * 新增的 item 會在 list 最底部,有可能超出捲動範圍而看不到,
14 | * 因此要自動往下捲使其可視
15 | * 但同時也要避免一般的點選行為出現亂跳現象,因此要精準判斷該物件是否需要被捲動
16 | */
17 | componentDidUpdate: function(){
18 |
19 | // 下面都是在操作 DOM api,因此一開始就不用 jquery 選取,會便宜一些
20 | var elem = document.querySelector('.todo-list .selected');
21 |
22 | // 當前沒有選取任何項目就不繼續了
23 | if(!elem) return;
24 |
25 | var parent = elem.parentElement;
26 |
27 | // 10 是安全區間,避免有時判斷失靈導致該顯示而沒捲動
28 | if( elem.getBoundingClientRect().top - parent.getBoundingClientRect().bottom > -10 ){
29 | elem.scrollIntoView();
30 | }
31 |
32 | },
33 |
34 | /**
35 | *
36 | */
37 | render: function() {
38 |
39 | var arrTodos = this.props.truth.arrTodos;
40 | var filterStr = this.props.truth.filter;
41 |
42 | // 接著針對 arr 做 filter() 與 map() 兩段處理
43 | var arr = arrTodos
44 |
45 | // 先依隨打即查關鍵字過濾
46 | .filter(function(item){
47 | return item.name.indexOf(filterStr) != -1;
48 | })
49 |
50 | // 再將合格的項目轉成 元件供顯示
51 | .map(function(item){
52 |
53 | //
54 | return
62 |
63 | }, this)
64 |
65 | // 當上面這段跑完時,arr[] 的內容會就是一包 元件
66 | // 下面就可直接使用
67 |
68 | return (
69 |
70 |
71 | {arr}
72 |
73 | );
74 |
75 | },
76 |
77 | /**
78 | * 大部份 ui 操作最終都是直接轉手給 actions 去處理
79 | */
80 | handleClick: function( item ){
81 | // console.log( '\n\nitem click: ', item.name );
82 | actions.selectTodo(item);
83 | },
84 |
85 | /**
86 | *
87 | */
88 | handleRemove: function( item ){
89 | actions.removeTodo(item);
90 | },
91 |
92 | //
93 | noop: function(){}
94 |
95 | });
96 |
97 | module.exports = comp;
--------------------------------------------------------------------------------
/app/js/views/ListItem.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | */
4 | var actions = require('../actions/AppActionCreator');
5 | var cx = React.addons.classSet;
6 |
7 | /**
8 | *
9 | */
10 | var comp = React.createClass({
11 |
12 | /**
13 | * didMount 代表 react 元件已出現在 DOM 上,
14 | * 預先將後面常用到的 elem 選出來存著,避免將來重覆選取
15 | */
16 | componentDidMount: function(){
17 | this.$input = $(this.getDOMNode()).find('span').first();
18 | this.$remove = this.$input.next();
19 | },
20 |
21 | /**
22 | *
23 | */
24 | render: function() {
25 |
26 | // 這裏使用 react class add-on 來切換樣式顯示
27 | // 這樣做比較有條理,比直接組合多個字串來的好控制
28 | var classes = cx({
29 | 'list-item': true,
30 | 'selected': this.props.selected
31 | });
32 |
33 | return (
34 |
35 |
44 |
45 | {this.props.todoItem.name}
46 |
47 |
49 |
50 |
51 | );
52 |
53 | },
54 |
55 | /**
56 | * 在 listItem 上雙響時要切換為編輯模式
57 | * 手法是加上 contenteditable 屬性
58 | * 這也代表只支援 modern browsers
59 | */
60 | handleDblClick: function(){
61 |
62 | var val = null;
63 |
64 | // 加上這屬性就可編輯元件
65 | this.$input.attr('contenteditable', true);
66 |
67 | // 將 I-beam 放到文字最後方
68 | this.setCaret();
69 |
70 | // 編輯結束後的處理流程
71 | this.$input.on('keydown focusout', function(evt){
72 |
73 | // enter key 或 文字框喪失focus 事件發生,即認定為退出編輯狀態
74 | if( evt.keyCode == 13 || evt.type == 'focusout' ){
75 |
76 | evt.preventDefault();
77 |
78 | // 取得編輯後的新值
79 | val = this.$input.text();
80 |
81 | // 移除 的編輯能力
82 | this.$input.removeAttr('contenteditable');
83 | // 也解掉掛的偵聽
84 | this.$input.off('keydown focusout');
85 |
86 | // 準備將新值存入 store,方法一樣是操作 actionCreator
87 | // this.props.todoItem.name = val;
88 | actions.updateTodo( this.props.todoItem, val );
89 |
90 | }
91 | }.bind(this))
92 | },
93 |
94 | /**
95 | * util: 設定 I-beam 位置
96 | */
97 | setCaret: function() {
98 | var el = this.$input[0];
99 | var range = document.createRange();
100 | var sel = window.getSelection();
101 | range.setStart(el.childNodes[0], el.innerText.length);
102 | range.collapse(true);
103 | sel.removeAllRanges();
104 | sel.addRange(range);
105 | el.focus();
106 | },
107 |
108 |
109 | /**
110 | * ListItem 內部預先處理過刪除事件
111 | */
112 | handleRemove: function(evt){
113 |
114 | // 停止此事件繼續向上廣播,不然會連帶觸發另個 onClick 事件
115 | evt.stopPropagation();
116 |
117 | // 如果外界有傳入 onRemove handler,就觸發它,並且將自已身份也傳出去,方便外界識別與處理
118 | if( this.props.onRemove ){
119 | this.props.onRemove(this.props.todoItem);
120 | }
121 |
122 | },
123 |
124 | /**
125 | * 滑鼠移到一個 item 時要顯示 ✖ 鈕供刪除
126 | * 並且滑鼠移開時要隱藏
127 | */
128 | handleMouseMovement: function(evt){
129 | if( evt.type == 'mouseover'){
130 | this.$remove.removeClass('hide')
131 | }else{
132 | this.$remove.addClass('hide')
133 | }
134 | },
135 |
136 | noop: function(){}
137 |
138 | });
139 |
140 | module.exports = comp;
--------------------------------------------------------------------------------
/app/js/views/MainApp.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 這是 root view,也稱為 controller-view
3 | */
4 |
5 |
6 | //========================================================================
7 | //
8 | // import
9 |
10 | // var React = require('react');
11 | var Header = React.createFactory( require('./Header.jsx') );
12 | var Footer = React.createFactory( require('./Footer.jsx') );
13 | var InputBox = React.createFactory( require('./InputBox.jsx') );
14 | var List = React.createFactory( require('./List.jsx') );
15 | var Detail = React.createFactory( require('./Detail.jsx') );
16 |
17 | var TodoStore = require('../stores/TodoStore');
18 | var AppConstants = require('../constants/AppConstants');
19 |
20 | var idResize;
21 |
22 | /**
23 | *
24 | */
25 | var MainApp = React.createClass({
26 |
27 | //========================================================================
28 | //
29 | // mount
30 |
31 | /**
32 | * 這是 component API, 在 mount 前會跑一次,取值做為 this.state 的預設值
33 | */
34 | getInitialState: function() {
35 | var o = this.getTruth();
36 | o.screenSize = 'tablet'
37 | return o;
38 | },
39 |
40 | /**
41 | * 主程式進入點
42 | */
43 | componentWillMount: function() {
44 | TodoStore.addListener( AppConstants.CHANGE_EVENT, this._onChange );
45 |
46 | // 要用 interval 擋一下
47 | window.addEventListener('resize', this.handleResize );
48 |
49 | this.handleResize();
50 | },
51 |
52 | handleResize: function(evt){
53 |
54 | clearTimeout( idResize );
55 |
56 | idResize = setTimeout(function(){
57 |
58 | var body = document.body;
59 | var size;
60 |
61 | // @todo: 改回 1024
62 | if(body.scrollWidth > 720){
63 | size = 'desktop';
64 | }else if(body.scrollWidth > 480){
65 | size = 'tablet';
66 | }else{
67 | size = 'phone';
68 | }
69 |
70 | // console.log( 'resize: ', body.scrollWidth, body.scrollHeight, ' >size: ', size );
71 |
72 | this.setState({screenSize: size});
73 |
74 | }.bind(this), 0)
75 |
76 | },
77 |
78 | /**
79 | * 重要:root view 建立後第一件事,就是偵聽 store 的 change 事件
80 | */
81 | componentDidMount: function() {
82 | //
83 | },
84 |
85 | //========================================================================
86 | //
87 | // unmount
88 |
89 | /**
90 | * 元件將從畫面上移除時,要做善後工作
91 | */
92 | componentWillUnmount: function() {
93 | TodoStore.removeChangeListener( this._onChange );
94 | },
95 |
96 | /**
97 | *
98 | */
99 | componentDidUnmount: function() {
100 | //
101 | },
102 |
103 | //========================================================================
104 | //
105 | // update
106 |
107 | /**
108 | * 在 render() 前執行,有機會可先處理 props 後用 setState() 存起來
109 | */
110 | componentWillReceiveProps: function(nextProps) {
111 | //
112 | },
113 |
114 | /**
115 | *
116 | */
117 | shouldComponentUpdate: function(nextProps, nextState) {
118 | return true;
119 | },
120 |
121 | // 這時已不可用 setState()
122 | componentWillUpdate: function(nextProps, nextState) {
123 | },
124 |
125 | /**
126 | *
127 | */
128 | componentDidUpdate: function(prevProps, prevState) {
129 | },
130 |
131 | //========================================================================
132 | //
133 | // render
134 |
135 | /**
136 | *
137 | */
138 | render: function() {
139 |
140 | var size = this.state.screenSize;
141 | // console.log( 'size: ', size );
142 |
143 | if( size == 'phone' ){
144 |
145 | // phone
146 | return (
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 | )
160 |
161 | }else if( size == 'tablet'){
162 |
163 | // tablet
164 | return (
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 | )
178 |
179 | }else{
180 |
181 | // desktop
182 | return (
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 | )
197 | }
198 | },
199 |
200 |
201 |
202 | //========================================================================
203 | //
204 | // private methods - 處理元件內部的事件
205 |
206 | /**
207 | * controller-view 偵聽到 model change 後
208 | * 執行這支,它操作另一支 private method 去跟 model 取最新值
209 | * 然後操作 component life cycle 的 setState() 將新值灌入元件體系
210 | * 就會觸發一連串 child components 跟著重繪
211 | */
212 | _onChange: function(){
213 | // 重要:從 root view 觸發所有 sub-view 重繪
214 | this.setState( this.getTruth() );
215 | },
216 |
217 | /**
218 | * 為何要獨立寫一支?因為會有兩個地方會用到,因此抽出來
219 | * 目地:向各個 store 取回資料,然後統一 setState() 再一層層往下傳遞
220 | */
221 | getTruth: function() {
222 | // 是從 TodoStore 取資料(as the single source of truth)
223 | return TodoStore.getAll();
224 | }
225 |
226 |
227 | });
228 |
229 | module.exports = MainApp;
230 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var browserify = require('browserify');
3 | var source = require('vinyl-source-stream');
4 | var minifyCSS = require('gulp-minify-css');
5 | var livereload = require('gulp-livereload');
6 | var notify = require('gulp-notify');
7 | var fs = require('fs');
8 |
9 | // 環境變數
10 | var env = 'prod'; // dev||prod
11 |
12 | var live = livereload();
13 | livereload.listen();
14 |
15 | // 路徑變數
16 | var paths = {
17 | main: './app/js/boot.js',
18 | css: './app/assets/css/*.css',
19 | destDir: 'build',
20 | destCSS: 'build/assets/css'
21 | };
22 |
23 | /**
24 | *
25 | */
26 | gulp.task('bundle-js', function() {
27 |
28 | // console.log( '\nbundle-js 跑' );
29 |
30 | return browserify({
31 | entries:[ paths.main ]
32 | })
33 |
34 | // 最優先編譯 jsx,確保後面其它 transform 運行無誤
35 | .transform( 'reactify' )
36 |
37 | // 所有檔案合併為一,並指定要生成 source map
38 | .bundle({debug: true})
39 |
40 | .on('error', function( err ){
41 | console.log( '[錯誤]', err );
42 | this.end();
43 | gulp.src('').pipe( notify('✖ Bunlde Failed ✖') )
44 | })
45 |
46 | // 利用 vinyl-source-stream 幫檔案取名字
47 | .pipe( source('bundle.js') )
48 |
49 | // 接著就回到 gulp 系統做剩下事
50 | // 這裏是直接存檔到硬碟
51 | .pipe( gulp.dest('./build') )
52 |
53 | });
54 |
55 | /**
56 | * 縮短 app.css
57 | */
58 | gulp.task('minify-css', function() {
59 | gulp.src( paths.css )
60 | .pipe(minifyCSS(
61 | {
62 | noAdvanced: false,
63 | keepBreaks:true,
64 | cache: true // 這是 gulp 插件獨有的
65 | }))
66 | .pipe(gulp.dest( paths.destCSS ))
67 | });
68 |
69 |
70 | /**
71 | * 將 index.html 與 css/ 複製到 build/ 下面
72 | * 才方便測試
73 | */
74 | gulp.task('copy', function(){
75 | return gulp.src([ 'app/index.html' ], { base: 'app' } )
76 | .pipe( gulp.dest(paths.destDir));
77 | })
78 |
79 |
80 | /**
81 | * 監控 app/ 下所有 js, jsx, html, css 變化就重新編譯
82 | */
83 | gulp.task('watch', function() {
84 | // console.log( 'watch 跑' );
85 |
86 | gulp.watch( 'app/**/*', ['bundle-js', 'minify-css', 'copy', 'refresh'] );
87 | });
88 |
89 | /**
90 | * livereload refresh
91 | */
92 | gulp.task( 'refresh', function(){
93 | // console.log( '\nlivereload > refresh\n' );
94 | setTimeout(function(){
95 | live.changed('');
96 | }, 500)
97 | })
98 |
99 |
100 | //========================================================================
101 | //
102 | // 總成的指令集
103 |
104 |
105 | /**
106 | * 初期讓 default 就是跑 dev task,將來可能會改成有 build, deploy 等花樣
107 | */
108 | gulp.task('default', ['dev']);
109 |
110 | /**
111 | * 編譯與打包 jsx 為一張檔案
112 | * 廣播 livereload 事件
113 | * 啟動 8000 server 供本地跑
114 | */
115 | gulp.task('dev', ['bundle-js', 'minify-css', 'copy', 'watch'] );
116 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Meetup",
3 | "version": "1.0.0",
4 | "description": "Sample application for meetup",
5 | "main": "js/app.js",
6 | "dependencies": {
7 | "debug": "^2.1.0",
8 | "es6-promise": "~0.1.1",
9 | "flux": "^2.0.1",
10 | "gulp-livereload": "^2.1.1",
11 | "gulp-minify-css": "^0.3.11",
12 | "gulp-notify": "^2.0.0",
13 | "shortid": "^2.0.1",
14 | "vinyl-source-stream": "^1.0.0"
15 | },
16 | "devDependencies": {
17 | "browserify": "^4.2.0",
18 | "connect": "^2.17.1",
19 | "gulp": "^3.8.5",
20 | "reactify": "^0.13.1"
21 | },
22 | "scripts": {
23 | "start": "gulp",
24 | "test": "echo \"Error: no test specified\" && exit 1"
25 | },
26 | "author": "Jeremy Lu",
27 | "license": "MIT"
28 | }
29 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## 執行方式
4 |
5 | `npm install` 安裝所有套件
6 |
7 | `node server` 啟動 local web server
8 |
9 | `npm start` 開始 bundle + watch
10 |
11 | `http://localhost:8000/` 即可看到畫面
12 |
13 | ## 課程講義
14 |
15 | http://goo.gl/625c0e
16 |
17 | ## 線上試用
18 |
19 | http://coodoo.github.io/react-meetup-1/
20 |
21 | ## Branch 結構
22 |
23 | - Master 內是最終成品,包含所有功能與修正
24 |
25 | - e0 是最基本的 CRUD 功能,課程將從此 branch 開始介紹
26 |
27 | - e1 - e4 是以 e0 為基礎而逐步新增的功能
28 |
29 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | var connect = require('connect'),
2 | http = require('http'),
3 | fs = require('fs'),
4 | app;
5 |
6 | // 建 server
7 | app = connect()
8 |
9 | // 指定供應 static file 的路徑
10 | // request 進來,先去 static 目錄找,沒有符合的才進入下一個 middleware
11 | .use( connect.static('build') )
12 |
13 | .use( function(req, res, next ){
14 | fs.readFile('./build/index.html', function(err, data){
15 | res.write(data);
16 | res.end();
17 | })
18 | })
19 |
20 | // 啟動 server 在 8000
21 | http.createServer(app).listen(8000, function() {
22 | console.log('Running on http://localhost:8000');
23 | });
--------------------------------------------------------------------------------