├── .bowerrc
├── .gitignore
├── demo
├── demo.js
├── hello.js
└── index.html
├── .travis.yml
├── Gruntfile.js
├── bower.json
├── .jshintrc
├── package.json
├── LICENSE
├── src
└── angular-react.js
├── karma.conf.js
├── README.md
└── tests
└── angular-react.spec.js
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "bower_components"
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .sw*
3 | .idea/
4 | .tmp/
5 | npm_debug.log
6 | bower_components/
7 | node_modules/
--------------------------------------------------------------------------------
/demo/demo.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('angular-react')
4 | .controller('DemoCtrl', function ($scope, $react) {
5 | $scope.data = {data: 'World'};
6 | });
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.10"
4 |
5 | before_script:
6 | - "export DISPLAY=:99.0"
7 | - "sh -e /etc/init.d/xvfb start"
8 | - npm install --quiet -g grunt-cli karma-cli bower
9 | - npm install
10 | - bower install
--------------------------------------------------------------------------------
/demo/hello.js:
--------------------------------------------------------------------------------
1 | /** @jsx React.DOM */
2 |
3 | angular.module('angular-react')
4 | .directive('hello', function ($react) {
5 | return $react.createClass('hello', {
6 | render: function () {
7 | var person = this.props.person || 'World';
8 | return (
9 |
10 | Hello {person}!
11 |
12 | );
13 | }
14 | });
15 | });
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function (grunt) {
4 | require('load-grunt-tasks')(grunt);
5 |
6 | grunt.initConfig({
7 | jshint: {
8 | options: {
9 | jshintrc: true
10 | },
11 | all: [
12 | 'Gruntfile.js',
13 | 'src/angular-react.js',
14 | 'tests/angular-react.spec.js',
15 | 'demo/demo.js'
16 | ]
17 | },
18 | karma: {
19 | unit: {
20 | configFile: 'karma.conf.js',
21 | autoWatch: false,
22 | singleRun: true,
23 | logLevel: 'ERROR'
24 | }
25 | }
26 | });
27 |
28 | grunt.registerTask('test', ['jshint', 'karma:unit']);
29 | };
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-react",
3 | "version": "0.2.0",
4 | "authors": [
5 | "Wesley Cho "
6 | ],
7 | "description": "ReactJS with Angular",
8 | "main": "src/angular-react.js",
9 | "keywords": [
10 | "react",
11 | "angular",
12 | "angular-react"
13 | ],
14 | "license": "MIT",
15 | "ignore": [
16 | "**/.*",
17 | "node_modules",
18 | "bower_components",
19 | "app/bower_components",
20 | "test",
21 | "tests"
22 | ],
23 | "dependencies": {
24 | "angular": "~1.2",
25 | "react": "~0.11"
26 | },
27 | "devDependencies": {
28 | "angular-mocks": "~1.2"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "curly": true,
3 | "immed": true,
4 | "latedef": "nofunc",
5 | "newcap": true,
6 | "noarg": true,
7 | "sub": true,
8 | "boss": true,
9 | "eqnull": true,
10 | "quotmark": "single",
11 | "trailing": true,
12 | "validthis": true,
13 | "strict": false,
14 | "globalstrict": true,
15 | "globals": {
16 | "angular": true,
17 | "React": true,
18 | "window": true,
19 | "jasmine": true,
20 | "ddescribe": true,
21 | "describe": true,
22 | "it": true,
23 | "iit": true,
24 | "xit": true,
25 | "expect": true,
26 | "module": true,
27 | "inject": true,
28 | "beforeEach": true,
29 | "require": true,
30 | "spyOn": true
31 | }
32 | }
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Demo of using ReactJS with Angular
5 |
6 |
7 | Example with two-way data binding
8 |
9 |
10 |
11 |
12 | Example without two-way binding
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-react",
3 | "version": "0.2.0",
4 | "description": "ReactJS with Angular",
5 | "main": "src/angular-react.js",
6 | "scripts": {
7 | "test": "grunt test"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git://github.com/wesleycho/angular-react.git"
12 | },
13 | "keywords": [
14 | "angular",
15 | "react",
16 | "reactjs",
17 | "react.js",
18 | "angular-react"
19 | ],
20 | "author": "Wesley Cho",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/wesleycho/angular-react/issues"
24 | },
25 | "homepage": "https://github.com/wesleycho/angular-react",
26 | "dependencies": {
27 | "grunt": "~0.4.2",
28 | "grunt-contrib-clean": "~0.6.0",
29 | "grunt-contrib-jshint": "~0.10.0",
30 | "grunt-contrib-uglify": "~0.6.0",
31 | "grunt-contrib-jshint": "~0.10.0",
32 | "grunt-karma": "~0.9.0",
33 | "load-grunt-tasks": "~0.6.0",
34 | "karma-firefox-launcher": "~0.1.3"
35 | },
36 | "devDependencies": {
37 | "karma-chrome-launcher": "^0.1.4",
38 | "karma-jasmine": "^0.1.5"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Wesley Cho
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/angular-react.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | (function(angular, React) {
4 |
5 | angular.module('angular-react', []).value('React', React)
6 | .factory('$react', ['React', function (React) {
7 |
8 | var components = {};
9 | var $react = {
10 | createClass: createReactDirective,
11 | renderComponent: renderComponent
12 | };
13 |
14 | return $react;
15 |
16 | function createReactDirective(componentName, reactClass, options) {
17 |
18 | if(!components[componentName]){
19 | components[componentName] = React.createClass(reactClass);
20 | }
21 |
22 | var component = components[componentName];
23 |
24 | var directive = {
25 | restrict: 'EA',
26 | link: renderComponent(component)
27 | };
28 |
29 | if (angular.isObject(options)) {
30 | angular.extend(directive, options);
31 | if (directive.compile) {
32 | delete directive.link;
33 | }
34 | }
35 |
36 | return directive;
37 | }
38 |
39 | function renderComponent(component) {
40 | return function (scope, elem, attrs) {
41 | var renderPostponed = false;
42 | if (attrs.props) {
43 | scope.$watch(function () {
44 | return attrs.props;
45 | }, function() {
46 | if (!renderPostponed) {
47 | renderPostponed = true;
48 | scope.$$postDigest(postponedRender);
49 | }
50 | }, true);
51 | } else {
52 | postponedRender();
53 | }
54 |
55 | scope.$on('$destroy', function() {
56 | React.unmountComponentAtNode(elem[0]);
57 | });
58 |
59 | function postponedRender() {
60 | renderPostponed = false;
61 | React.renderComponent(component(scope[attrs.props]), elem[0]);
62 | }
63 | };
64 | }
65 | }]);
66 | })(angular, React);
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // Generated on Mon Sep 01 2014 06:33:27 GMT-0700 (PDT)
3 |
4 | module.exports = function(config) {
5 | config.set({
6 |
7 | // base path that will be used to resolve all patterns (eg. files, exclude)
8 | basePath: '',
9 |
10 |
11 | // frameworks to use
12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
13 | frameworks: ['jasmine'],
14 |
15 |
16 | // list of files / patterns to load in the browser
17 | files: [
18 | 'bower_components/react/react-with-addons.js',
19 | 'bower_components/angular/angular.js',
20 | 'bower_components/angular-mocks/angular-mocks.js',
21 | 'src/angular-react.js',
22 | 'tests/**/*.spec.js'
23 | ],
24 |
25 |
26 | // list of files to exclude
27 | exclude: [
28 | ],
29 |
30 |
31 | // preprocess matching files before serving them to the browser
32 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
33 | preprocessors: {
34 | },
35 |
36 |
37 | // test results reporter to use
38 | // possible values: 'dots', 'progress'
39 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
40 | reporters: ['progress'],
41 |
42 |
43 | // web server port
44 | port: 9876,
45 |
46 |
47 | // enable / disable colors in the output (reporters and logs)
48 | colors: true,
49 |
50 |
51 | // level of logging
52 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
53 | logLevel: config.LOG_INFO,
54 |
55 |
56 | // enable / disable watching file and executing tests whenever any file changes
57 | autoWatch: true,
58 |
59 |
60 | // start these browsers
61 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
62 | browsers: ['Firefox'],
63 |
64 |
65 | // Continuous Integration mode
66 | // if true, Karma captures browsers, runs the tests and exits
67 | singleRun: false
68 | });
69 | };
70 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # angular-react [](https://travis-ci.org/wesleycho/angular-react)
2 |
3 | This project is for those wishing to integrate [AngularJS](https://angularjs.org) using [ReactJS](http://facebook.github.io/react/), a performant view layer that can play well with a popular MV* framework for complex web applications.
4 |
5 | This is currently a rough WIP.
6 |
7 | ## Setup
8 |
9 | * (Recommended) Install `react-tools` via `npm install -g react-tools`
10 | * (Recommended) Run `jsx --watch src/ dest/` (source and destination directories - stub paths as needed)
11 |
12 | ## Usage
13 |
14 | To use this library, one must inject the `$react` helper service into your directive. `$react` is a wrapper around the `React` api that implements a createClass method, which registers the React component class and creates a directive object. If the component is already registered, it will return you the component.
15 |
16 | ### Example
17 |
18 |
19 | /* hello directive - lives in separate file for JSX to compile */
20 | module.directive('hello', function ($react) {
21 | return $react.createClass('hello', {
22 | render: function () {
23 | /** @jsx React.DOM */
24 | var person = this.props.person || 'World';
25 | return (
26 | <div>Hello {person}!</div>
27 | );
28 | }
29 | });
30 | });
31 |
32 | /* Scripts */
33 | module.controller('DemoCtrl', function ($scope) {
34 | $scope.props = {
35 | person: 'Wesley'
36 | };
37 | });
38 |
39 | /* html */
40 | <hello></hello> // renders <div>Hello World!</div>
41 | <hello props="props"></hello> // renders <div>Hello Wesley!</div>
42 |
43 |
44 | ## API
45 |
46 | `$react.createClass(componentName, reactClass, options)` - function that takes a componentName, React component class, and options and returns a directive object back
47 | * componentName: String
48 | * reactClass: React class - standard input for [React.createClass](http://facebook.github.io/react/docs/top-level-api.html#react.createclass)
49 | * options: Object. Standard input for [directive object definitions](https://docs.angularjs.org/api/ng/service/$compile)
50 |
51 | `$react.renderComponent(scope, element, attributes)` - function that takes a $scope, element, and attributes. These are the same arguments passed in by a linking function in a directive.
--------------------------------------------------------------------------------
/tests/angular-react.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('angular-react', function () {
4 | describe('Factory: $react', function () {
5 | var $react,
6 | bar = {
7 | render: function () {
8 | return React.DOM.div('bar');
9 | }
10 | };
11 |
12 | beforeEach(module('angular-react'));
13 | beforeEach(inject(function (_$react_) {
14 | $react = _$react_;
15 | }));
16 |
17 | describe('createClass', function () {
18 | it('should register a class successfully', function () {
19 | expect($react.createClass('foo', bar)).toEqual(jasmine.objectContaining({
20 | restrict: 'EA',
21 | link: jasmine.any(Function)
22 | }));
23 | });
24 |
25 | it('should take options to override the defaults', function () {
26 | var reactDirective = $react.createClass('foo', bar, {
27 | restrict: 'A',
28 | scope: true
29 | });
30 | expect(reactDirective).toEqual(jasmine.objectContaining({
31 | restrict: 'A',
32 | scope: true,
33 | link: jasmine.any(Function)
34 | }));
35 | });
36 |
37 | it('should override the linking function with a compile function', function () {
38 | var compile = function () {
39 | return 'moo';
40 | };
41 | var reactDirective = $react.createClass('foo', bar, {
42 | compile: compile
43 | });
44 | expect(reactDirective).toEqual(jasmine.objectContaining({
45 | restrict: 'EA',
46 | compile: compile
47 | }));
48 | });
49 | });
50 |
51 | describe('renderComponent', function () {
52 | var component, scope, elem, attrs;
53 |
54 | beforeEach(function () {
55 | component = React.createClass(bar);
56 | inject(function ($rootScope) {
57 | scope = $rootScope.$new();
58 | elem = angular.element('');
59 | attrs = {};
60 | });
61 | });
62 |
63 | it('should run a first time render', function () {
64 | spyOn(React, 'renderComponent');
65 | $react.renderComponent(component)(scope, elem, attrs);
66 |
67 | expect(scope.$$watchers).toBe(null);
68 | expect(scope.$$listeners.$destroy.length).toBe(1);
69 |
70 | scope.$apply();
71 | expect(React.renderComponent).toHaveBeenCalledWith(component(undefined), elem[0]);
72 | });
73 |
74 | it('should create a $$watcher and $$listener', function () {
75 | attrs.props = 'foo';
76 | scope.foo = 'bar';
77 | $react.renderComponent(component)(scope, elem, attrs);
78 |
79 | expect(scope.$$watchers.length).toBe(1);
80 | expect(scope.$$listeners.$destroy.length).toBe(1);
81 | });
82 |
83 | it('should render the component when the attributes change', function () {
84 | spyOn(React, 'renderComponent');
85 | attrs.props = 'foo';
86 | scope.foo = 'bar';
87 | scope.bar = 'baz';
88 | $react.renderComponent(component)(scope, elem, attrs);
89 | scope.$apply();
90 |
91 | expect(React.renderComponent).toHaveBeenCalledWith(component('bar'), elem[0]);
92 |
93 | attrs.props = 'bar';
94 | scope.$apply();
95 |
96 | expect(React.renderComponent.calls.length).toBe(2);
97 | expect(React.renderComponent).toHaveBeenCalledWith(component('baz'), elem[0]);
98 | });
99 | });
100 | });
101 | });
--------------------------------------------------------------------------------