├── .gitignore ├── README.md ├── app.jsx ├── index.html ├── package.json ├── style.css └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | app.js 2 | npm-debug.log 3 | node_modules 4 | .module-cache 5 | .*.sw* 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is just a little sandbox for me to play with React + Angular together. 2 | 3 | To run the demo: 4 | 5 | ``` 6 | cd angular_react_directive_example 7 | npm install 8 | npm run dev 9 | Browse to http://localhost:8080 10 | ``` 11 | -------------------------------------------------------------------------------- /app.jsx: -------------------------------------------------------------------------------- 1 | require('./style.css'); 2 | var angular = require('angular'); 3 | var _ = require('lodash'); 4 | 5 | var DAYS = _.range(1, 32).map((day) => ("Oct " + day)); 6 | 7 | var randomMillis = function() { 8 | return Math.floor(Math.random() * 10000); 9 | } 10 | 11 | angular.module("myapp", []). 12 | directive("myCalendar", function() { 13 | return { 14 | restrict: 'E', 15 | scope: true, 16 | replace: true, 17 | template:` 18 |
19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 31 | 32 |
24 | {{day}} 25 |
29 | 30 |
33 | 34 | `, 35 | link: function(scope, element, attrs) { 36 | scope.loaded = false; 37 | scope.hours = _.range(24); 38 | scope.days = DAYS; 39 | 40 | scope.searchAll = function() { 41 | scope.$broadcast('allSearchRequested'); 42 | } 43 | 44 | scope.load = function() { 45 | scope.loaded = true; 46 | } 47 | } 48 | } 49 | }). 50 | directive("myCalendarCell", function() { 51 | return { 52 | restrict: 'E', 53 | replace: true, 54 | scope: true, 55 | template: ` 56 |
57 |
58 | {{hour}}:00 59 |
60 |
61 | ... 62 |
63 |
64 |
{{status.searchResults.options}}
65 |
results
66 |
67 |
68 | `, 69 | link: function(scope, element, attrs) { 70 | scope.day = attrs.day; 71 | scope.hour = attrs.hour; 72 | scope.status = {}; 73 | }, 74 | controller: function($scope, $rootScope, $timeout) { 75 | $scope.showSpinner = function() { 76 | return $scope.status.isSearching; 77 | } 78 | $scope.showHour = function() { 79 | return !$scope.status.isSearching && !$scope.status.searchResults; 80 | } 81 | $scope.showSearchResults = function() { 82 | return $scope.status.searchResults; 83 | } 84 | $scope.cellClass = function() { 85 | if ($scope.status.isSearching) { 86 | return 'searching'; 87 | } else if ($scope.status.searchResults) { 88 | if ($scope.status.searchResults.options > 3) { 89 | return 'good-results' 90 | } else if ($scope.status.searchResults.options > 1) { 91 | return 'weak-results' 92 | } else { 93 | return 'bad-results' 94 | } 95 | } 96 | } 97 | $scope.cellClicked = function() { 98 | delete $scope.status.searchResults; 99 | $scope.status.isSearching = true; 100 | // Simulate an AJAX request: 101 | $timeout(function() { 102 | $scope.status.isSearching = false; 103 | $scope.status.searchResults = {options: Math.floor(Math.random() * 5)}; 104 | }, randomMillis()); 105 | } 106 | $scope.$on('allSearchRequested', function() { 107 | $scope.cellClicked(); 108 | }); 109 | } 110 | } 111 | }). 112 | 113 | directive("myCalendarReact", function() { 114 | return { 115 | restrict: 'E', 116 | scope: true, 117 | template: '
', 118 | link: function(scope, element, attrs) { 119 | // React and Angular, living in harmony: 120 | React.render(, element[0]); 121 | } 122 | } 123 | }); 124 | 125 | 126 | /* React Components */ 127 | 128 | var EventEmitter = require('events'); 129 | var React = require('react/addons'); 130 | 131 | var Cell = React.createClass({ 132 | render: function() { 133 | if (this.state.isSearching) { 134 | return ( 135 | 136 |
137 | ... 138 |
139 | 140 | ); 141 | } else if (this.state.searchResults) { 142 | var options = this.state.searchResults.options; 143 | var classes = React.addons.classSet({ 144 | 'good-results': options > 3, 145 | 'weak-results': options > 1 && options <= 3, 146 | 'bad-results' : options >= 0 && options <= 1 147 | }); 148 | return ( 149 | 150 |
151 |
{this.state.searchResults}
152 |
results
153 |
154 | 155 | ); 156 | } else { 157 | return ( 158 | 159 |
160 | {this.props.hour}:00 161 |
162 | 163 | ); 164 | } 165 | }, 166 | getInitialState: function() { 167 | return { 168 | isSearching: false, 169 | searchResults: null 170 | } 171 | }, 172 | clicked: function() { 173 | this.search(); 174 | }, 175 | search: function() { 176 | var self = this; 177 | self.setState({ 178 | isSearching: true, 179 | searchResults: {options: null} 180 | }); 181 | setTimeout(function() { 182 | self.setState({ 183 | isSearching: false, 184 | searchResults: {options: Math.floor(Math.random() * 5)} 185 | }); 186 | }, randomMillis()); 187 | }, 188 | componentWillMount: function() { 189 | this.props.events.on('search', () => this.search()); 190 | } 191 | }); 192 | 193 | var Calendar = React.createClass({ 194 | render: function() { 195 | return ( 196 |
197 | {this.state.isLoaded || 198 | } 199 | {this.state.isLoaded && 200 | } 201 | {this.state.isLoaded && 202 | 203 | 204 | {DAYS.map((day) => ( 205 | 206 | ))} 207 | 208 | {_.range(24).map((hour) => ( 209 | 210 | {DAYS.map((day) => ( 211 | 212 | ))} 213 | 214 | ))} 215 |
{day}
216 | } 217 |
218 | ) 219 | }, 220 | componentWillMount: function() { 221 | this.events = new EventEmitter(); 222 | this.events.setMaxListeners(0); 223 | }, 224 | getInitialState: function() { 225 | return { 226 | isLoaded: false 227 | } 228 | }, 229 | load: function() { 230 | this.setState({isLoaded: true}); 231 | }, 232 | searchAll: function(args) { 233 | this.events.emit('search'); 234 | } 235 | }); 236 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Angular + React = ♥ 5 | 6 | 7 | 8 | 9 |

