├── .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 [![Build Status](https://travis-ci.org/wesleycho/angular-react.svg?branch=master)](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 | }); --------------------------------------------------------------------------------