├── .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 |
29 |
30 | |
31 |
32 |
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 | {day} |
206 | ))}
207 |
208 | {_.range(24).map((hour) => (
209 |
210 | {DAYS.map((day) => (
211 | |
212 | ))}
213 |
214 | ))}
215 |
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 |
--------------------------------------------------------------------------------