Vanilla Angular Directive

10 | 11 | 12 |

Angular Directive with React Inside

13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-react-directive-example", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "webpack-dev-server --progress --colors --port 8080" 6 | }, 7 | "dependencies": { 8 | "angular": "^1.4.0-beta.5", 9 | "events": "^1.0.2", 10 | "lodash": "^3.4.0", 11 | "react": "^0.12.2" 12 | }, 13 | "devDependencies": { 14 | "css-loader": "^0.9.1", 15 | "jsx-loader": "^0.12.2", 16 | "style-loader": "^0.8.3", 17 | "webpack": "^1.6.0", 18 | "webpack-dev-server": "^1.7.0" 19 | }, 20 | "author": "Dave Smith", 21 | "license": "ISC" 22 | } 23 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: arial; 3 | margin-bottom: 500px; 4 | } 5 | 6 | .btn { 7 | margin-bottom: 10px; 8 | } 9 | 10 | table { 11 | border-right: 2px solid gray; 12 | border-spacing: 0; 13 | border-collapse: collapse; 14 | } 15 | 16 | .day-header { 17 | border: 1px solid gray; 18 | white-space: nowrap; 19 | } 20 | 21 | .hour-cell:hover > div { 22 | background-color: #559 !important; 23 | color: white; 24 | } 25 | 26 | .hour-cell { 27 | width: 120px; 28 | text-align: center; 29 | border-left: 1px solid gray; 30 | border-bottom: 1px solid #eee; 31 | cursor: pointer; 32 | height: 30px; 33 | padding: 0; 34 | } 35 | 36 | .hour-cell > div { 37 | height: 100%; 38 | } 39 | 40 | .hour-cell .time { 41 | padding-top: 8px; /* poor man's vertical centering */ 42 | color: #555; 43 | } 44 | 45 | .hour-cell .searching { 46 | color: #88d; 47 | font-size: 12px; 48 | padding-top: 12px; /* poor man's vertical centering */ 49 | } 50 | 51 | .hour-cell .result-label { 52 | font-size: 10px; 53 | } 54 | 55 | .hour-cell .good-results { 56 | color: #090; 57 | background: #efffef; 58 | } 59 | 60 | .hour-cell .weak-results { 61 | color: orange; 62 | background: #ffffef; 63 | } 64 | 65 | .hour-cell .bad-results { 66 | color: red; 67 | background: #ffefef; 68 | } 69 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './app.jsx', 3 | output: { 4 | publicPath: 'http://localhost:8080/assets' 5 | }, 6 | module: { 7 | loaders: [ 8 | {test: /\.jsx$/, loader: 'jsx-loader?insertPragma=React.DOM&harmony'}, 9 | {test: /\.css$/, loader: 'style-loader!css-loader'} 10 | ] 11 | }, 12 | } 13 | --------------------------------------------------------------------------------