├── README.md ├── dnd ├── README.md ├── index.html ├── react-dnd.js ├── run_server.sh ├── screenshot.png ├── scripts.js └── style.css ├── kanban ├── README.md ├── img │ ├── kanban.png │ └── structure.png ├── index.html ├── react-dnd.js ├── requirements.txt ├── scripts.js ├── server.py └── style.css ├── mailbox ├── README.md ├── img │ ├── screenshot.png │ └── structure.png ├── index.html ├── run_server.sh ├── scripts.js └── style.css ├── modal ├── README.md ├── index.html ├── run_server.sh ├── scripts.js └── style.css └── nuclear-test ├── .gitignore ├── build └── index.html ├── client ├── actions.js ├── components │ ├── app.js │ ├── contact.js │ ├── contacts.js │ ├── index.js │ └── list.js ├── getters.js ├── index.js ├── pages │ ├── contact.js │ ├── contacts.js │ ├── favorites.js │ ├── index.js │ └── newcontact.js ├── reactor.js ├── routes.js ├── stores │ ├── contacts.js │ ├── index.js │ └── messages.js └── stylesheets │ └── app.css ├── db.json ├── package.json ├── readme.md ├── run.sh └── webpack.config.js /README.md: -------------------------------------------------------------------------------- 1 | # React Examples 2 | 3 | Examples of working with [React][react]. 4 | 5 | # Examples 6 | 7 | Unless explicitly stated, all of these can be run by running `./run_server.sh` 8 | on their directory. 9 | 10 | ## [Mailbox][mailbox] 11 | 12 | [Online version](http://tryolabs.github.io/react-examples/mailbox/) 13 | 14 | A static email client, this is basically the simplest React app that implements 15 | callbacks and event handlers. 16 | 17 | ## [Modal][modal] 18 | 19 | [Online version](http://tryolabs.github.io/react-examples/modal/) 20 | 21 | A reusable modal component. 22 | 23 | ## [Drag and Drop][dnd] 24 | 25 | [Online version](http://tryolabs.github.io/react-examples/dnd/) 26 | 27 | A tutorial on drag-and-drop in React. 28 | 29 | ## [Kanban][kanban] 30 | 31 | A simple Kanban app with a Python server for persistence. 32 | 33 | To run, ensure [Flask][flask] is installed and run `python server.py`. 34 | 35 | [react]: https://facebook.github.io/react/ 36 | [flask]: http://flask.pocoo.org/ 37 | [mailbox]: https://github.com/tryolabs/react-examples/tree/master/mailbox#readme 38 | [modal]: https://github.com/tryolabs/react-examples/tree/master/modal#readme 39 | [dnd]: https://github.com/tryolabs/react-examples/tree/master/dnd#readme 40 | [kanban]: https://github.com/tryolabs/react-examples/tree/master/kanban#readme 41 | 42 | # License 43 | 44 | Copyright (c) 2015 Tryolabs SRL 45 | 46 | Licensed under the MIT License. 47 | -------------------------------------------------------------------------------- /dnd/README.md: -------------------------------------------------------------------------------- 1 | # Drag and Drop 2 | 3 | ![Screenshot](screenshot.png) 4 | 5 | This is an example of implementing drag and drop functionality using React. This 6 | is the first example where we'll use an external library, in this case, 7 | [React DnD][dnd]. 8 | 9 | [dnd]: https://github.com/gaearon/react-dnd 10 | 11 | ## Components 12 | 13 | This example will have two major components: `Bin`, a component where objects 14 | can be dropped, and `Item`, an item that can be dragged and dropped. 15 | 16 | React DND, as you would expect, supports applying different functionality to 17 | different sets of data that is to be dragged and dropped. We declare the 18 | different types of item in the `ItemTypes` object, which is simple since in this 19 | example we only have one type of draggable item: 20 | 21 | ```js 22 | const ItemTypes = { 23 | ITEM: 'item' 24 | }; 25 | ``` 26 | 27 | First, we define the `Item` component, and declare it draggable. Because React 28 | DND, like the underlying HTML5 drag-and-drop API, supports dragging *data* as 29 | well as visible objects, we have to decalare what kind of data should be moved 30 | along with the component. The `beginDrag` function does it, and in this case we 31 | only carry the component's name, since it's the only data it has. 32 | 33 | ```js 34 | var Item = React.createClass({ 35 | mixins: [DragDropMixin], 36 | 37 | statics: { 38 | configureDragDrop: function(register) { 39 | register(ItemTypes.ITEM, { 40 | dragSource: { 41 | beginDrag: function(component) { 42 | return { 43 | item: { 44 | name: component.props.name 45 | } 46 | }; 47 | } 48 | } 49 | }); 50 | } 51 | }, 52 | 53 | render: function () { 54 | return ( 55 |
  • 57 | {this.props.name} 58 |
  • 59 | ); 60 | } 61 | }); 62 | ``` 63 | 64 | Next we define the component we can drop objects into. The `Bin` component has a 65 | list of dropped items in its state, and an `addItem` method which takes the name 66 | of an item to add to that list. 67 | 68 | ```js 69 | var Bin = React.createClass({ 70 | mixins: [DragDropMixin], 71 | 72 | getInitialState: function() { 73 | return { items: [] }; 74 | }, 75 | 76 | addItem: function(name) { 77 | clone = this.state.items.slice(0); 78 | clone.push(name); 79 | this.setState({ items: clone }); 80 | }, 81 | ``` 82 | 83 | Now we register the `dropTarget` function, which calls `addItem` when an object 84 | is dropped into the bin: 85 | 86 | ```js 87 | statics: { 88 | configureDragDrop: function(register) { 89 | register(ItemTypes.ITEM, { 90 | dropTarget: { 91 | acceptDrop: function(component, item) { 92 | component.addItem(item.name); 93 | } 94 | } 95 | }); 96 | } 97 | }, 98 | ``` 99 | 100 | Here we have the render function. We look at the component's drop state to see whether: 101 | 102 | * Nothing is happening. 103 | * A droppable object is being dragged. 104 | * A droppable object has been dragged over the component. 105 | 106 | We use this to change the component's class name, then later we'll use CSS to 107 | style it. 108 | 109 | We also query the component's drop state to determine whether the text on the 110 | bin should read "Release to drop: or "Drag item here". 111 | 112 | ```js 113 | render: function() { 114 | const dropState = this.getDropState(ItemTypes.ITEM); 115 | 116 | var stateClass = 'none'; 117 | if (dropState.isHovering) { 118 | stateClass = 'hovering'; 119 | } else if (dropState.isDragging) { 120 | stateClass = 'dragging'; 121 | } 122 | 123 | const dropped = this.state.items.map(function(name) { 124 | return
  • {name}
  • ; 125 | }); 126 | 127 | return ( 128 |
    130 | {dropState.isHovering ? 131 | 'Release to drop' : 132 | 'Drag item here'} 133 | 136 |
    137 | ); 138 | } 139 | }); 140 | ``` 141 | 142 | Finally, we create a small container object for this example and add some 143 | example items: 144 | 145 | ```js 146 | var Container = React.createClass({ 147 | render: function() { 148 | return ( 149 |
    150 | 151 | 156 |
    157 | ); 158 | } 159 | }); 160 | 161 | React.render( 162 | , 163 | document.body 164 | ); 165 | ``` 166 | 167 | ## Style 168 | 169 | Now it's time to add some CSS. 170 | 171 | First, some general style: 172 | 173 | ```css 174 | body { 175 | margin: 0; 176 | padding: 0; 177 | min-height: 100vh; 178 | 179 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 180 | } 181 | ``` 182 | 183 | Now we make the bin a big square in the middle: 184 | 185 | ```css 186 | .bin { 187 | width: 300px; 188 | margin: 35px auto; 189 | padding: 45px; 190 | 191 | color: white; 192 | font-size: 2em; 193 | text-align: center; 194 | 195 | border-radius: 5px; 196 | } 197 | ``` 198 | 199 | We used CSS classes to mark up the different states of the bin, so now let's use 200 | those to change the color of the bin to match the state: 201 | 202 | ```css 203 | .bin-state-none { 204 | background-color: #34495E; 205 | } 206 | 207 | .bin-state-dragging { 208 | background-color: #E98B39; 209 | } 210 | 211 | .bin-state-hovering { 212 | background-color: #2ECC71; 213 | } 214 | ``` 215 | 216 | Since we show a list of items that have been dropped inside the bin, let's style 217 | that: 218 | 219 | ```css 220 | .dropped { 221 | margin: 20px 0; 222 | padding: 0; 223 | } 224 | 225 | .dropped li { 226 | list-style-type: none; 227 | font-size: 0.6em; 228 | } 229 | ``` 230 | 231 | And finally, the items that can be dropped: 232 | 233 | ```css 234 | .items { 235 | padding: 0; 236 | text-align: center; 237 | } 238 | 239 | .item { 240 | display: inline-block; 241 | padding: 20px; 242 | margin: 25px 10px; 243 | 244 | border: 2px solid #E74C3C; 245 | } 246 | ``` 247 | -------------------------------------------------------------------------------- /dnd/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Drag and Drop 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /dnd/react-dnd.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):"object"==typeof exports?exports.ReactDND=e():t.ReactDND=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var i=n[r]={exports:{},id:r,loaded:!1};return t[r].call(i.exports,i,i.exports,e),i.loaded=!0,i.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";var r=n(32),i=r.HTML5,o=n(39);t.exports={DragDropMixin:o(i),ImagePreloaderMixin:n(34),DragLayerMixin:n(33),HorizontalDragAnchors:n(19),VerticalDragAnchors:n(20),NativeDragItemTypes:n(8),DropEffects:n(7)}},function(t){function e(t){return"number"==typeof t&&t>-1&&t%1==0&&n>=t}var n=Math.pow(2,53)-1;t.exports=e},function(t){function e(t){var e=typeof t;return"function"==e||t&&"object"==e||!1}t.exports=e},function(t,e,n){function r(t){return null==t?!1:f.call(t)==s?l.test(u.call(t)):o(t)&&a.test(t)||!1}var i=n(73),o=n(4),s="[object Function]",a=/^\[object .+?Constructor\]$/,c=Object.prototype,u=Function.prototype.toString,f=c.toString,l=RegExp("^"+i(f).replace(/toString|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");t.exports=r},function(t){function e(t){return t&&"object"==typeof t||!1}t.exports=e},function(t,e,n){var r=n(1),i=n(3),o=n(4),s="[object Array]",a=Object.prototype,c=a.toString,u=i(u=Array.isArray)&&u,f=u||function(t){return o(t)&&r(t.length)&&c.call(t)==s||!1};t.exports=f},function(t,e,n){"use strict";var r=n(17),i=r({DRAG_START:null,DRAG_END:null,DRAG:null,DROP:null});t.exports=i},function(t){"use strict";var e={COPY:"copy",MOVE:"move",LINK:"link"};t.exports=e},function(t){"use strict";var e={FILE:"__NATIVE_FILE__",URL:"__NATIVE_URL__"};t.exports=e},function(t,e,n){"use strict";var r=n(48).Dispatcher,i=n(15),o=i(new r,{handleAction:function(t){this.dispatch({action:t})}});t.exports=o},function(t,e,n){"use strict";var r=n(9),i=n(6),o=n(22),s=null,a=null,c=null,u=o({getInitialOffsetFromContainer:function(){return s},getInitialOffsetFromClient:function(){return a},getCurrentOffsetFromClient:function(){return c}});u.dispatchToken=r.register(function(t){var e=t.action;switch(e.type){case i.DRAG_START:s=e.offsetFromContainer,a=e.offsetFromClient,c=e.offsetFromClient,u.emitChange();break;case i.DRAG:c=e.offsetFromClient,u.emitChange();break;case i.DRAG_END:s=null,a=null,c=null,u.emitChange()}}),t.exports=u},function(t,e,n){"use strict";var r=n(9),i=n(6),o=n(10),s=n(22),a=null,c=null,u=null,f=null,l=s({isDragging:function(){return!!a},getEffectsAllowed:function(){return u},getDropEffect:function(){return f},getDraggedItem:function(){return a},getDraggedItemType:function(){return c}});l.dispatchToken=r.register(function(t){r.waitFor([o.dispatchToken]);var e=t.action;switch(e.type){case i.DRAG_START:f=null,a=e.item,c=e.itemType,u=e.effectsAllowed,l.emitChange();break;case i.DROP:f=e.dropEffect,l.emitChange();break;case i.DRAG_END:a=null,c=null,u=null,f=null,l.emitChange()}}),t.exports=l},function(t){"use strict";function e(){return!!window.safari}t.exports=e},function(t){function e(t,e){return t=+t,e=null==e?n:e,t>-1&&t%1==0&&e>t}var n=Math.pow(2,53)-1;t.exports=e},function(t,e,n){function r(t){var e=o(t)?t.length:void 0;return i(e)&&c.call(t)==s||!1}var i=n(1),o=n(4),s="[object Arguments]",a=Object.prototype,c=a.toString;t.exports=r},function(t){function e(t){if(null==t)throw new TypeError("Object.assign target cannot be null or undefined");for(var e=Object(t),n=Object.prototype.hasOwnProperty,r=1;r-1},getStateForDragDropMixin:function(){return{draggedItem:c.getDraggedItem(),draggedItemType:c.getDraggedItemType()}},getDragState:function(t){return r(this,t),i(this,t),{isDragging:this.state.ownDraggedItemType===t}},getDropState:function(t){r(this,t),o(this,t);var e=this.getActiveDropTargetType()===t,n=!!this.state.currentDropEffect;return{isDragging:e,isHovering:e&&n}},componentWillMount:function(){this._monitor=new f,this._dragSources={},this._dropTargets={},v(this.constructor.configureDragDrop,"%s must implement static configureDragDrop(register, context) to use DragDropMixin",this.constructor.displayName),this.constructor.configureDragDrop(this.registerDragDropItemTypeHandlers,u)},componentDidMount:function(){e(this),c.addChangeListener(this.handleStoreChangeInDragDropMixin)},componentWillUnmount:function(){n(this),c.removeChangeListener(this.handleStoreChangeInDragDropMixin)},registerDragDropItemTypeHandlers:function(t,e){r(this,t);var n=e.dragSource,i=e.dropTarget;n&&(v(!this._dragSources[t],"Drag source for %s specified twice. See configureDragDrop in %s",t,this.constructor.displayName),this._dragSources[t]=D(n,g)),i&&(v(!this._dropTargets[t],"Drop target for %s specified twice. See configureDragDrop in %s",t,this.constructor.displayName),this._dropTargets[t]=D(i,p))},handleStoreChangeInDragDropMixin:function(){this.isMounted()&&this.setState(this.getStateForDragDropMixin())},dragSourceFor:function(e){return r(this,e),i(this,e),t.getDragSourceProps(this,e)},handleDragStart:function(e,n){var r=this,i=this._dragSources[e],o=i.canDrag,s=i.beginDrag;if(!c.isDragging()&&o(this)){var u,f=s(this),g=f.item,p=f.dragPreview,h=f.dragAnchors,d=f.effectsAllowed,m=this.getDOMNode(),D=m.getBoundingClientRect(),_=t.getOffsetFromClient(this,n);u={x:_.x-D.left,y:_.y-D.top},p||(p=m),d||(d=[l.MOVE]),v(y(d)&&d.length>0,"Expected effectsAllowed to be non-empty array"),v(x(g),'Expected return value of beginDrag to contain "item" object'),t.beginDrag(this,n,m,p,h,u,d),a.startDragging(e,g,d,_,u),setTimeout(function(){r.isMounted()&&c.getDraggedItem()===g&&r.setState({ownDraggedItemType:e})})}},handleDragEnd:function(e){t.endDrag(this);var n=this._dragSources[e].endDrag,r=c.getDropEffect();a.endDragging(),this.isMounted()&&this.setState({ownDraggedItemType:null}),n(this,r)},dropTargetFor:function(){for(var e=this,n=arguments.length,i=Array(n),s=0;n>s;s++)i[s]=arguments[s];return i.forEach(function(t){r(e,t),o(e,t)}),t.getDropTargetProps(this,i)},handleDragEnter:function(t,e){if(this.isAnyDropTargetActive(t)&&this._monitor.enter(e.target)){e.preventDefault();var n=this._dropTargets[this.state.draggedItemType],r=n.enter,i=n.getDropEffect,o=c.getEffectsAllowed(),s=c.getDraggedItemType();d(s)&&(o=[l.COPY]);var a=i(this,o);a&&v(o.indexOf(a)>-1,"Effect %s supplied by drop target is not one of the effects allowed by drag source: %s",a,o.join(", ")),this.setState({currentDropEffect:a}),r(this,this.state.draggedItem)}},handleDragOver:function(e,n){if(this.isAnyDropTargetActive(e)){n.preventDefault();var r=this._dropTargets[this.state.draggedItemType].over;r(this,this.state.draggedItem),t.dragOver(this,n,this.state.currentDropEffect||"move")}},handleDragLeave:function(t,e){if(this.isAnyDropTargetActive(t)&&this._monitor.leave(e.target)){this.setState({currentDropEffect:null});var n=this._dropTargets[this.state.draggedItemType].leave;n(this,this.state.draggedItem)}},handleDrop:function(t,e){if(this.isAnyDropTargetActive(t)){e.preventDefault();var n=this.state.draggedItem,r=this._dropTargets[this.state.draggedItemType].acceptDrop,i=this.state.currentDropEffect,o=!!c.getDropEffect();n||(n=h(e)),this._monitor.reset(),o||a.recordDrop(i),this.setState({currentDropEffect:null}),r(this,n,o,c.getDropEffect())}}}}var a=n(18),c=n(11),u=n(37),f=n(21),l=n(7),g=n(35),p=n(36),h=n(41),d=n(24),v=n(16),m=n(15),D=n(70),y=n(5),x=n(2);t.exports=s},function(t){"use strict";function e(t,e){return-1!==t.indexOf(e,t.length-e.length)}t.exports=e},function(t,e,n){"use strict";function r(t){return i(t)?{files:Array.prototype.slice.call(t.dataTransfer.files)}:o(t)?{urls:(t.dataTransfer.getData("Url")||t.dataTransfer.getData("text/uri-list")||"").split("\n")}:void 0}var i=n(23),o=n(25);t.exports=r},function(t,e,n){"use strict";function r(t){var e=t.indexOf(i.COPY)>-1,n=t.indexOf(i.MOVE)>-1,r=t.indexOf(i.LINK)>-1;return e&&n&&r?"all":e&&n?"copyMove":r&&n?"linkMove":e&&r?"copyLink":e?"copy":n?"move":r?"link":"none"}var i=n(7);t.exports=r},function(t,e,n){"use strict";function r(t,e,n,r){n=n||{};var a=t.offsetWidth,c=t.offsetHeight,u=e instanceof Image,f=u?e.width:a,l=u?e.height:c,g=n.horizontal||i.CENTER,p=n.vertical||o.CENTER,h=r.x,d=r.y;switch(s()&&(l/=window.devicePixelRatio,f/=window.devicePixelRatio),g){case i.LEFT:break;case i.CENTER:h*=f/a;break;case i.RIGHT:h=f-f*(1-h/a)}switch(p){case o.TOP:break;case o.CENTER:d*=l/c;break;case o.BOTTOM:d=l-l*(1-d/c)}return s()&&(d+=(window.devicePixelRatio-1)*l),{x:h,y:d}}var i=n(19),o=n(20),s=n(12);t.exports=r},function(t,e,n){"use strict";function r(){return i()||o()?window.devicePixelRatio:1}var i=n(45),o=n(12);t.exports=r},function(t){"use strict";function e(){return/firefox/i.test(navigator.userAgent)}t.exports=e},function(t){"use strict";function e(){return"WebkitAppearance"in document.documentElement.style}t.exports=e},function(t,e,n){"use strict";function r(t){return t?i()&&t instanceof Image&&o(t.src,".gif")?!1:!0:!1}var i=n(12),o=n(40);t.exports=r},function(t,e,n){t.exports.Dispatcher=n(49)},function(t,e,n){"use strict";function r(){this.$Dispatcher_callbacks={},this.$Dispatcher_isPending={},this.$Dispatcher_isHandled={},this.$Dispatcher_isDispatching=!1,this.$Dispatcher_pendingPayload=null}var i=n(50),o=1,s="ID_";r.prototype.register=function(t){var e=s+o++;return this.$Dispatcher_callbacks[e]=t,e},r.prototype.unregister=function(t){i(this.$Dispatcher_callbacks[t],"Dispatcher.unregister(...): `%s` does not map to a registered callback.",t),delete this.$Dispatcher_callbacks[t]},r.prototype.waitFor=function(t){i(this.$Dispatcher_isDispatching,"Dispatcher.waitFor(...): Must be invoked while dispatching.");for(var e=0;e=200?s(e):null,l=e.length;f&&(c=o,u=!1,e=f);t:for(;++ae&&(e=-e>i?0:i+e),n="undefined"==typeof n||n>i?i:+n||0,0>n&&(n+=i),i=e>n?0:n-e>>>0,e>>>=0;for(var o=Array(i);++r=200,f=u?s():null,l=[];f?(r=o,c=!1):(u=!1,f=e?[]:l);t:for(;++nn||null==r)return r;var s=e[n-2],a=e[n-1],c=e[3];n>3&&"function"==typeof s?(s=i(s,a,5),n-=2):(s=n>2&&"function"==typeof a?a:null,n-=s?1:0),c&&o(e[1],e[2],c)&&(s=3==n?null:s,n=2);for(var u=0;++u0;++rt||isNaN(t))throw TypeError("n must be a positive number");return this._maxListeners=t,this},e.prototype.emit=function(t){var e,r,s,a,c,u;if(this._events||(this._events={}),"error"===t&&(!this._events.error||i(this._events.error)&&!this._events.error.length)){if(e=arguments[1],e instanceof Error)throw e;throw TypeError('Uncaught, unspecified "error" event.')}if(r=this._events[t],o(r))return!1;if(n(r))switch(arguments.length){case 1:r.call(this);break;case 2:r.call(this,arguments[1]);break;case 3:r.call(this,arguments[1],arguments[2]);break;default:for(s=arguments.length,a=new Array(s-1),c=1;s>c;c++)a[c-1]=arguments[c];r.apply(this,a)}else if(i(r)){for(s=arguments.length,a=new Array(s-1),c=1;s>c;c++)a[c-1]=arguments[c];for(u=r.slice(),s=u.length,c=0;s>c;c++)u[c].apply(this,a)}return!0},e.prototype.addListener=function(t,r){var s;if(!n(r))throw TypeError("listener must be a function");if(this._events||(this._events={}),this._events.newListener&&this.emit("newListener",t,n(r.listener)?r.listener:r),this._events[t]?i(this._events[t])?this._events[t].push(r):this._events[t]=[this._events[t],r]:this._events[t]=r,i(this._events[t])&&!this._events[t].warned){var s;s=o(this._maxListeners)?e.defaultMaxListeners:this._maxListeners,s&&s>0&&this._events[t].length>s&&(this._events[t].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[t].length),"function"==typeof console.trace&&console.trace())}return this},e.prototype.on=e.prototype.addListener,e.prototype.once=function(t,e){function r(){this.removeListener(t,r),i||(i=!0,e.apply(this,arguments))}if(!n(e))throw TypeError("listener must be a function");var i=!1;return r.listener=e,this.on(t,r),this},e.prototype.removeListener=function(t,e){var r,o,s,a;if(!n(e))throw TypeError("listener must be a function");if(!this._events||!this._events[t])return this;if(r=this._events[t],s=r.length,o=-1,r===e||n(r.listener)&&r.listener===e)delete this._events[t],this._events.removeListener&&this.emit("removeListener",t,e);else if(i(r)){for(a=s;a-->0;)if(r[a]===e||r[a].listener&&r[a].listener===e){o=a;break}if(0>o)return this;1===r.length?(r.length=0,delete this._events[t]):r.splice(o,1),this._events.removeListener&&this.emit("removeListener",t,e)}return this},e.prototype.removeAllListeners=function(t){var e,r;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[t]&&delete this._events[t],this;if(0===arguments.length){for(e in this._events)"removeListener"!==e&&this.removeAllListeners(e);return this.removeAllListeners("removeListener"),this._events={},this}if(r=this._events[t],n(r))this.removeListener(t,r);else for(;r.length;)this.removeListener(t,r[r.length-1]);return delete this._events[t],this},e.prototype.listeners=function(t){var e;return e=this._events&&this._events[t]?n(this._events[t])?[this._events[t]]:this._events[t].slice():[]},e.listenerCount=function(t,e){var r;return r=t._events&&t._events[e]?n(t._events[e])?1:t._events[e].length:0}}])}); -------------------------------------------------------------------------------- /dnd/run_server.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PY_VERSION=`python -c 'import sys; print("%i" % (sys.hexversion<0x03000000))'` 4 | 5 | if [ $PY_VERSION -eq 0 ]; then 6 | python -m http.server 7 | else 8 | python -m SimpleHTTPServer 9 | fi 10 | -------------------------------------------------------------------------------- /dnd/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/react-examples/362fa371bbcefcec133800a7df7028b418d2dc73/dnd/screenshot.png -------------------------------------------------------------------------------- /dnd/scripts.js: -------------------------------------------------------------------------------- 1 | var DragDropMixin = ReactDND.DragDropMixin; 2 | 3 | const ItemTypes = { 4 | ITEM: 'item' 5 | }; 6 | 7 | var Item = React.createClass({ 8 | mixins: [DragDropMixin], 9 | 10 | statics: { 11 | configureDragDrop: function(register) { 12 | register(ItemTypes.ITEM, { 13 | dragSource: { 14 | beginDrag: function(component) { 15 | return { 16 | item: { 17 | name: component.props.name 18 | } 19 | }; 20 | } 21 | } 22 | }); 23 | } 24 | }, 25 | 26 | render: function () { 27 | return ( 28 |
  • 30 | {this.props.name} 31 |
  • 32 | ); 33 | } 34 | }); 35 | 36 | var Bin = React.createClass({ 37 | mixins: [DragDropMixin], 38 | 39 | getInitialState: function() { 40 | return { items: [] }; 41 | }, 42 | 43 | addItem: function(name) { 44 | clone = this.state.items.slice(0); 45 | clone.push(name); 46 | this.setState({ items: clone }); 47 | }, 48 | 49 | statics: { 50 | configureDragDrop: function(register) { 51 | register(ItemTypes.ITEM, { 52 | dropTarget: { 53 | acceptDrop: function(component, item) { 54 | component.addItem(item.name); 55 | } 56 | } 57 | }); 58 | } 59 | }, 60 | 61 | render: function() { 62 | const dropState = this.getDropState(ItemTypes.ITEM); 63 | 64 | var stateClass = 'none'; 65 | if (dropState.isHovering) { 66 | stateClass = 'hovering'; 67 | } else if (dropState.isDragging) { 68 | stateClass = 'dragging'; 69 | } 70 | 71 | const dropped = this.state.items.map(function(name) { 72 | return
  • {name}
  • ; 73 | }); 74 | 75 | return ( 76 |
    78 | {dropState.isHovering ? 79 | 'Release to drop' : 80 | 'Drag item here'} 81 |
      82 | {dropped} 83 |
    84 |
    85 | ); 86 | } 87 | }); 88 | 89 | var Container = React.createClass({ 90 | render: function() { 91 | return ( 92 |
    93 | 94 |
      95 | 96 | 97 | 98 |
    99 |
    100 | ); 101 | } 102 | }); 103 | 104 | React.render( 105 | , 106 | document.body 107 | ); 108 | -------------------------------------------------------------------------------- /dnd/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | min-height: 100vh; 5 | 6 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 7 | } 8 | 9 | .bin { 10 | width: 300px; 11 | margin: 35px auto; 12 | padding: 45px; 13 | 14 | color: white; 15 | font-size: 2em; 16 | text-align: center; 17 | 18 | border-radius: 5px; 19 | } 20 | 21 | .bin-state-none { 22 | background-color: #34495E; 23 | } 24 | 25 | .bin-state-dragging { 26 | background-color: #E98B39; 27 | } 28 | 29 | .bin-state-hovering { 30 | background-color: #2ECC71; 31 | } 32 | 33 | .dropped { 34 | margin: 20px 0; 35 | padding: 0; 36 | } 37 | 38 | .dropped li { 39 | list-style-type: none; 40 | font-size: 0.6em; 41 | } 42 | 43 | .items { 44 | padding: 0; 45 | text-align: center; 46 | } 47 | 48 | .item { 49 | display: inline-block; 50 | padding: 20px; 51 | margin: 25px 10px; 52 | 53 | border: 2px solid #E74C3C; 54 | } 55 | -------------------------------------------------------------------------------- /kanban/README.md: -------------------------------------------------------------------------------- 1 | # Kanban 2 | 3 | ![Screenshot of the app](img/kanban.png) 4 | 5 | This example is basically a [Trello][trello] clone: We have a set of lists of 6 | tasks ("Todo", "Done"), and each holds some tasks. We can add tasks, delete 7 | them, or drag them from one list to the other. We'll [React DnD][dnd] for 8 | drag-and-drop and a simple Python server for persistence. 9 | 10 | ## The Server 11 | 12 | We'll be using [Flask][flask] for the server, since we don't need much. First, 13 | the imports and the app definition: 14 | 15 | ```python 16 | import json 17 | 18 | from flask import Flask, request 19 | app = Flask(__name__, static_url_path='', static_folder='.') 20 | ``` 21 | 22 | Now, some barebones models: 23 | 24 | ```python 25 | class Task(object): 26 | """A task.""" 27 | 28 | def __init__(self, text): 29 | self.text = text 30 | 31 | def to_dict(self): 32 | return {"text": self.text} 33 | 34 | 35 | class TaskList(object): 36 | """A list of Task objects.""" 37 | 38 | def __init__(self, name, tasks): 39 | self.name = name 40 | self.tasks = tasks 41 | 42 | def to_dict(self): 43 | return { 44 | "name": self.name, 45 | "tasks": [task.to_dict() for task in self.tasks] 46 | } 47 | 48 | 49 | class Board(object): 50 | """A collection of TaskLists.""" 51 | 52 | def __init__(self, lists): 53 | self.lists = lists 54 | 55 | def to_dict(self): 56 | return { 57 | "lists": [list.to_dict() for list in self.lists] 58 | } 59 | ``` 60 | 61 | For persistence, we'll just use the memory: 62 | 63 | ```python 64 | DB = Board([ 65 | TaskList(name="Todo", 66 | tasks=[ 67 | Task("Write example React app"), 68 | Task("Write documentation") 69 | ]), 70 | TaskList(name="Done", 71 | tasks=[ 72 | Task("Learn the basics of React") 73 | ]) 74 | ]) 75 | ``` 76 | 77 | ### Routes 78 | 79 | And now, the routes. This is just a simple REST API that uses JSON, handles 80 | actions and even some errors. 81 | 82 | First, we need a route that will be called on application startup, to get the 83 | initial (or current) state of the Kanban board: 84 | 85 | ```python 86 | @app.route("/api/board/") 87 | def get_board(): 88 | """Return the state of the board.""" 89 | return json.dumps(DB.to_dict()) 90 | ``` 91 | 92 | We also need a way to add new tasks to a list, which is what `add_task` does. 93 | 94 | ```python 95 | @app.route("/api//task", methods=["PUT"]) 96 | def add_task(list_id): 97 | # Add a task to a list. 98 | try: 99 | DB.lists[list_id].tasks.append(Task(text=request.form.get("text"))) 100 | except IndexError: 101 | return json.dumps({"status": "FAIL"}) 102 | return json.dumps({"status": "OK"}) 103 | ``` 104 | 105 | And a way to delete tasks: 106 | 107 | ```python 108 | @app.route("/api//task/", methods=["DELETE"]) 109 | def delete_task(list_id, task_id): 110 | # Remove a task from a list. 111 | try: 112 | del DB.lists[list_id].tasks[task_id] 113 | except IndexError: 114 | return json.dumps({"status": "FAIL"}) 115 | return json.dumps({"status": "OK"}) 116 | ``` 117 | 118 | Finally, we use the root route to serve the `index.html` file: 119 | 120 | ```python 121 | @app.route("/") 122 | def index(): 123 | return app.send_static_file('index.html') 124 | ``` 125 | 126 | And now we tell Flask to always run the server on port 8000: 127 | 128 | ```python 129 | if __name__ == "__main__": 130 | app.run(port=8000, debug=True) 131 | ``` 132 | 133 | ## The Client 134 | 135 | ![Component structure](img/structure.png) 136 | 137 | First, let's import the `DragDropMixin`: 138 | 139 | ```js 140 | var DragDropMixin = ReactDND.DragDropMixin; 141 | ``` 142 | 143 | Since we're only moving one type of component, tasks, we declare that: 144 | 145 | ```js 146 | const ItemTypes = { 147 | TASK: 'task' 148 | }; 149 | ``` 150 | 151 | First, we define the `Task` component. Since we're going to be dragging it from 152 | task list to task list, we use the `DragDropMixin`. 153 | 154 | We then implement the `beginDrag` function and register it with React 155 | DnD. Whenever a task is dragged, we carry the task's text and deletion callback 156 | along with it. 157 | 158 | ```js 159 | var Task = React.createClass({ 160 | mixins: [DragDropMixin], 161 | 162 | statics: { 163 | configureDragDrop: function(register) { 164 | register(ItemTypes.TASK, { 165 | dragSource: { 166 | beginDrag: function(component) { 167 | return { 168 | item: { 169 | text: component.props.text, 170 | deleteTask: component.props.deleteTask 171 | } 172 | }; 173 | } 174 | } 175 | }); 176 | } 177 | }, 178 | 179 | render: function() { 180 | return ( 181 |
  • 183 | {this.props.text} 184 | 186 |
  • 187 | ); 188 | } 189 | }); 190 | ``` 191 | 192 | Then we implement `AddTask`, a sub component of `TaskList` for adding new tasks 193 | to the task list: 194 | 195 | ```js 196 | var AddTask = React.createClass({ 197 | getInitialState: function() { 198 | return { text: "" }; 199 | }, 200 | 201 | handleChange: function(event) { 202 | this.setState({text: event.target.value}); 203 | }, 204 | 205 | render: function() { 206 | return ( 207 |
    208 | 211 | 214 |
    215 | ); 216 | } 217 | }); 218 | ``` 219 | 220 | The `TaskDropBin` is the component in a `TaskList` where tasks being dragged can 221 | be dropped into. 222 | 223 | The `acceptDrop` function takes a component (The drop bin itself) and an item, 224 | which is a JavaScript object representing the data being dragged. We call the 225 | item's `deleteTask` method, which removes it from its original list, and call 226 | the `addTask` method of the drop bin's parent task list to add the task to the 227 | list. 228 | 229 | ```js 230 | var TaskDropBin = React.createClass({ 231 | mixins: [DragDropMixin], 232 | 233 | statics: { 234 | configureDragDrop: function(register) { 235 | register(ItemTypes.TASK, { 236 | dropTarget: { 237 | acceptDrop: function(component, item) { 238 | // When a task is dropped, add it to the parent task list 239 | item.deleteTask(); 240 | component.props.list.addTask(item.text); 241 | } 242 | } 243 | }); 244 | } 245 | }, 246 | ``` 247 | 248 | We query the component's drop state and set its class name accordingly. We'll 249 | later use this to decide whether to show or hide it, and change its color to let 250 | the user know when they can drop a component into the bin. 251 | 252 | ```js 253 | render: function() { 254 | const dropState = this.getDropState(ItemTypes.TASK); 255 | 256 | var stateClass = 'none'; 257 | if (dropState.isHovering) { 258 | stateClass = 'hovering'; 259 | } else if (dropState.isDragging) { 260 | stateClass = 'dragging'; 261 | } 262 | 263 | return
    265 | Drop here 266 |
    ; 267 | } 268 | }); 269 | ``` 270 | 271 | Finally, the task list itself. Since it holds a list of tasks which can be 272 | deleted, and to which new ones can be added, we make that a big of state: 273 | 274 | ```js 275 | var TaskList = React.createClass({ 276 | getInitialState: function() { 277 | return { tasks: this.props.tasks }; 278 | }, 279 | ``` 280 | 281 | The `deleteTask` and `addTask` methods send requests to the server to carry out 282 | the operations in the backend so data remains consistent. 283 | 284 | ```js 285 | deleteTask: function(id) { 286 | var self = this; 287 | $.ajax({ 288 | url: '/api/' + this.props.id + '/task/' + id, 289 | type: 'DELETE', 290 | success: function(result) { 291 | var tasks = self.state.tasks; 292 | tasks.splice(id, 1); 293 | self.setState({ tasks: tasks }); 294 | } 295 | }); 296 | }, 297 | 298 | addTask: function(text) { 299 | var self = this; 300 | $.ajax({ 301 | url: '/api/' + this.props.id + '/task', 302 | type: 'PUT', 303 | data: { 'text' : text }, 304 | success: function(result) { 305 | self.setState({ tasks: self.state.tasks.concat([{ text: text }]) }); 306 | } 307 | }); 308 | }, 309 | ``` 310 | 311 | Now we have the render function, which builds up the list of `Task` components 312 | along with other things, like the list's title, its task drop bin and `AddTask` 313 | component. 314 | 315 | ```js 316 | render: function() { 317 | var self = this; 318 | var task_list = this.state.tasks.map(function(task, index) { 319 | return ( 320 | 323 | ); 324 | }); 325 | return ( 326 |
    327 |

    328 | {this.props.name} 329 |

    330 |
      331 | {task_list} 332 |
    333 | 334 | 335 |
    336 | ); 337 | } 338 | }); 339 | ``` 340 | 341 | Finally, the app component. This is just a wrapper around the other components 342 | that wraps `TaskList` components in a `div`. 343 | 344 | ```js 345 | var App = React.createClass({ 346 | render: function() { 347 | var lists = this.props.lists.map(function(list, index) { 348 | return ( 349 | 353 | ); 354 | }); 355 | return ( 356 |
    357 | {lists} 358 |
    359 | ); 360 | } 361 | }); 362 | ``` 363 | 364 | Once the document has loaded, we ask the server for the initial state of the 365 | board, and render that using the `App` component: 366 | 367 | ```js 368 | $(document).ready(function() { 369 | $.getJSON('http://localhost:8000/api/board', function(data) { 370 | React.render( 371 | , 372 | document.body 373 | ); 374 | }); 375 | }); 376 | ``` 377 | 378 | ## Style 379 | 380 | And finally, we add some style. First, general `body` style: 381 | 382 | ```css 383 | @charset "utf-8"; 384 | 385 | body { 386 | margin: 0; 387 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 388 | } 389 | ``` 390 | 391 | Next, we style the task lists: 392 | 393 | ```css 394 | .lists { 395 | padding: 50px; 396 | } 397 | 398 | .task-list { 399 | width: 220px; 400 | float: left; 401 | margin-right: 25px; 402 | padding: 25px; 403 | border: 1px solid #ccc; 404 | border-radius: 5px; 405 | } 406 | 407 | .list-title { 408 | margin: 0; 409 | text-align: center; 410 | } 411 | 412 | .list-tasks { 413 | padding: 0; 414 | } 415 | ``` 416 | 417 | The task list's drop bin is just a big rectangle with the text "Drop here", 418 | which is not very complicated to style: 419 | 420 | ```css 421 | .drop { 422 | width: 100%; 423 | text-align: center; 424 | font-weight: bold; 425 | padding: 15px 0; 426 | } 427 | ``` 428 | 429 | Since we change the class of the task list's drop bin depending on its state, we 430 | use this state to style it. When the state is `none`, we hide it, on the two 431 | other states, we change the color: Orange when a task is being dragged around, 432 | and green when it is hovering over a drop bin. 433 | 434 | ```css 435 | .drop-state-none { 436 | display: none; 437 | } 438 | 439 | .drop-state-dragging { 440 | background-color: #E98B39; 441 | } 442 | 443 | .drop-state-hovering { 444 | background-color: #2ECC71; 445 | } 446 | ``` 447 | 448 | Finally, some style for the individual tasks: 449 | 450 | ```css 451 | .task { 452 | list-style-type: none; 453 | border: 1px solid #ccc; 454 | border-radius: 5px; 455 | padding: 10px; 456 | margin-bottom: 10px; 457 | } 458 | ``` 459 | 460 | And we use the CSS [content property][content-prop] to put a Unicode times 461 | symbol and make a neat little delete button: 462 | 463 | ```css 464 | .delete:before { 465 | content: "×"; 466 | } 467 | 468 | .delete { 469 | color: red; 470 | float: right; 471 | font-weight: bold; 472 | } 473 | ``` 474 | 475 | [flask]: http://flask.pocoo.org/ 476 | [trello]: https://trello.com/ 477 | [dnd]: https://github.com/gaearon/react-dnd 478 | [content-prop]: https://developer.mozilla.org/en-US/docs/Web/CSS/content 479 | -------------------------------------------------------------------------------- /kanban/img/kanban.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/react-examples/362fa371bbcefcec133800a7df7028b418d2dc73/kanban/img/kanban.png -------------------------------------------------------------------------------- /kanban/img/structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/react-examples/362fa371bbcefcec133800a7df7028b418d2dc73/kanban/img/structure.png -------------------------------------------------------------------------------- /kanban/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Kanban 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /kanban/react-dnd.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):"object"==typeof exports?exports.ReactDND=e():t.ReactDND=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var i=n[r]={exports:{},id:r,loaded:!1};return t[r].call(i.exports,i,i.exports,e),i.loaded=!0,i.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";var r=n(32),i=r.HTML5,o=n(39);t.exports={DragDropMixin:o(i),ImagePreloaderMixin:n(34),DragLayerMixin:n(33),HorizontalDragAnchors:n(19),VerticalDragAnchors:n(20),NativeDragItemTypes:n(8),DropEffects:n(7)}},function(t){function e(t){return"number"==typeof t&&t>-1&&t%1==0&&n>=t}var n=Math.pow(2,53)-1;t.exports=e},function(t){function e(t){var e=typeof t;return"function"==e||t&&"object"==e||!1}t.exports=e},function(t,e,n){function r(t){return null==t?!1:f.call(t)==s?l.test(u.call(t)):o(t)&&a.test(t)||!1}var i=n(73),o=n(4),s="[object Function]",a=/^\[object .+?Constructor\]$/,c=Object.prototype,u=Function.prototype.toString,f=c.toString,l=RegExp("^"+i(f).replace(/toString|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");t.exports=r},function(t){function e(t){return t&&"object"==typeof t||!1}t.exports=e},function(t,e,n){var r=n(1),i=n(3),o=n(4),s="[object Array]",a=Object.prototype,c=a.toString,u=i(u=Array.isArray)&&u,f=u||function(t){return o(t)&&r(t.length)&&c.call(t)==s||!1};t.exports=f},function(t,e,n){"use strict";var r=n(17),i=r({DRAG_START:null,DRAG_END:null,DRAG:null,DROP:null});t.exports=i},function(t){"use strict";var e={COPY:"copy",MOVE:"move",LINK:"link"};t.exports=e},function(t){"use strict";var e={FILE:"__NATIVE_FILE__",URL:"__NATIVE_URL__"};t.exports=e},function(t,e,n){"use strict";var r=n(48).Dispatcher,i=n(15),o=i(new r,{handleAction:function(t){this.dispatch({action:t})}});t.exports=o},function(t,e,n){"use strict";var r=n(9),i=n(6),o=n(22),s=null,a=null,c=null,u=o({getInitialOffsetFromContainer:function(){return s},getInitialOffsetFromClient:function(){return a},getCurrentOffsetFromClient:function(){return c}});u.dispatchToken=r.register(function(t){var e=t.action;switch(e.type){case i.DRAG_START:s=e.offsetFromContainer,a=e.offsetFromClient,c=e.offsetFromClient,u.emitChange();break;case i.DRAG:c=e.offsetFromClient,u.emitChange();break;case i.DRAG_END:s=null,a=null,c=null,u.emitChange()}}),t.exports=u},function(t,e,n){"use strict";var r=n(9),i=n(6),o=n(10),s=n(22),a=null,c=null,u=null,f=null,l=s({isDragging:function(){return!!a},getEffectsAllowed:function(){return u},getDropEffect:function(){return f},getDraggedItem:function(){return a},getDraggedItemType:function(){return c}});l.dispatchToken=r.register(function(t){r.waitFor([o.dispatchToken]);var e=t.action;switch(e.type){case i.DRAG_START:f=null,a=e.item,c=e.itemType,u=e.effectsAllowed,l.emitChange();break;case i.DROP:f=e.dropEffect,l.emitChange();break;case i.DRAG_END:a=null,c=null,u=null,f=null,l.emitChange()}}),t.exports=l},function(t){"use strict";function e(){return!!window.safari}t.exports=e},function(t){function e(t,e){return t=+t,e=null==e?n:e,t>-1&&t%1==0&&e>t}var n=Math.pow(2,53)-1;t.exports=e},function(t,e,n){function r(t){var e=o(t)?t.length:void 0;return i(e)&&c.call(t)==s||!1}var i=n(1),o=n(4),s="[object Arguments]",a=Object.prototype,c=a.toString;t.exports=r},function(t){function e(t){if(null==t)throw new TypeError("Object.assign target cannot be null or undefined");for(var e=Object(t),n=Object.prototype.hasOwnProperty,r=1;r-1},getStateForDragDropMixin:function(){return{draggedItem:c.getDraggedItem(),draggedItemType:c.getDraggedItemType()}},getDragState:function(t){return r(this,t),i(this,t),{isDragging:this.state.ownDraggedItemType===t}},getDropState:function(t){r(this,t),o(this,t);var e=this.getActiveDropTargetType()===t,n=!!this.state.currentDropEffect;return{isDragging:e,isHovering:e&&n}},componentWillMount:function(){this._monitor=new f,this._dragSources={},this._dropTargets={},v(this.constructor.configureDragDrop,"%s must implement static configureDragDrop(register, context) to use DragDropMixin",this.constructor.displayName),this.constructor.configureDragDrop(this.registerDragDropItemTypeHandlers,u)},componentDidMount:function(){e(this),c.addChangeListener(this.handleStoreChangeInDragDropMixin)},componentWillUnmount:function(){n(this),c.removeChangeListener(this.handleStoreChangeInDragDropMixin)},registerDragDropItemTypeHandlers:function(t,e){r(this,t);var n=e.dragSource,i=e.dropTarget;n&&(v(!this._dragSources[t],"Drag source for %s specified twice. See configureDragDrop in %s",t,this.constructor.displayName),this._dragSources[t]=D(n,g)),i&&(v(!this._dropTargets[t],"Drop target for %s specified twice. See configureDragDrop in %s",t,this.constructor.displayName),this._dropTargets[t]=D(i,p))},handleStoreChangeInDragDropMixin:function(){this.isMounted()&&this.setState(this.getStateForDragDropMixin())},dragSourceFor:function(e){return r(this,e),i(this,e),t.getDragSourceProps(this,e)},handleDragStart:function(e,n){var r=this,i=this._dragSources[e],o=i.canDrag,s=i.beginDrag;if(!c.isDragging()&&o(this)){var u,f=s(this),g=f.item,p=f.dragPreview,h=f.dragAnchors,d=f.effectsAllowed,m=this.getDOMNode(),D=m.getBoundingClientRect(),_=t.getOffsetFromClient(this,n);u={x:_.x-D.left,y:_.y-D.top},p||(p=m),d||(d=[l.MOVE]),v(y(d)&&d.length>0,"Expected effectsAllowed to be non-empty array"),v(x(g),'Expected return value of beginDrag to contain "item" object'),t.beginDrag(this,n,m,p,h,u,d),a.startDragging(e,g,d,_,u),setTimeout(function(){r.isMounted()&&c.getDraggedItem()===g&&r.setState({ownDraggedItemType:e})})}},handleDragEnd:function(e){t.endDrag(this);var n=this._dragSources[e].endDrag,r=c.getDropEffect();a.endDragging(),this.isMounted()&&this.setState({ownDraggedItemType:null}),n(this,r)},dropTargetFor:function(){for(var e=this,n=arguments.length,i=Array(n),s=0;n>s;s++)i[s]=arguments[s];return i.forEach(function(t){r(e,t),o(e,t)}),t.getDropTargetProps(this,i)},handleDragEnter:function(t,e){if(this.isAnyDropTargetActive(t)&&this._monitor.enter(e.target)){e.preventDefault();var n=this._dropTargets[this.state.draggedItemType],r=n.enter,i=n.getDropEffect,o=c.getEffectsAllowed(),s=c.getDraggedItemType();d(s)&&(o=[l.COPY]);var a=i(this,o);a&&v(o.indexOf(a)>-1,"Effect %s supplied by drop target is not one of the effects allowed by drag source: %s",a,o.join(", ")),this.setState({currentDropEffect:a}),r(this,this.state.draggedItem)}},handleDragOver:function(e,n){if(this.isAnyDropTargetActive(e)){n.preventDefault();var r=this._dropTargets[this.state.draggedItemType].over;r(this,this.state.draggedItem),t.dragOver(this,n,this.state.currentDropEffect||"move")}},handleDragLeave:function(t,e){if(this.isAnyDropTargetActive(t)&&this._monitor.leave(e.target)){this.setState({currentDropEffect:null});var n=this._dropTargets[this.state.draggedItemType].leave;n(this,this.state.draggedItem)}},handleDrop:function(t,e){if(this.isAnyDropTargetActive(t)){e.preventDefault();var n=this.state.draggedItem,r=this._dropTargets[this.state.draggedItemType].acceptDrop,i=this.state.currentDropEffect,o=!!c.getDropEffect();n||(n=h(e)),this._monitor.reset(),o||a.recordDrop(i),this.setState({currentDropEffect:null}),r(this,n,o,c.getDropEffect())}}}}var a=n(18),c=n(11),u=n(37),f=n(21),l=n(7),g=n(35),p=n(36),h=n(41),d=n(24),v=n(16),m=n(15),D=n(70),y=n(5),x=n(2);t.exports=s},function(t){"use strict";function e(t,e){return-1!==t.indexOf(e,t.length-e.length)}t.exports=e},function(t,e,n){"use strict";function r(t){return i(t)?{files:Array.prototype.slice.call(t.dataTransfer.files)}:o(t)?{urls:(t.dataTransfer.getData("Url")||t.dataTransfer.getData("text/uri-list")||"").split("\n")}:void 0}var i=n(23),o=n(25);t.exports=r},function(t,e,n){"use strict";function r(t){var e=t.indexOf(i.COPY)>-1,n=t.indexOf(i.MOVE)>-1,r=t.indexOf(i.LINK)>-1;return e&&n&&r?"all":e&&n?"copyMove":r&&n?"linkMove":e&&r?"copyLink":e?"copy":n?"move":r?"link":"none"}var i=n(7);t.exports=r},function(t,e,n){"use strict";function r(t,e,n,r){n=n||{};var a=t.offsetWidth,c=t.offsetHeight,u=e instanceof Image,f=u?e.width:a,l=u?e.height:c,g=n.horizontal||i.CENTER,p=n.vertical||o.CENTER,h=r.x,d=r.y;switch(s()&&(l/=window.devicePixelRatio,f/=window.devicePixelRatio),g){case i.LEFT:break;case i.CENTER:h*=f/a;break;case i.RIGHT:h=f-f*(1-h/a)}switch(p){case o.TOP:break;case o.CENTER:d*=l/c;break;case o.BOTTOM:d=l-l*(1-d/c)}return s()&&(d+=(window.devicePixelRatio-1)*l),{x:h,y:d}}var i=n(19),o=n(20),s=n(12);t.exports=r},function(t,e,n){"use strict";function r(){return i()||o()?window.devicePixelRatio:1}var i=n(45),o=n(12);t.exports=r},function(t){"use strict";function e(){return/firefox/i.test(navigator.userAgent)}t.exports=e},function(t){"use strict";function e(){return"WebkitAppearance"in document.documentElement.style}t.exports=e},function(t,e,n){"use strict";function r(t){return t?i()&&t instanceof Image&&o(t.src,".gif")?!1:!0:!1}var i=n(12),o=n(40);t.exports=r},function(t,e,n){t.exports.Dispatcher=n(49)},function(t,e,n){"use strict";function r(){this.$Dispatcher_callbacks={},this.$Dispatcher_isPending={},this.$Dispatcher_isHandled={},this.$Dispatcher_isDispatching=!1,this.$Dispatcher_pendingPayload=null}var i=n(50),o=1,s="ID_";r.prototype.register=function(t){var e=s+o++;return this.$Dispatcher_callbacks[e]=t,e},r.prototype.unregister=function(t){i(this.$Dispatcher_callbacks[t],"Dispatcher.unregister(...): `%s` does not map to a registered callback.",t),delete this.$Dispatcher_callbacks[t]},r.prototype.waitFor=function(t){i(this.$Dispatcher_isDispatching,"Dispatcher.waitFor(...): Must be invoked while dispatching.");for(var e=0;e=200?s(e):null,l=e.length;f&&(c=o,u=!1,e=f);t:for(;++ae&&(e=-e>i?0:i+e),n="undefined"==typeof n||n>i?i:+n||0,0>n&&(n+=i),i=e>n?0:n-e>>>0,e>>>=0;for(var o=Array(i);++r=200,f=u?s():null,l=[];f?(r=o,c=!1):(u=!1,f=e?[]:l);t:for(;++nn||null==r)return r;var s=e[n-2],a=e[n-1],c=e[3];n>3&&"function"==typeof s?(s=i(s,a,5),n-=2):(s=n>2&&"function"==typeof a?a:null,n-=s?1:0),c&&o(e[1],e[2],c)&&(s=3==n?null:s,n=2);for(var u=0;++u0;++rt||isNaN(t))throw TypeError("n must be a positive number");return this._maxListeners=t,this},e.prototype.emit=function(t){var e,r,s,a,c,u;if(this._events||(this._events={}),"error"===t&&(!this._events.error||i(this._events.error)&&!this._events.error.length)){if(e=arguments[1],e instanceof Error)throw e;throw TypeError('Uncaught, unspecified "error" event.')}if(r=this._events[t],o(r))return!1;if(n(r))switch(arguments.length){case 1:r.call(this);break;case 2:r.call(this,arguments[1]);break;case 3:r.call(this,arguments[1],arguments[2]);break;default:for(s=arguments.length,a=new Array(s-1),c=1;s>c;c++)a[c-1]=arguments[c];r.apply(this,a)}else if(i(r)){for(s=arguments.length,a=new Array(s-1),c=1;s>c;c++)a[c-1]=arguments[c];for(u=r.slice(),s=u.length,c=0;s>c;c++)u[c].apply(this,a)}return!0},e.prototype.addListener=function(t,r){var s;if(!n(r))throw TypeError("listener must be a function");if(this._events||(this._events={}),this._events.newListener&&this.emit("newListener",t,n(r.listener)?r.listener:r),this._events[t]?i(this._events[t])?this._events[t].push(r):this._events[t]=[this._events[t],r]:this._events[t]=r,i(this._events[t])&&!this._events[t].warned){var s;s=o(this._maxListeners)?e.defaultMaxListeners:this._maxListeners,s&&s>0&&this._events[t].length>s&&(this._events[t].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[t].length),"function"==typeof console.trace&&console.trace())}return this},e.prototype.on=e.prototype.addListener,e.prototype.once=function(t,e){function r(){this.removeListener(t,r),i||(i=!0,e.apply(this,arguments))}if(!n(e))throw TypeError("listener must be a function");var i=!1;return r.listener=e,this.on(t,r),this},e.prototype.removeListener=function(t,e){var r,o,s,a;if(!n(e))throw TypeError("listener must be a function");if(!this._events||!this._events[t])return this;if(r=this._events[t],s=r.length,o=-1,r===e||n(r.listener)&&r.listener===e)delete this._events[t],this._events.removeListener&&this.emit("removeListener",t,e);else if(i(r)){for(a=s;a-->0;)if(r[a]===e||r[a].listener&&r[a].listener===e){o=a;break}if(0>o)return this;1===r.length?(r.length=0,delete this._events[t]):r.splice(o,1),this._events.removeListener&&this.emit("removeListener",t,e)}return this},e.prototype.removeAllListeners=function(t){var e,r;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[t]&&delete this._events[t],this;if(0===arguments.length){for(e in this._events)"removeListener"!==e&&this.removeAllListeners(e);return this.removeAllListeners("removeListener"),this._events={},this}if(r=this._events[t],n(r))this.removeListener(t,r);else for(;r.length;)this.removeListener(t,r[r.length-1]);return delete this._events[t],this},e.prototype.listeners=function(t){var e;return e=this._events&&this._events[t]?n(this._events[t])?[this._events[t]]:this._events[t].slice():[]},e.listenerCount=function(t,e){var r;return r=t._events&&t._events[e]?n(t._events[e])?1:t._events[e].length:0}}])}); -------------------------------------------------------------------------------- /kanban/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | Flask-Cors==2.0.1 3 | -------------------------------------------------------------------------------- /kanban/scripts.js: -------------------------------------------------------------------------------- 1 | var DragDropMixin = ReactDND.DragDropMixin; 2 | 3 | const ItemTypes = { 4 | TASK: 'task' 5 | }; 6 | 7 | var Task = React.createClass({ 8 | mixins: [DragDropMixin], 9 | 10 | statics: { 11 | configureDragDrop: function(register) { 12 | register(ItemTypes.TASK, { 13 | dragSource: { 14 | beginDrag: function(component) { 15 | return { 16 | item: { 17 | text: component.props.text, 18 | deleteTask: component.props.deleteTask 19 | } 20 | }; 21 | } 22 | } 23 | }); 24 | } 25 | }, 26 | 27 | render: function() { 28 | return ( 29 |
  • 31 | {this.props.text} 32 | 34 |
  • 35 | ); 36 | } 37 | }); 38 | 39 | var AddTask = React.createClass({ 40 | getInitialState: function() { 41 | return { text: "" }; 42 | }, 43 | 44 | handleChange: function(event) { 45 | this.setState({text: event.target.value}); 46 | }, 47 | 48 | render: function() { 49 | return ( 50 |
    51 | 54 | 57 |
    58 | ); 59 | } 60 | }); 61 | 62 | var TaskDropBin = React.createClass({ 63 | mixins: [DragDropMixin], 64 | 65 | statics: { 66 | configureDragDrop: function(register) { 67 | register(ItemTypes.TASK, { 68 | dropTarget: { 69 | acceptDrop: function(component, item) { 70 | // When a task is dropped, add it to the parent task list 71 | item.deleteTask(); 72 | component.props.list.addTask(item.text); 73 | } 74 | } 75 | }); 76 | } 77 | }, 78 | 79 | render: function() { 80 | const dropState = this.getDropState(ItemTypes.TASK); 81 | 82 | var stateClass = 'none'; 83 | if (dropState.isHovering) { 84 | stateClass = 'hovering'; 85 | } else if (dropState.isDragging) { 86 | stateClass = 'dragging'; 87 | } 88 | 89 | return
    91 | Drop here 92 |
    ; 93 | } 94 | }); 95 | 96 | var TaskList = React.createClass({ 97 | getInitialState: function() { 98 | return { tasks: this.props.tasks }; 99 | }, 100 | 101 | deleteTask: function(id) { 102 | var self = this; 103 | $.ajax({ 104 | url: '/api/' + this.props.id + '/task/' + id, 105 | type: 'DELETE', 106 | success: function(result) { 107 | var tasks = self.state.tasks; 108 | tasks.splice(id, 1); 109 | self.setState({ tasks: tasks }); 110 | } 111 | }); 112 | }, 113 | 114 | addTask: function(text) { 115 | var self = this; 116 | $.ajax({ 117 | url: '/api/' + this.props.id + '/task', 118 | type: 'PUT', 119 | data: { 'text' : text }, 120 | success: function(result) { 121 | self.setState({ tasks: self.state.tasks.concat([{ text: text }]) }); 122 | } 123 | }); 124 | }, 125 | 126 | render: function() { 127 | var self = this; 128 | var task_list = this.state.tasks.map(function(task, index) { 129 | return ( 130 | 133 | ); 134 | }); 135 | return ( 136 |
    137 |

    138 | {this.props.name} 139 |

    140 |
      141 | {task_list} 142 |
    143 | 144 | 145 |
    146 | ); 147 | } 148 | }); 149 | 150 | var App = React.createClass({ 151 | render: function() { 152 | var lists = this.props.lists.map(function(list, index) { 153 | return ( 154 | 158 | ); 159 | }); 160 | return ( 161 |
    162 | {lists} 163 |
    164 | ); 165 | } 166 | }); 167 | 168 | $(document).ready(function() { 169 | $.getJSON('http://localhost:8000/api/board', function(data) { 170 | React.render( 171 | , 172 | document.body 173 | ); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /kanban/server.py: -------------------------------------------------------------------------------- 1 | import json 2 | from flask import Flask, request 3 | from flask.ext.cors import CORS 4 | 5 | 6 | app = Flask(__name__, static_url_path='', static_folder='.') 7 | cors = CORS(app) 8 | 9 | 10 | # One of the simplest configurations. Exposes all resources matching /api/* to 11 | # CORS and allows the Content-Type header, which is necessary to POST JSON 12 | # cross origin. 13 | CORS(app, resources=r'/api/*', allow_headers='Content-Type') 14 | 15 | 16 | class Task(object): 17 | """A task.""" 18 | 19 | def __init__(self, text): 20 | self.text = text 21 | 22 | def to_dict(self): 23 | return {"text": self.text} 24 | 25 | 26 | class TaskList(object): 27 | """A list of Task objects.""" 28 | 29 | def __init__(self, name, tasks): 30 | self.name = name 31 | self.tasks = tasks 32 | 33 | def to_dict(self): 34 | return { 35 | "name": self.name, 36 | "tasks": [task.to_dict() for task in self.tasks] 37 | } 38 | 39 | 40 | class Board(object): 41 | """A collection of TaskLists.""" 42 | 43 | def __init__(self, lists): 44 | self.lists = lists 45 | 46 | def to_dict(self): 47 | return { 48 | "lists": [list.to_dict() for list in self.lists] 49 | } 50 | 51 | 52 | DB = Board([ 53 | TaskList(name="Todo", 54 | tasks=[ 55 | Task("Write example React app"), 56 | Task("Write documentation") 57 | ]), 58 | TaskList(name="Done", 59 | tasks=[ 60 | Task("Learn the basics of React") 61 | ]) 62 | ]) 63 | 64 | 65 | @app.route("/api/board/") 66 | def get_board(): 67 | """Return the state of the board.""" 68 | return json.dumps(DB.to_dict()) 69 | 70 | 71 | @app.route("/api//task", methods=["PUT"]) 72 | def add_task(list_id): 73 | # Add a task to a list. 74 | try: 75 | DB.lists[list_id].tasks.append(Task(text=request.form.get("text"))) 76 | except IndexError: 77 | return json.dumps({"status": "FAIL"}) 78 | return json.dumps({"status": "OK"}) 79 | 80 | 81 | @app.route("/api//task/", methods=["DELETE"]) 82 | def delete_task(list_id, task_id): 83 | # Remove a task from a list. 84 | try: 85 | del DB.lists[list_id].tasks[task_id] 86 | except IndexError: 87 | return json.dumps({"status": "FAIL"}) 88 | return json.dumps({"status": "OK"}) 89 | 90 | 91 | @app.route("/") 92 | def index(): 93 | return app.send_static_file('index.html') 94 | 95 | if __name__ == "__main__": 96 | app.run(port=8000, debug=True) 97 | -------------------------------------------------------------------------------- /kanban/style.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | margin: 0; 5 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 6 | } 7 | 8 | .lists { 9 | padding: 50px; 10 | } 11 | 12 | .task-list { 13 | width: 220px; 14 | float: left; 15 | margin-right: 25px; 16 | padding: 25px; 17 | border: 1px solid #ccc; 18 | border-radius: 5px; 19 | } 20 | 21 | .list-title { 22 | margin: 0; 23 | text-align: center; 24 | } 25 | 26 | .list-tasks { 27 | padding: 0; 28 | } 29 | 30 | .drop { 31 | width: 100%; 32 | text-align: center; 33 | font-weight: bold; 34 | padding: 15px 0; 35 | } 36 | 37 | .drop-state-none { 38 | display: none; 39 | } 40 | 41 | .drop-state-dragging { 42 | background-color: #E98B39; 43 | } 44 | 45 | .drop-state-hovering { 46 | background-color: #2ECC71; 47 | } 48 | 49 | .task { 50 | list-style-type: none; 51 | border: 1px solid #ccc; 52 | border-radius: 5px; 53 | padding: 10px; 54 | margin-bottom: 10px; 55 | } 56 | 57 | .delete:before { 58 | content: "×"; 59 | } 60 | 61 | .delete { 62 | color: red; 63 | float: right; 64 | font-weight: bold; 65 | } 66 | -------------------------------------------------------------------------------- /mailbox/README.md: -------------------------------------------------------------------------------- 1 | # Mailbox 2 | 3 | ![Screenshot](img/screenshot.png) 4 | 5 | The app we want to design is basically a React clone of the email client in 6 | Ember's home page. It won't send email, or communicate with a backend to pull a 7 | list of emails, it's just a bit of static data with some Bootstrap styling. 8 | 9 | ## Components 10 | 11 | Each component is, as always, a visible, semantic element on the screen. We have 12 | components to represent an email, a list of emails, the list of mailboxes, and a 13 | general component for when no email or mailbox has been selected. I’ve 14 | highlighted component structure, as the React examples do, in this image: 15 | 16 | ![Component structure](img/structure.png) 17 | 18 | The interface is precisely what you’d expect. There’s a list of mailboxes 19 | (Inbox, Spam), when you click on one it updates the list of emails, and when you 20 | click on an email from the list it displays its metadata and body. 21 | 22 | First things first: The `Email` component. 23 | 24 | ```javascript 25 | var Email = React.createClass({ 26 | render: function() { 27 | return ( 28 |
    29 |
    30 |
    From
    31 |
    {this.props.from}
    32 | 33 |
    To
    34 |
    {this.props.to}
    35 | 36 |
    Subject
    37 |
    {this.props.subject}
    38 |
    39 |
    40 |
    41 | ); 42 | } 43 | }); 44 | ``` 45 | 46 | This is just a `div` with a definition list of the various props, nothing that 47 | requires an explanation. We embed the raw HTML body using React's 48 | `dangerouslySetInnerHTML`. Note that this is just an example, you probably 49 | should never use `dangerouslySetInnerHTML` in "real" code as it is, well, 50 | dangerous. You can read more about that 51 | [here](https://facebook.github.io/react/tips/dangerously-set-inner-html.html). 52 | 53 | Now, the list of emails. Which is actually rendered as a table, but semantically 54 | is a list. Let's first go through the list itself: 55 | 56 | ```javascript 57 | var EmailList = React.createClass({ 58 | render: function() { 59 | var email_list = this.props.emails.map(function(mail) { 60 | return ( 61 | 66 | ); 67 | }.bind(this)); 68 | 69 | return ( 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | {email_list} 80 | 81 |
    SubjectFromTo
    82 | ); 83 | } 84 | }); 85 | ``` 86 | 87 | The render function iterates over the email list, collecting (Or it 'maps over') 88 | `` components, passing some initial props to them. There are two 89 | props here that matter: 90 | 91 | * `key`: This is a prop that you pass to every list item when building lists in 92 | react. It can simply be the index (Which the `map` function provides), or a 93 | more domain-specific identifier. 94 | 95 | * `on_click`: This prop sends a function that's passed from even higher up down 96 | to the ``. We use `bind` to partially apply the function. 97 | 98 | Now, the `` component looks like this: 99 | 100 | ```javascript 101 | var EmailListItem = React.createClass({ 102 | render: function() { 103 | return ( 104 | 105 | {this.props.subject} 106 | {this.props.from} 107 | {this.props.to} 108 | 109 | ); 110 | } 111 | }); 112 | ``` 113 | 114 | Here we use React's `onClick` prop to declare that when that table row is 115 | clicked, the `on_click` prop should be called. 116 | 117 | Here we take a slight detour to make a small, reusable component. We use this 118 | whenever we let the user choose an element from a list, to represent the initial 119 | state when no element is chosen. The `` component uses a single 120 | prop, `text`. 121 | 122 | ```javascript 123 | var NoneSelected = React.createClass({ 124 | render: function() { 125 | return ( 126 |
    127 | No {this.props.text} selected. 128 |
    129 | ); 130 | } 131 | }); 132 | ``` 133 | 134 | Now, we build a Mailbox component we can use to display the current email and 135 | the list of emails. The `` has only one item of state: The ID of the 136 | selected email, which is either a natural number or null. A method, 137 | `handleSelectEmail`, will be passed down the component hierarchy as a callback 138 | for the `onClick` event of an `` instance. 139 | 140 | The `render` functions is very simple: If an email is selected, it filters its 141 | data out of the props and instantiates a corresponding `` component. If 142 | it isn't, the "email" is just an instance of ``. Then the email 143 | list and selected email are displayed. 144 | 145 | ```javascript 146 | var Mailbox = React.createClass({ 147 | getInitialState: function(){ 148 | return { email_id: null }; 149 | }, 150 | 151 | handleSelectEmail: function(id) { 152 | this.setState({ email_id: id }); 153 | }, 154 | 155 | render: function() { 156 | var email_id = this.state.email_id; 157 | if (email_id) { 158 | var mail = this.props.emails.filter(function(mail) { 159 | return mail.id == email_id; 160 | })[0]; 161 | selected_email = ; 166 | } else { 167 | selected_email = ; 168 | } 169 | 170 | return ( 171 |
    172 | 174 |
    175 | {selected_email} 176 |
    177 |
    178 | ); 179 | } 180 | }); 181 | ``` 182 | 183 | And now the list of mailboxes. This isn't too complicated, the `render` function 184 | just maps over its props to create an array of list items, which it embeds in 185 | the JSX. 186 | 187 | ```javascript 188 | var MailboxList = React.createClass({ 189 | render: function() { 190 | var mailbox_list = this.props.mailboxes.map(function(mailbox) { 191 | return ( 192 |
  • 195 | 196 | {mailbox.emails.length} 197 | 198 | {mailbox.name} 199 |
  • 200 | ); 201 | }.bind(this)); 202 | 203 | return ( 204 |
    205 |
      206 | {mailbox_list} 207 |
    208 |
    209 | ); 210 | } 211 | }); 212 | ``` 213 | 214 | The `` component ties everything together. Like the `` component, 215 | this has an ID (state) and a method to track the currently selected mailbox. The 216 | `render` function is essentially the same as well: It renders a `` 217 | list, and if a mailbox is selected, it renders it, otherwise it renders a 218 | ``. 219 | 220 | ```javascript 221 | var App = React.createClass({ 222 | getInitialState: function(){ 223 | return { mailbox_id: null }; 224 | }, 225 | 226 | handleSelectMailbox: function(id) { 227 | this.setState({ mailbox_id: id }); 228 | }, 229 | 230 | render: function() { 231 | var mailbox_id = this.state.mailbox_id; 232 | if (mailbox_id) { 233 | var mailbox = this.props.mailboxes.filter(function(mailbox) { 234 | return mailbox.id == mailbox_id; 235 | })[0]; 236 | selected_mailbox = ; 238 | } else { 239 | selected_mailbox = ; 240 | } 241 | 242 | return ( 243 |
    244 | 246 |
    247 |
    248 |
    249 | {selected_mailbox} 250 |
    251 |
    252 |
    253 |
    254 | ); 255 | } 256 | }); 257 | ``` 258 | 259 | We'll use the following fixtures for our mailboxes and emails: 260 | 261 | ```javascript 262 | var fixtures = [ 263 | { 264 | id: 1, 265 | name: "Inbox", 266 | emails: [ 267 | { 268 | id: 1, 269 | from: "joe@tryolabs.com", 270 | to: "fernando@tryolabs.com", 271 | subject: "Meeting", 272 | body: "hi" 273 | }, 274 | { 275 | id: 2, 276 | from: "newsbot@tryolabs.com", 277 | to: "fernando@tryolabs.com", 278 | subject: "News Digest", 279 | body: "

    Intro to React

    " 280 | } 281 | ] 282 | }, 283 | { 284 | id: 2, 285 | name: "Spam", 286 | emails: [ 287 | { 288 | id: 3, 289 | from: "nigerian.prince@gmail.com", 290 | to: "fernando@tryolabs.com", 291 | subject: "Obivous 419 scam", 292 | body: "You've won the prize!!!1!1!!!" 293 | } 294 | ] 295 | } 296 | ]; 297 | ``` 298 | 299 | We render the app into the document's body, using the fixtures as the list of mailboxes. 300 | 301 | ```javascript 302 | React.render( 303 | , 304 | document.body 305 | ); 306 | ``` 307 | 308 | Finally, we add a little style: 309 | 310 | ```css 311 | .mailboxes { 312 | margin: 25px auto; 313 | width: 120px; 314 | } 315 | 316 | .mailbox { 317 | margin-top: 25px; 318 | } 319 | 320 | .email-viewer { 321 | padding: 25px; 322 | } 323 | 324 | .none-selected { 325 | margin: 20px; 326 | padding: 20px; 327 | font-size: 1.2em; 328 | } 329 | ``` 330 | 331 | And that’s it. That’s the simplest react application with some actions and more 332 | than a couple components. You can run this using the `run_server.sh` script in 333 | this repository. 334 | 335 | ## A closer look at callbacks 336 | 337 | Callbacks are passed down through the component hierarchy by props, and actions 338 | climb their way back up to the component that handles them. For instance, when 339 | selecting mailboxes: 340 | 341 | 1. The `` component has a method, `handleSelectMailbox`, which takes a 342 | mailbox ID and sets the app's current mailbox ID to it. 343 | 2. In `render`, the method is passed to `` as the `onSelectMailbox` 344 | prop. 345 | 3. In ``, it's bound to null and assigned to the `onClick` event 346 | prop of the mailbox list item. 347 | 348 | The repetition (Passing things again and again) rather violates DRY, but it's 349 | not hard to follow after an initial look through the code. 350 | -------------------------------------------------------------------------------- /mailbox/img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/react-examples/362fa371bbcefcec133800a7df7028b418d2dc73/mailbox/img/screenshot.png -------------------------------------------------------------------------------- /mailbox/img/structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/react-examples/362fa371bbcefcec133800a7df7028b418d2dc73/mailbox/img/structure.png -------------------------------------------------------------------------------- /mailbox/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Email Client 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /mailbox/run_server.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PY_VERSION=`python -c 'import sys; print("%i" % (sys.hexversion<0x03000000))'` 4 | 5 | if [ $PY_VERSION -eq 0 ]; then 6 | python -m http.server 7 | else 8 | python -m SimpleHTTPServer 9 | fi 10 | -------------------------------------------------------------------------------- /mailbox/scripts.js: -------------------------------------------------------------------------------- 1 | var Email = React.createClass({ 2 | render: function() { 3 | return ( 4 |
    5 |
    6 |
    From
    7 |
    {this.props.from}
    8 | 9 |
    To
    10 |
    {this.props.to}
    11 | 12 |
    Subject
    13 |
    {this.props.subject}
    14 |
    15 |
    16 |
    17 | ); 18 | } 19 | }); 20 | 21 | var EmailListItem = React.createClass({ 22 | render: function() { 23 | return ( 24 | 25 | {this.props.subject} 26 | {this.props.from} 27 | {this.props.to} 28 | 29 | ); 30 | } 31 | }); 32 | 33 | var EmailList = React.createClass({ 34 | render: function() { 35 | var email_list = this.props.emails.map(function(mail) { 36 | return ( 37 | 42 | ); 43 | }.bind(this)); 44 | 45 | return ( 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | {email_list} 56 | 57 |
    SubjectFromTo
    58 | ); 59 | } 60 | }); 61 | 62 | var NoneSelected = React.createClass({ 63 | render: function() { 64 | return ( 65 |
    66 | No {this.props.text} selected. 67 |
    68 | ); 69 | } 70 | }); 71 | 72 | var Mailbox = React.createClass({ 73 | getInitialState: function(){ 74 | return { email_id: null }; 75 | }, 76 | 77 | handleSelectEmail: function(id) { 78 | this.setState({ email_id: id }); 79 | }, 80 | 81 | render: function() { 82 | var email_id = this.state.email_id; 83 | if (email_id) { 84 | var mail = this.props.emails.filter(function(mail) { 85 | return mail.id == email_id; 86 | })[0]; 87 | selected_email = ; 92 | } else { 93 | selected_email = ; 94 | } 95 | 96 | return ( 97 |
    98 | 100 |
    101 | {selected_email} 102 |
    103 |
    104 | ); 105 | } 106 | }); 107 | 108 | var MailboxList = React.createClass({ 109 | render: function() { 110 | var mailbox_list = this.props.mailboxes.map(function(mailbox) { 111 | return ( 112 |
  • 115 | 116 | {mailbox.emails.length} 117 | 118 | {mailbox.name} 119 |
  • 120 | ); 121 | }.bind(this)); 122 | 123 | return ( 124 |
    125 |
      126 | {mailbox_list} 127 |
    128 |
    129 | ); 130 | } 131 | }); 132 | 133 | var App = React.createClass({ 134 | getInitialState: function(){ 135 | return { mailbox_id: null }; 136 | }, 137 | 138 | handleSelectMailbox: function(id) { 139 | this.setState({ mailbox_id: id }); 140 | }, 141 | 142 | render: function() { 143 | var mailbox_id = this.state.mailbox_id; 144 | if (mailbox_id) { 145 | var mailbox = this.props.mailboxes.filter(function(mailbox) { 146 | return mailbox.id == mailbox_id; 147 | })[0]; 148 | selected_mailbox = ; 150 | } else { 151 | selected_mailbox = ; 152 | } 153 | 154 | return ( 155 |
    156 | 158 |
    159 |
    160 |
    161 | {selected_mailbox} 162 |
    163 |
    164 |
    165 |
    166 | ); 167 | } 168 | }); 169 | 170 | var fixtures = [ 171 | { 172 | id: 1, 173 | name: "Inbox", 174 | emails: [ 175 | { 176 | id: 1, 177 | from: "joe@tryolabs.com", 178 | to: "fernando@tryolabs.com", 179 | subject: "Meeting", 180 | body: "hi" 181 | }, 182 | { 183 | id: 2, 184 | from: "newsbot@tryolabs.com", 185 | to: "fernando@tryolabs.com", 186 | subject: "News Digest", 187 | body: "

    Intro to React

    " 188 | } 189 | ] 190 | }, 191 | { 192 | id: 2, 193 | name: "Spam", 194 | emails: [ 195 | { 196 | id: 3, 197 | from: "nigerian.prince@gmail.com", 198 | to: "fernando@tryolabs.com", 199 | subject: "Obivous 419 scam", 200 | body: "You've won the prize!!!1!1!!!" 201 | } 202 | ] 203 | } 204 | ]; 205 | 206 | React.render( 207 | , 208 | document.body 209 | ); 210 | -------------------------------------------------------------------------------- /mailbox/style.css: -------------------------------------------------------------------------------- 1 | .mailboxes { 2 | margin: 25px auto; 3 | width: 120px; 4 | } 5 | 6 | .mailbox { 7 | margin-top: 25px; 8 | } 9 | 10 | .email-viewer { 11 | padding: 25px; 12 | } 13 | 14 | .none-selected { 15 | margin: 20px; 16 | padding: 20px; 17 | font-size: 1.2em; 18 | } 19 | -------------------------------------------------------------------------------- /modal/README.md: -------------------------------------------------------------------------------- 1 | # Modal 2 | 3 | This is an example of creating a reusable modal component with React that 4 | supports CSS animations. 5 | 6 | It would be fairly easy to create a specific modal component that is mounted and 7 | unmounted by toggling a bit of state in the parent component. However, what we 8 | want is a little more complex: 9 | 10 | * We want a reusable modal component, that you just wrap around the modal's 11 | contents, add some props, and it works, so you don't have to create a new 12 | component for each modal in your app. 13 | 14 | * Animation support: We want to be able to write some CSS selectors that 15 | correspond to the various stages of the modal's lifecycle. 16 | 17 | ## Components 18 | 19 | First, we have to use React's 20 | [CSS Transition Group component](https://facebook.github.io/react/docs/animation.html): 21 | 22 | ```javascript 23 | var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; 24 | ``` 25 | 26 | Next, we create the modal. This component automates the following: 27 | 28 | * It determines whether or not it should be shown using a prop. 29 | * It wraps its contents in a CSS Transition Group. 30 | 31 | ```javascript 32 | var Modal = React.createClass({ 33 | render: function() { 34 | if(this.props.isOpen){ 35 | return ( 36 | 37 |
    38 | {this.props.children} 39 |
    40 |
    41 | ); 42 | } else { 43 | return ; 44 | } 45 | } 46 | }); 47 | ``` 48 | 49 | Note how we use `this.props.children` to extract the component's body. 50 | 51 | Because React requires that the render function return a component, rather than 52 | returning `null` when the modal is not open, we return an empty transition 53 | group. 54 | 55 | Now, for this example, we'll create an `App` component to hold the state that 56 | tells React whether the modal is open, and a couple of methods to open and close 57 | it. 58 | 59 | ```javascript 60 | var App = React.createClass({ 61 | getInitialState: function() { 62 | return { isModalOpen: false }; 63 | }, 64 | 65 | openModal: function() { 66 | this.setState({ isModalOpen: true }); 67 | }, 68 | 69 | closeModal: function() { 70 | this.setState({ isModalOpen: false }); 71 | }, 72 | 73 | render: function() { 74 | return ( 75 |
    76 |

    App

    77 | 78 | 80 |

    My Modal

    81 |
    82 |

    This is the modal's body.

    83 |
    84 | 85 |
    86 |
    87 | ); 88 | } 89 | }); 90 | ``` 91 | 92 | As you can see in `render`, all we had to do was wrap the contents of the modal 93 | in the `Modal` component, pass the state that determines whether its open, and 94 | give the CSS transition a name (We'll use this later). Inside the body, we make 95 | insert a call to the `closeModal` method. There's no need to bind anything. 96 | 97 | Finally, we render the `App` component, and this concludes the JavaScript part 98 | of this example: 99 | 100 | ```javascript 101 | React.render( 102 | , 103 | document.body 104 | ); 105 | ``` 106 | 107 | ## Style 108 | 109 | Most of the stylesheet is not important to this example, it's just giving the 110 | app and modal components a shape. The important part is the CSS animation 111 | classes that React will use. 112 | 113 | The `enter` selector sets the style for the component's initial state before 114 | animation begins, and `enter-active` sets the final state: 115 | 116 | ```css 117 | .modal-anim-enter { 118 | opacity: 0.00; 119 | transform: scale(0.7); 120 | transition: all 0.2s; 121 | } 122 | 123 | .modal-anim-enter.modal-anim-enter-active { 124 | opacity: 1; 125 | transform: scale(1); 126 | } 127 | ``` 128 | 129 | `leave` and `leave-active` are the opposite: 130 | 131 | ```css 132 | .modal-anim-leave { 133 | opacity: 1; 134 | transform: scale(1); 135 | transition: all 0.2s; 136 | } 137 | 138 | .modal-anim-leave.modal-anim-leave-active { 139 | opacity: 0.00; 140 | transform: scale(0.7); 141 | } 142 | ``` 143 | -------------------------------------------------------------------------------- /modal/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Modal 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /modal/run_server.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PY_VERSION=`python -c 'import sys; print("%i" % (sys.hexversion<0x03000000))'` 4 | 5 | if [ $PY_VERSION -eq 0 ]; then 6 | python -m http.server 7 | else 8 | python -m SimpleHTTPServer 9 | fi 10 | -------------------------------------------------------------------------------- /modal/scripts.js: -------------------------------------------------------------------------------- 1 | var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; 2 | 3 | var Modal = React.createClass({ 4 | render: function() { 5 | if(this.props.isOpen){ 6 | return ( 7 | 8 |
    9 | {this.props.children} 10 |
    11 |
    12 | ); 13 | } else { 14 | return ; 15 | } 16 | } 17 | }); 18 | 19 | var App = React.createClass({ 20 | getInitialState: function() { 21 | return { isModalOpen: false }; 22 | }, 23 | 24 | openModal: function() { 25 | this.setState({ isModalOpen: true }); 26 | }, 27 | 28 | closeModal: function() { 29 | this.setState({ isModalOpen: false }); 30 | }, 31 | 32 | render: function() { 33 | return ( 34 |
    35 |

    App

    36 | 37 | 39 |

    My Modal

    40 |
    41 |

    This is the modal's body.

    42 |
    43 | 44 |
    45 |
    46 | ); 47 | } 48 | }); 49 | 50 | React.render( 51 | , 52 | document.body 53 | ); 54 | -------------------------------------------------------------------------------- /modal/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; 3 | margin: 0; 4 | 5 | min-height: 100vh; 6 | 7 | display: flex; 8 | align-items: center; 9 | } 10 | 11 | .app { 12 | width: 300px; 13 | margin: 0 auto; 14 | padding: 20px; 15 | 16 | border: 1px solid #ccc; 17 | border-radius: 4px; 18 | 19 | z-index: 0; 20 | } 21 | 22 | h1, h3 { 23 | text-align: center; 24 | margin: 0 0 20px 0; 25 | } 26 | 27 | .modal { 28 | width: 200px; 29 | height: 100px; 30 | padding: 20px; 31 | 32 | position: absolute; 33 | z-index: 1; 34 | top: 50%; 35 | left: 50%; 36 | margin: -70px 0 0 -120px; 37 | 38 | border: 1px solid #ccc; 39 | background: white; 40 | } 41 | 42 | button { 43 | display: block; 44 | margin: 0 auto; 45 | padding: 6px 12px; 46 | 47 | border: 1px solid #ccc; 48 | border-radius: 4px; 49 | background: white; 50 | 51 | color: #0066CC; 52 | font-weight: bold; 53 | 54 | font-size: 14px; 55 | line-height: 20px; 56 | } 57 | 58 | /* Animation */ 59 | 60 | .modal-anim-enter { 61 | opacity: 0.00; 62 | transform: scale(0.7); 63 | transition: all 0.2s; 64 | } 65 | 66 | .modal-anim-enter.modal-anim-enter-active { 67 | opacity: 1; 68 | transform: scale(1); 69 | } 70 | 71 | .modal-anim-leave { 72 | opacity: 1; 73 | transform: scale(1); 74 | transition: all 0.2s; 75 | } 76 | 77 | .modal-anim-leave.modal-anim-leave-active { 78 | opacity: 0.00; 79 | transform: scale(0.7); 80 | } 81 | -------------------------------------------------------------------------------- /nuclear-test/.gitignore: -------------------------------------------------------------------------------- 1 | build/main.js 2 | build/vendors.js 3 | node_modules/* 4 | 5 | -------------------------------------------------------------------------------- /nuclear-test/build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NuclearJs Test 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /nuclear-test/client/actions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import reactor from './reactor'; 4 | import axios from 'axios'; 5 | 6 | 7 | var actionsNames = [ 8 | 'CONTACT_SELECTED', 9 | 10 | 'CONTACTS_REQUESTED', 11 | 'CONTACTS_RECEIVED', 12 | 'CONTACTS_RECEIVE_FAILED', 13 | 14 | 'CONTACT_ADD_REQUESTED', 15 | 'CONTACT_ADDED', 16 | 'CONTACT_ADD_FAILED', 17 | 18 | 'CONTACT_DELETE_REQUESTED', 19 | 'CONTACT_DELETED', 20 | 'CONTACT_DELETE_FAILED', 21 | 22 | 'CONTACT_FAV_REQUESTED', 23 | 'CONTACT_FAVED', 24 | 'CONTACT_FAV_FAILED', 25 | 26 | 'CONTACT_UNFAV_REQUESTED', 27 | 'CONTACT_UNFAVED', 28 | 'CONTACT_UNFAV_FAILED', 29 | 30 | 'MESSAGE_DISMISSED' 31 | ]; 32 | 33 | 34 | var Actions = {}; 35 | 36 | for(let actionName of actionsNames) { 37 | Actions[actionName] = actionName; 38 | } 39 | 40 | function dispatch(action, params) { 41 | reactor.dispatch(action, params); 42 | } 43 | 44 | 45 | class ActionCreators { 46 | static getContacts() { 47 | axios.get('http://localhost:3000/contacts') 48 | .then(response => dispatch(Actions.CONTACTS_RECEIVED, response.data)) 49 | .catch(error => dispatch(Actions.CONTACTS_RECEIVE_FAILED, error)); 50 | dispatch(Actions.CONTACTS_REQUESTED); 51 | } 52 | 53 | static addContact(contact) { 54 | axios.post('http://localhost:3000/contacts', contact) 55 | .then(response => dispatch(Actions.CONTACT_ADDED, response.data)) 56 | .catch(error => dispatch(Actions.CONTACT_ADD_FAILED, error)); 57 | dispatch(Actions.CONTACT_ADD_REQUESTED, contact.toJS()); 58 | } 59 | 60 | static deleteContact(contact) { 61 | axios.delete(`http://localhost:3000/contacts/${contact.get('id')}`) 62 | .then(response => dispatch(Actions.CONTACT_DELETED, contact.toJS())) 63 | .catch(error => dispatch(Actions.CONTACT_DELETE_FAILED, contact.toJS())); 64 | dispatch(Actions.CONTACT_DELETE_REQUESTED, contact.toJS()); 65 | } 66 | 67 | static favContact(contact) { 68 | axios.patch(`http://localhost:3000/contacts/${contact.get('id')}`, 69 | {fav: true}) 70 | .then(response => dispatch(Actions.CONTACT_FAVED, response.data)) 71 | .catch(error => dispatch(Actions.CONTACT_FAV_FAILED, contact.toJS())); 72 | dispatch(Actions.CONTACT_FAV_REQUESTED, contact.toJS()); 73 | } 74 | 75 | static unfavContact(contact) { 76 | axios.patch(`http://localhost:3000/contacts/${contact.get('id')}`, 77 | {fav: false}) 78 | .then(response => dispatch(Actions.CONTACT_UNFAVED, response.data)) 79 | .catch(error => dispatch(Actions.CONTACT_UNFAV_FAILED, contact.toJS())); 80 | dispatch(Actions.CONTACT_UNFAV_REQUESTED, contact.toJS()); 81 | } 82 | 83 | static selectContact(contactId) { 84 | dispatch(Actions.CONTACT_SELECTED, contactId); 85 | } 86 | 87 | static dismissMessage(userAction) { 88 | dispatch(Actions.MESSAGE_DISMISSED, userAction); 89 | } 90 | }; 91 | 92 | 93 | export { 94 | Actions, 95 | ActionCreators 96 | }; 97 | -------------------------------------------------------------------------------- /nuclear-test/client/components/app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react'; 4 | import Router from 'react-router'; 5 | import Mixin from 'react-mixin'; 6 | import cx from 'classnames'; 7 | 8 | import reactor from '../reactor'; 9 | import {messageGetter} from '../getters'; 10 | import {ActionCreators} from '../actions'; 11 | 12 | import 'bootstrap-sass-loader'; 13 | 14 | 15 | class App extends React.Component { 16 | getDataBindings() { 17 | return { 18 | message: messageGetter 19 | }; 20 | } 21 | 22 | handleCloseMessage() { 23 | ActionCreators.dismissMessage(true); 24 | clearTimeout(this.dismissedId); 25 | } 26 | 27 | render () { 28 | let contactsPageActive = this.isActive('contacts') || this.isActive('contact') || this.isActive('new-contact'); 29 | 30 | let message; 31 | if(this.state.message) { 32 | message = ( 33 |
    38 | 42 | {this.state.message.get('message')} 43 |
    44 | ); 45 | if(this.state.message.get('timeout')) { 46 | this.dismissedId = setTimeout( 47 | () => ActionCreators.dismissMessage(false), 48 | this.state.message.get('timeout') * 1000); 49 | } 50 | } 51 | return ( 52 |
    53 |
    54 | {message} 55 |
    56 | 65 |
    66 |
      67 |
    • 71 | 72 | Contacts 73 | 74 |
    • 75 |
    • 76 | 77 | Favorites 78 | 79 |
    • 80 |
    81 | 82 |
    83 |
    84 | ); 85 | } 86 | } 87 | 88 | Mixin.onClass(App, Router.State); 89 | Mixin.onClass(App, reactor.ReactMixin); 90 | 91 | export default App; 92 | -------------------------------------------------------------------------------- /nuclear-test/client/components/contact.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import {toImmutable} from 'nuclear-js'; 5 | import cn from 'classnames'; 6 | 7 | 8 | class Contact extends React.Component { 9 | render() { 10 | const defaultContact = { 11 | fav: false, 12 | name: '', 13 | email: '', 14 | address: '', 15 | phone: '' 16 | } 17 | let contact = this.props.contact || toImmutable(defaultContact); 18 | 19 | const dataFields = ['email', 'address', 'phone']; 20 | 21 | let rows = []; 22 | for(let field of dataFields) { 23 | rows.push({field}{contact.get(field)}); 24 | } 25 | 26 | return ( 27 |
    28 |
    29 |
    30 | 32 |
    33 |
    34 | 35 |
    36 |
    37 |

    {contact.get('name')}

    38 |
    39 |
    40 |
    41 | 42 | {rows} 43 |
    44 |
    45 |
    46 | ); 47 | } 48 | } 49 | 50 | 51 | export default Contact; 52 | -------------------------------------------------------------------------------- /nuclear-test/client/components/contacts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import Router from 'react-router'; 5 | import cn from 'classnames'; 6 | 7 | 8 | class ContactListItem extends React.Component { 9 | render() { 10 | let contact = this.props.contact; 11 | 12 | return ( 13 |
  • 14 |
    15 |
    16 | 18 |
    19 |
    20 | 21 |
    22 |
    23 | 25 |
    26 |
    27 | 28 |

    {contact.get('name')}

    29 |
    30 |
    31 |
    32 |
  • 33 | ); 34 | } 35 | 36 | handleDelete() { 37 | this.props.onDelete(this.props.contact); 38 | } 39 | 40 | handleFav() { 41 | let contact = this.props.contact; 42 | this.props.onFav(contact, !contact.get('fav')); 43 | } 44 | } 45 | 46 | 47 | export default ContactListItem; 48 | -------------------------------------------------------------------------------- /nuclear-test/client/components/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import App from './app'; 4 | import ContactListItem from './contacts'; 5 | import Contact from './contact'; 6 | import List from './list'; 7 | 8 | 9 | export { 10 | App, 11 | ContactListItem, 12 | Contact, 13 | List 14 | }; 15 | -------------------------------------------------------------------------------- /nuclear-test/client/components/list.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | 5 | 6 | class List extends React.Component { 7 | render() { 8 | return ( 9 |
      10 | {this.props.items} 11 |
    12 | ); 13 | } 14 | } 15 | 16 | 17 | export default List; 18 | -------------------------------------------------------------------------------- /nuclear-test/client/getters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const contactsGetter = [ 4 | ['contacts'], 5 | contacts => contacts.toList() 6 | ]; 7 | 8 | const currentContactGetter = [ 9 | ['contacts'], 10 | ['currentContact'], 11 | (contacts, id) => id ? contacts.get(id.toString()) : null 12 | ]; 13 | 14 | const favoritesGetter = [ 15 | ['contacts'], 16 | contacts => contacts.toList().filter(contact => contact.get('fav')) 17 | ]; 18 | 19 | const messageGetter = [ 20 | ['message'], 21 | message => message 22 | ]; 23 | 24 | 25 | export { 26 | contactsGetter, 27 | currentContactGetter, 28 | favoritesGetter, 29 | messageGetter 30 | }; 31 | -------------------------------------------------------------------------------- /nuclear-test/client/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import Router from 'react-router'; 5 | 6 | import routes from './routes'; 7 | 8 | import './stylesheets/app.css'; 9 | 10 | 11 | Router.run(routes, Router.HashLocation, (Root) => { 12 | React.render(, document.body); 13 | }); 14 | -------------------------------------------------------------------------------- /nuclear-test/client/pages/contact.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import Mixin from 'react-mixin'; 5 | 6 | import {Contact} from '../components/'; 7 | import reactor from '../reactor'; 8 | import {currentContactGetter} from '../getters'; 9 | import {ActionCreators} from '../actions'; 10 | 11 | 12 | class ContactPage extends React.Component { 13 | getDataBindings() { 14 | return { 15 | contact: currentContactGetter 16 | }; 17 | } 18 | 19 | static willTransitionTo(transition, params, query) { 20 | ActionCreators.selectContact(parseInt(params.contactId)); 21 | ActionCreators.getContacts(); 22 | } 23 | 24 | render() { 25 | return ( 26 |
    27 |
    28 | 29 |
    30 |
    31 | ); 32 | } 33 | } 34 | 35 | Mixin.onClass(ContactPage, reactor.ReactMixin); 36 | 37 | 38 | export default ContactPage; 39 | -------------------------------------------------------------------------------- /nuclear-test/client/pages/contacts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import Router from 'react-router'; 5 | import Mixin from 'react-mixin'; 6 | import cx from 'classnames'; 7 | 8 | import {ContactListItem, List} from '../components/'; 9 | import reactor from '../reactor'; 10 | import {contactsGetter} from '../getters'; 11 | import {ActionCreators} from '../actions'; 12 | 13 | 14 | class ContactsPage extends React.Component { 15 | getDataBindings() { 16 | return { 17 | contacts: contactsGetter 18 | }; 19 | } 20 | 21 | static willTransitionTo(transition, params, query) { 22 | ActionCreators.getContacts(); 23 | } 24 | 25 | render() { 26 | let items = this.state.contacts.toArray().map(contact => 27 | ); 29 | return ( 30 |
    31 | 38 | 39 |
    40 | ); 41 | } 42 | 43 | handleDelete(contact) { 44 | ActionCreators.deleteContact(contact); 45 | } 46 | 47 | handleFav(contact, fav) { 48 | if (fav) { 49 | ActionCreators.favContact(contact); 50 | } else { 51 | ActionCreators.unfavContact(contact); 52 | } 53 | 54 | } 55 | } 56 | 57 | Mixin.onClass(ContactsPage, reactor.ReactMixin); 58 | 59 | 60 | export default ContactsPage; 61 | -------------------------------------------------------------------------------- /nuclear-test/client/pages/favorites.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import Router from 'react-router'; 5 | import Mixin from 'react-mixin'; 6 | import cx from 'classnames'; 7 | 8 | import {ContactListItem, List} from '../components/'; 9 | import reactor from '../reactor'; 10 | import {favoritesGetter} from '../getters'; 11 | import {ActionCreators} from '../actions'; 12 | 13 | 14 | class FavoritesPage extends React.Component { 15 | getDataBindings() { 16 | return { 17 | contacts: favoritesGetter 18 | }; 19 | } 20 | 21 | static willTransitionTo(transition, params, query) { 22 | ActionCreators.getContacts(); 23 | } 24 | 25 | render() { 26 | let items = this.state.contacts.toArray().map(contact => 27 | ); 29 | return ( 30 |
    31 | 32 |
    33 | ); 34 | } 35 | 36 | handleFav(contact, fav) { 37 | if (fav) { 38 | ActionCreators.favContact(contact); 39 | } else { 40 | ActionCreators.unfavContact(contact); 41 | } 42 | 43 | } 44 | } 45 | 46 | Mixin.onClass(FavoritesPage, reactor.ReactMixin); 47 | 48 | 49 | export default FavoritesPage; 50 | -------------------------------------------------------------------------------- /nuclear-test/client/pages/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import ContactsPage from './contacts'; 4 | import ContactPage from './contact'; 5 | import NewContactPage from './newcontact'; 6 | import FavoritesPage from './favorites'; 7 | 8 | export { 9 | ContactsPage, 10 | ContactPage, 11 | NewContactPage, 12 | FavoritesPage 13 | }; 14 | -------------------------------------------------------------------------------- /nuclear-test/client/pages/newcontact.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import Addons from 'react/addons'; 5 | import Router from 'react-router'; 6 | import Mixin from 'react-mixin'; 7 | 8 | import {ActionCreators} from '../actions'; 9 | 10 | 11 | class NewContactPage extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | name: '', 16 | phone: '', 17 | email: '', 18 | address: '' 19 | }; 20 | } 21 | 22 | render() { 23 | return ( 24 |
    25 |
    26 |
    27 | 28 |
    29 | 31 |
    32 |
    33 |
    34 | 35 |
    36 | 38 |
    39 |
    40 |
    41 | 42 |
    43 | 45 |
    46 |
    47 |
    48 | 49 |
    50 | 52 |
    53 |
    54 | 55 |
    56 |
    57 | ); 58 | } 59 | 60 | handleSubmit(e) { 61 | e.preventDefault(); 62 | let contact = { 63 | name: this.state.name, 64 | fav: false, 65 | email: this.state.email, 66 | address: this.state.address, 67 | phone: this.state.phone 68 | }; 69 | ActionCreators.addContact(contact); 70 | this.transitionTo('contacts'); 71 | } 72 | } 73 | 74 | Mixin.onClass(NewContactPage, Addons.addons.LinkedStateMixin); 75 | Mixin.onClass(NewContactPage, Router.Navigation); 76 | 77 | 78 | export default NewContactPage; 79 | -------------------------------------------------------------------------------- /nuclear-test/client/reactor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Nuclear from 'nuclear-js'; 4 | 5 | import {ContactStore, CurrentContactStore, MessageStore} from './stores/'; 6 | 7 | 8 | const reactor = new Nuclear.Reactor({debug: true}); 9 | 10 | reactor.registerStores({ 11 | contacts: new ContactStore(), 12 | currentContact: new CurrentContactStore(), 13 | message: new MessageStore() 14 | }); 15 | 16 | export default reactor; 17 | -------------------------------------------------------------------------------- /nuclear-test/client/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import Router from 'react-router'; 5 | 6 | import {App} from './components/'; 7 | import {ContactsPage, ContactPage, NewContactPage, FavoritesPage} from './pages/'; 8 | 9 | 10 | // declare our routes and their hierarchy 11 | export default ( 12 | 13 | 14 | 15 | 16 | 18 | 20 | 21 | 22 | 23 | ); 24 | -------------------------------------------------------------------------------- /nuclear-test/client/stores/contacts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {Store, toImmutable} from 'nuclear-js'; 4 | 5 | import {Actions} from '../actions'; 6 | 7 | 8 | class ContactStore extends Store { 9 | getInitialState() { 10 | return toImmutable({}); 11 | } 12 | 13 | initialize() { 14 | this.on(Actions.CONTACTS_RECEIVED, this.contactsReceived); 15 | this.on(Actions.CONTACT_ADDED, this.contactAdded); 16 | this.on(Actions.CONTACT_DELETED, this.contactDeleted); 17 | this.on(Actions.CONTACT_FAVED, this.contactFaved); 18 | this.on(Actions.CONTACT_UNFAVED, this.contactFaved); 19 | } 20 | 21 | contactsReceived(state, contacts) { 22 | let contactsMap = {}; 23 | for(let contact of contacts) { 24 | contactsMap[contact.id] = contact; 25 | } 26 | return toImmutable(contactsMap); 27 | } 28 | 29 | contactAdded(state, contact) { 30 | let map = {}; 31 | map[contact.id] = contact; 32 | return state.merge(toImmutable(map)); 33 | } 34 | 35 | contactDeleted(state, contact) { 36 | return state.delete(contact.id); 37 | } 38 | 39 | contactFaved(state, contact) { 40 | let map = {}; 41 | map[contact.id] = contact; 42 | return state.merge(toImmutable(map)); 43 | } 44 | } 45 | 46 | 47 | class CurrentContactStore extends Store { 48 | getInitialState() { 49 | return null; 50 | } 51 | 52 | initialize() { 53 | this.on(Actions.CONTACT_SELECTED, this.contactSelected); 54 | } 55 | 56 | contactSelected(state, contactId) { 57 | return contactId; 58 | } 59 | } 60 | 61 | 62 | export { 63 | ContactStore, 64 | CurrentContactStore 65 | }; 66 | -------------------------------------------------------------------------------- /nuclear-test/client/stores/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {ContactStore, CurrentContactStore} from './contacts'; 4 | import MessageStore from './messages'; 5 | 6 | export { 7 | ContactStore, 8 | CurrentContactStore, 9 | MessageStore 10 | }; 11 | -------------------------------------------------------------------------------- /nuclear-test/client/stores/messages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {Store, toImmutable} from 'nuclear-js'; 4 | 5 | import {Actions} from '../actions'; 6 | 7 | 8 | class MessageStore extends Store { 9 | getInitialState() { 10 | return null; 11 | } 12 | 13 | initialize() { 14 | this.on(Actions.CONTACTS_REQUESTED, this.contactsRequested); 15 | this.on(Actions.CONTACTS_RECEIVED, this.contactsReceived); 16 | this.on(Actions.CONTACTS_RECEIVE_FAILED, this.contactsReceiveFailed); 17 | 18 | this.on(Actions.CONTACT_ADD_REQUESTED, this.contactAddRequested); 19 | this.on(Actions.CONTACT_ADDED, this.contactAdded); 20 | this.on(Actions.CONTACT_ADD_FAILED, this.contactAddFailed); 21 | 22 | this.on(Actions.CONTACT_DELETE_REQUESTED, this.contactDeleteRequested); 23 | this.on(Actions.CONTACT_DELETED, this.contactDeleted); 24 | this.on(Actions.CONTACT_DELETE_FAILED, this.contactDeleteFailed); 25 | 26 | this.on(Actions.CONTACT_FAV_REQUESTED, this.contactFavRequested); 27 | this.on(Actions.CONTACT_FAVED, this.contactFaved); 28 | this.on(Actions.CONTACT_FAV_FAILED, this.contactFavFailed); 29 | 30 | this.on(Actions.CONTACT_UNFAV_REQUESTED, this.contactUnfavRequested); 31 | this.on(Actions.CONTACT_UNFAVED, this.contactUnfaved); 32 | this.on(Actions.CONTACT_UNFAV_FAILED, this.contactUnfavFailed); 33 | 34 | this.on(Actions.MESSAGE_DISMISSED, this.messageDismissed); 35 | } 36 | 37 | contactsRequested(state) { 38 | return toImmutable({ 39 | type: 'info', 40 | message: 'Loading contacts...', 41 | }); 42 | } 43 | 44 | contactsReceived(state, contacts) { 45 | return null; 46 | } 47 | 48 | contactsReceiveFailed(state, error) { 49 | return toImmutable({ 50 | type: 'error', 51 | message: 'Fail to load contacts', 52 | timeout: 5 53 | }); 54 | } 55 | 56 | contactAddRequested(state, contact) { 57 | return toImmutable({ 58 | type: 'info', 59 | message: `Adding ${contact.name}` 60 | }); 61 | } 62 | 63 | contactAdded(state, contact) { 64 | return toImmutable({ 65 | type: 'success', 66 | message: 'Contact added', 67 | timeout: 5 68 | }); 69 | } 70 | 71 | contactAddFailed(state, error) { 72 | return toImmutable({ 73 | type: 'error', 74 | message: 'Failed to add contact', 75 | timeout: 5 76 | }); 77 | } 78 | 79 | contactDeleteRequested(state, contact) { 80 | return toImmutable({ 81 | type: 'info', 82 | message: `Deleting ${contact.name}` 83 | }); 84 | } 85 | 86 | contactDeleted(state, contact) { 87 | return toImmutable({ 88 | type: 'success', 89 | message: 'Contact deleted', 90 | timeout: 5 91 | }); 92 | } 93 | 94 | contactDeleteFailed(state, error) { 95 | return toImmutable({ 96 | type: 'error', 97 | message: 'Failed to delete contact', 98 | timeout: 5 99 | }); 100 | } 101 | 102 | contactFavRequested(state, contact) { 103 | return toImmutable({ 104 | type: 'info', 105 | message: `Faving ${contact.name}` 106 | }); 107 | } 108 | 109 | contactFaved(state, contact) { 110 | return toImmutable({ 111 | type: 'success', 112 | message: `${contact.name} is a favorite`, 113 | timeout: 5 114 | }); 115 | } 116 | 117 | contactFavFailed(state, error) { 118 | return toImmutable({ 119 | type: 'error', 120 | message: 'Failed to fav', 121 | timeout: 5 122 | }); 123 | } 124 | 125 | contactUnfavRequested(state, contact) { 126 | return toImmutable({ 127 | type: 'info', 128 | message: `Unfaving ${contact.name}` 129 | }); 130 | } 131 | 132 | contactUnfaved(state, contact) { 133 | return toImmutable({ 134 | type: 'success', 135 | message: `${contact.name} is not a favorite`, 136 | timeout: 5 137 | }); 138 | } 139 | 140 | contactUnfavFailed(state, error) { 141 | return toImmutable({ 142 | type: 'error', 143 | message: 'Failed to unfav', 144 | timeout: 5 145 | }); 146 | } 147 | 148 | messageDismissed(state) { 149 | return null; 150 | } 151 | } 152 | 153 | 154 | export default MessageStore; 155 | -------------------------------------------------------------------------------- /nuclear-test/client/stylesheets/app.css: -------------------------------------------------------------------------------- 1 | .icon { 2 | padding-right: 10px; 3 | } 4 | 5 | .messages { 6 | position: fixed; 7 | width: 40%; 8 | top: 10px; 9 | left: 30%; 10 | z-index: 9999; 11 | text-align: center; 12 | } 13 | 14 | .messages .alert { 15 | display: inline-block; 16 | } 17 | 18 | .messages .close { 19 | margin-left: 10px; 20 | } 21 | -------------------------------------------------------------------------------- /nuclear-test/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "contacts": [ 3 | { 4 | "id": 5, 5 | "fav": true, 6 | "name": "Chelsey Dietrich", 7 | "email": "Lucio_Hettinger@annie.ca", 8 | "address": "Skiles Walks Suite 351, Roscoeview", 9 | "phone": "(254)954-1289" 10 | }, 11 | { 12 | "id": 6, 13 | "fav": true, 14 | "name": "Mrs. Dennis Schulist", 15 | "email": "Karley_Dach@jasper.info", 16 | "address": "Norberto Crossing Apt. 950, South Christy", 17 | "phone": "1-477-935-8478 x6430" 18 | }, 19 | { 20 | "id": 8, 21 | "fav": false, 22 | "name": "Nicholas Runolfsdottir V", 23 | "email": "Sherwood@rosamond.me", 24 | "address": "Ellsworth Summit Suite 729, Aliyaview", 25 | "phone": "586.493.6943 x140" 26 | }, 27 | { 28 | "id": 9, 29 | "fav": false, 30 | "name": "Glenna Reichert", 31 | "email": "Chaim_McDermott@dana.io", 32 | "address": "Dayna Park Suite 449, Bartholomebury", 33 | "phone": "(775)976-6794 x41206" 34 | }, 35 | { 36 | "id": 10, 37 | "fav": true, 38 | "name": "Clementina DuBuque", 39 | "email": "Rey.Padberg@karina.biz", 40 | "address": "Kattie Turnpike Suite 198, Lebsackbury", 41 | "phone": "024-648-3804" 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /nuclear-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuclear-test", 3 | "version": "0.0.0", 4 | "description": "A nuclearjs test", 5 | "main": "client/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "$(npm bin)/webpack", 9 | "run": "sh run.sh" 10 | }, 11 | "author": "Diego Gadola ", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "axios": "^0.5.4", 15 | "babel-core": "^5.6.15", 16 | "babel-loader": "^5.2.2", 17 | "bootstrap": "^3.3.5", 18 | "bootstrap-sass": "^3.3.5", 19 | "bootstrap-sass-loader": "^1.0.4", 20 | "bootstrap-webpack": "0.0.3", 21 | "classnames": "^2.1.3", 22 | "css-loader": "^0.15.1", 23 | "file-loader": "^0.8.4", 24 | "jquery": "^2.1.4", 25 | "json-server": "^0.7.25", 26 | "node-libs-browser": "^0.5.2", 27 | "node-sass": "^3.2.0", 28 | "nuclear-js": "^1.0.5", 29 | "react": "^0.13.3", 30 | "react-mixin": "^1.5.0", 31 | "react-router": "^0.13.3", 32 | "sass-loader": "^1.0.2", 33 | "style-loader": "^0.12.3", 34 | "url-loader": "^0.5.6", 35 | "webpack": "^1.10.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /nuclear-test/readme.md: -------------------------------------------------------------------------------- 1 | ``` 2 | npm install 3 | npm run build 4 | npm run run 5 | ``` 6 | 7 | Go to [http://localhost:8000][localhost8000] on your browser. 8 | 9 | [localhost8000]: http://localhost:8000 'localhost' 10 | -------------------------------------------------------------------------------- /nuclear-test/run.sh: -------------------------------------------------------------------------------- 1 | echo 'Starting contacts API ...' 2 | $(npm bin)/json-server -w db.json &> /dev/null & 3 | export JS_PID=$! 4 | echo 'API started' 5 | echo 'Starting frontend server ...' 6 | cd build 7 | python -m SimpleHTTPServer &> /dev/null & 8 | export HTTP_PID=$! 9 | echo 'frontend started' 10 | trap 'kill -2 $JS_PID && kill -9 $HTTP_PID && echo API and frontend stopped.' SIGINT 11 | trap 'kill -9 $JS_PID && kill -9 $HTTP_PID && echo API and frontend stopped.' SIGKILL 12 | wait 13 | -------------------------------------------------------------------------------- /nuclear-test/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | var path = require('path'); 5 | 6 | 7 | var sassPaths = [ 8 | 'client/stylesheets/', 9 | ] 10 | .map(function(m){ return path.resolve(__dirname, m); }) 11 | .join('&includePaths[]='); 12 | 13 | module.exports = { 14 | context: __dirname + '/client', 15 | entry: { 16 | main: "./index.js", 17 | vendors: ['react', 'react-router', 'nuclear-js', 'jquery', 'classnames', 'react-mixin', 'axios'] 18 | }, 19 | 20 | output: { 21 | path: "./build", 22 | filename: "[name].js" 23 | }, 24 | 25 | resolve: { 26 | modulesDirectories: [ 'node_modules' ], 27 | extensions: ['', '.js', '.jsx'], 28 | }, 29 | 30 | plugins: [ 31 | new webpack.optimize.DedupePlugin(), 32 | new webpack.optimize.CommonsChunkPlugin({ 33 | name:'vendors' 34 | }), 35 | new webpack.ProvidePlugin({ 36 | $: 'jquery', 37 | jQuery: 'jquery', 38 | 'window.jQuery': 'jquery' 39 | }) 40 | ], 41 | 42 | module: { 43 | loaders: [ 44 | { test: /\.css$/, loader: "style!css" }, 45 | { test: /\.scss$/, loader: "style!css!sass?includePaths[]=" + sassPaths}, 46 | { test: /\.jsx$|\.js$/, loader: 'babel'}, 47 | { test: /\.woff$/, loader: "url-loader?limit=10000&mimetype=application/font-woff" }, 48 | { test: /\.ttf$/, loader: "file-loader" }, 49 | { test: /\.eot$/, loader: "file-loader" }, 50 | { test: /\.svg$/, loader: "file-loader" } 51 | ] 52 | } 53 | } 54 | --------------------------------------------------------------------------------