├── .babelrc ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── demo ├── public │ ├── angular-include-react-logo.jpg │ └── index.html ├── server.js ├── src │ ├── angular-component.js │ ├── angular-container-component.js │ ├── index.js │ ├── react-component.js │ ├── react-component.scss │ ├── react-container-component.js │ └── store.js └── webpack.config.js ├── index.js ├── package.json └── src ├── include-react-angular-component.js ├── include-react-controller.js ├── include-react-controller.spec.js ├── include-react-react-component.js ├── include-react-react-component.spec.js ├── include-react-service.js ├── include-react-service.spec.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react" 5 | ], 6 | "plugins": [ 7 | "transform-object-rest-spread", 8 | "syntax-async-functions", 9 | "transform-async-to-generator" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:react/recommended" 5 | ], 6 | "parser": "babel-eslint", 7 | "parserOptions": { 8 | "ecmaFeatures": { 9 | "jsx": true 10 | }, 11 | }, 12 | "plugins": [ "react" ], 13 | "env": { 14 | "mocha": true, 15 | "node": true, 16 | "es6": true, 17 | }, 18 | "rules": { 19 | "arrow-body-style": [ "error", "as-needed" ], 20 | "brace-style": [ "error", "1tbs", { allowSingleLine: true } ], 21 | "comma-dangle": [ "error", "always-multiline" ], 22 | "comma-spacing": [ "error", { "before": false, "after": true } ], 23 | "curly": [ "error", "all" ], 24 | "eol-last": "error", 25 | "eqeqeq": [ "error", "always", { "null": "ignore" } ], 26 | "guard-for-in": "error", 27 | "indent": [ "error", "tab", { SwitchCase: 1 } ], 28 | "key-spacing": [ "error", { beforeColon: false, afterColon: true } ], 29 | "keyword-spacing": [ "error", { after: true } ], 30 | "newline-before-return": "error", 31 | "no-bitwise": "error", 32 | "no-caller": "error", 33 | "no-confusing-arrow": [ "error", { allowParens: true } ], 34 | "no-console": "warn", 35 | "no-duplicate-imports": "error", 36 | "no-empty": "error", 37 | "no-implicit-coercion": [ "error", { allow: [ "!!" ] } ], 38 | "no-mixed-spaces-and-tabs": [ "error", "smart-tabs" ], 39 | "no-multiple-empty-lines": [ "error", { max: 1 } ], 40 | "no-shadow": "off", 41 | "no-spaced-func": "error", 42 | "no-trailing-spaces": "error", 43 | "no-undef": "error", 44 | "no-underscore-dangle": "off", 45 | "no-unexpected-multiline": "error", 46 | "no-unused-vars": "error", 47 | "no-use-before-define": "off", 48 | "no-useless-computed-key": "error", 49 | "no-useless-constructor": "error", 50 | "no-useless-rename": "error", 51 | "no-with": "error", 52 | "object-curly-spacing": [ "error", "always" ], 53 | "object-shorthand": "error", 54 | "one-var": [ "error", "never" ], 55 | "padded-blocks": [ "error", "never" ], 56 | "prefer-const": "error", 57 | "prefer-rest-params": "error", 58 | "prefer-spread": "error", 59 | "prefer-template": "error", 60 | "quotes": [ "error", "single" ], 61 | "rest-spread-spacing": "error", 62 | "semi": [ "error", "always" ], 63 | "space-before-blocks": [ "error", "always" ], 64 | "space-before-function-paren": [ "error", "never" ], 65 | "space-in-parens": [ "error", "never" ], 66 | "space-infix-ops": "error", 67 | "space-unary-ops": [ "error", { words: false, nonwords: false } ], 68 | "strict": "off", 69 | "symbol-description": "error", 70 | "template-curly-spacing": "error", 71 | "newline-per-chained-call": "off", 72 | "no-var": "error" 73 | }, 74 | "globals": { 75 | "browser": true, 76 | "expect": true 77 | }, 78 | "settings": { 79 | "react": { 80 | "version": "detect" 81 | } 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editor 2 | .idea/ 3 | *.iml 4 | 5 | # Node 6 | node_modules/ 7 | 8 | # Output 9 | dist/ 10 | lib/ 11 | coverage/ 12 | 13 | # Test 14 | test/visual/*.new.png 15 | test/visual/*.diff.png 16 | 17 | # Log files 18 | *.log 19 | 20 | # Mac 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Medallia, Inc. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Angular Include React](https://cloud.githubusercontent.com/assets/402730/25906472/1c77fd16-357b-11e7-9524-5d42ea2a76f6.jpg) 2 | 3 | ============= 4 | 5 | React Include allows you to embed React [container components](https://medium.com/@learnreact/container-components-c0e67432e005) inside an AngularJS application, synchronizing them through a Redux store. You can think of it as a lightweight alternative to [ng-react](https://www.npmjs.com/package/ngreact), that avoids all the integration quirks by connecting the components through redux (that's framework agnostic), instead of transforming AngularJS bindings to React props. 6 | 7 | It is built on top of the most widely adopted redux-frameworks for both libraries, [ng-redux](https://www.npmjs.com/package/ng-redux) and [react-redux](https://www.npmjs.com/package/react-redux), and follows the best practices that are also widely adopted in the javascript community regarding Redux integration with each framework. 8 | 9 | 10 | ## Installation 11 | 12 | Install via yarn: 13 | 14 | ```bash 15 | yarn add @m/angular-include-react 16 | ``` 17 | 18 | or via npm: 19 | 20 | ```bash 21 | npm i --save @m/angular-include-react 22 | ``` 23 | 24 | ## Usage 25 | 26 | Add angularIncludeReact as a dependency to your angular application 27 | 28 | ```javascript 29 | import angular from 'angular'; 30 | import angularIncludeReact from '@m/angular-include-react'; 31 | 32 | const ngModule = angular.module('app', [ 33 | // Add angular include react angular module as a dependency 34 | angularIncludeReact 35 | ]); 36 | ``` 37 | 38 | Register some react container components via the _includeReact_ angular service, with an arbitrary name 39 | 40 | ```javascript 41 | import { connect } from 'react-redux'; 42 | 43 | const ReactContainerComponent = connect(/* ... */); 44 | 45 | ngModule.run(function(includeReact) { 46 | includeReact.registerComponent('my-react-component', ReactContainerComponent); 47 | }); 48 | ``` 49 | 50 | And finally, include the components into your angular template with the include-react directive 51 | 52 | ```html 53 | 54 | ``` 55 | 56 | ## Developing 57 | 58 | To start the development server, just run: 59 | 60 | ```bash 61 | npm install 62 | npm start 63 | ``` 64 | 65 | ### Build Library 66 | 67 | ```bash 68 | npm run build 69 | ``` 70 | 71 | ### Build Demo 72 | 73 | ```bash 74 | npm run build:demo 75 | ``` 76 | 77 | ### Start development server 78 | 79 | ```bash 80 | npm run serve 81 | ``` 82 | 83 | ### Tests 84 | 85 | ```bash 86 | npm test 87 | ``` 88 | 89 | ### Lint 90 | 91 | ```bash 92 | npm run lint 93 | ``` 94 | 95 | or 96 | 97 | ```bash 98 | npm run lint:fix 99 | ``` 100 | 101 | ## License & Copyright 102 | This software is copyrighted 2017 by Medallia, Inc. and released under the 103 | [MIT License][1]. 104 | 105 | [1]: ./LICENSE 106 | 107 | 108 | -------------------------------------------------------------------------------- /demo/public/angular-include-react-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medallia/angular-include-react/47ea5dfdcd97f2f06dd2f8cf15bef685c6723411/demo/public/angular-include-react-logo.jpg -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React Include Demo 7 | 8 | 9 |

Angular World:

10 | 11 | 12 |

React World:

13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /demo/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var http = require('http'); 4 | var path = require('path'); 5 | var express = require('express'); 6 | 7 | var port = process.env.PORT || 3030; 8 | var app = module.exports = express(); 9 | 10 | // serve public 11 | app.use(express.static(path.join(__dirname, 'public'))); 12 | 13 | // serve demo dist 14 | app.use('/dist', express.static(path.join(__dirname, './dist'))); 15 | 16 | 17 | var server = http.createServer(app); 18 | 19 | server.listen(port, function(err) { 20 | if (err) { 21 | throw err; 22 | } 23 | console.log('Server listening on port', port); 24 | }); 25 | -------------------------------------------------------------------------------- /demo/src/angular-component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Angular component, capable of triggering a 'flip' action, and showing the current flip state 3 | * 4 | * This is a standard presentational component in an application with ng-redux. 5 | */ 6 | export default { 7 | bindings: { 8 | flipped: '<', 9 | flip: '&', 10 | }, 11 | template: ` 12 |
13 | {{ $ctrl.flipped ? 'Flipped' : 'Normal' }} 14 | 15 |
16 | `, 17 | }; 18 | -------------------------------------------------------------------------------- /demo/src/angular-container-component.js: -------------------------------------------------------------------------------- 1 | import { flip } from './store'; 2 | 3 | /** 4 | * Angular container component, that connects to redux to get a 'flip' action bound to the dispatch function, and takes the the current flip state. 5 | * 6 | * This is a standard container component in an application with ng-redux. 7 | */ 8 | export default { 9 | controller: class AngularContainerComponentController { 10 | constructor($ngRedux) { 11 | this._$ngRedux = $ngRedux; 12 | } 13 | 14 | $onInit() { 15 | this._disconnect = this._$ngRedux.connect( 16 | (state) => ({ flipped: state.flipped }), 17 | { flip } 18 | )(this); 19 | } 20 | 21 | $onDestroy() { 22 | this._disconnect(); 23 | } 24 | }, 25 | template: '', 26 | }; 27 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import ngRedux from 'ng-redux'; 3 | import angularIncludeReact from '../../.'; 4 | import angularComponent from './angular-component'; 5 | import angularContainerComponent from './angular-container-component'; 6 | 7 | import { reducer } from './store'; 8 | import ReactContainerComponent from './react-container-component'; 9 | 10 | /** This demo file shows the integration of angular-include-react library into the angular application */ 11 | 12 | const ngModule = angular.module('angular-include-react-demo', [ 13 | // Add react include angular module as a dependency 14 | angularIncludeReact, 15 | ngRedux, 16 | ]); 17 | 18 | ngModule.component('angularContainerComponent', angularContainerComponent); 19 | ngModule.component('angularComponent', angularComponent); 20 | 21 | ngModule.config(function($ngReduxProvider) { 22 | $ngReduxProvider.createStoreWith( 23 | reducer 24 | ); 25 | }); 26 | 27 | // Inject includeReact service into a run function in your module 28 | ngModule.run(function(includeReact) { 29 | // Register a react container component with an arbitrary name into the include-react registry. 30 | // The component won't receive any props, so its must be able to fetch its entire state from the redux store. 31 | includeReact.registerComponent('react-component', ReactContainerComponent); 32 | }); 33 | -------------------------------------------------------------------------------- /demo/src/react-component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './react-component.scss'; 4 | 5 | /** 6 | * React component, capable of triggering a 'flip' action, and showing the current flip state. 7 | * 8 | * This is a standard presentational component in an application with react-redux. 9 | */ 10 | class ReactComponent extends React.Component { 11 | render() { 12 | const { flipped, flip } = this.props; 13 | const className = `react-component ${flipped ? 'react-component--flipped' : ''}`; 14 | 15 | return ( 16 | 17 | ); 18 | } 19 | } 20 | 21 | ReactComponent.propTypes = { 22 | flipped: PropTypes.bool, 23 | flip: PropTypes.func, 24 | }; 25 | 26 | export default ReactComponent; 27 | -------------------------------------------------------------------------------- /demo/src/react-component.scss: -------------------------------------------------------------------------------- 1 | .react-component { 2 | width: 100%; 3 | transition: 0.6s; 4 | transform-style: preserve-3d; 5 | } 6 | 7 | .react-component--flipped { 8 | transform: rotateY(180deg); 9 | } 10 | -------------------------------------------------------------------------------- /demo/src/react-container-component.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { flip } from './store'; 3 | import ReactComponent from './react-component'; 4 | 5 | /** 6 | * React container component, that connects to redux to get a 'flip' action bound to the dispatch function, and takes the the current flip state. 7 | * 8 | * This is a standard container component in an application with react-redux. 9 | */ 10 | export default connect( 11 | (state) => ({ flipped: state.flipped }), 12 | (dispatch) => ({ flip: () => dispatch(flip()) }) 13 | )(ReactComponent); 14 | -------------------------------------------------------------------------------- /demo/src/store.js: -------------------------------------------------------------------------------- 1 | const FLIP = 'FLIP'; 2 | const INITIAL_STATE = { 3 | flipped: false, 4 | }; 5 | 6 | function reducer(state = INITIAL_STATE, action) { 7 | if (action.type === FLIP) { 8 | return { 9 | flipped: !state.flipped, 10 | }; 11 | } 12 | 13 | return state; 14 | } 15 | 16 | function flip() { 17 | return { 18 | type: FLIP, 19 | }; 20 | } 21 | 22 | export { 23 | reducer, 24 | flip, 25 | }; 26 | -------------------------------------------------------------------------------- /demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | const webpack = require('webpack'); 3 | const { getIfUtils, removeEmpty } = require('webpack-config-utils'); 4 | const { ifDevelopment, ifProduction } = getIfUtils(process.env.NODE_ENV); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | 7 | module.exports = { 8 | context: resolve(__dirname, 'src'), 9 | entry: ['./index.js'], 10 | output: { 11 | path: resolve(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: './', 14 | pathinfo: ifDevelopment(), // Include info about the modules path, e.g. require(/* ./test */23), 15 | library: ['feature'], 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.js$/, 21 | exclude: /node_modules/, 22 | loader: 'babel-loader', 23 | }, 24 | { 25 | enforce: 'post', 26 | test: /\.js$/, 27 | exclude: /node_modules/, 28 | loader: 'ng-annotate-loader', 29 | }, 30 | { 31 | test: /\.scss$/, 32 | use: ExtractTextPlugin.extract({ 33 | use: [ 34 | 'css-loader', 35 | { 36 | loader: 'sass-loader', 37 | options: { 38 | includePaths: [resolve(__dirname, 'node_modules')], // Include SCSS files from node_modules 39 | }, 40 | }, 41 | ], 42 | }), 43 | }, 44 | { 45 | test: /\.css$/, 46 | use: ExtractTextPlugin.extract({ 47 | use: ['css-loader'], 48 | }), 49 | }, 50 | { 51 | test: /\.html$/, 52 | loader: 'html-loader', 53 | }, 54 | 55 | // Inline SVGs encoded in UTF-8 56 | { 57 | test: /\.svg$/, 58 | loader: 'svg-url-loader', 59 | }, 60 | 61 | // Inline PNGs in Base64 if it is smaller than 10KB; otherwise, emmit files using file-loader. 62 | { 63 | test: /\.png$/, 64 | loader: 'url-loader', 65 | options: { mimetype: 'image/png', limit: 10000, name: '[name]-[hash:6].[ext]' }, 66 | }, 67 | ], 68 | }, 69 | plugins: removeEmpty([ 70 | // Define global variables for webpack and its plugins 71 | new webpack.DefinePlugin({ 72 | 'process.env': { 73 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 74 | }, 75 | }), 76 | 77 | // Dump bundled CSS into one file 78 | new ExtractTextPlugin({ filename: 'bundle.css', allChunks: true }), 79 | 80 | // JS minification 81 | ifProduction(new webpack.optimize.UglifyJsPlugin({ 82 | comments: false, 83 | mangle: true, 84 | sourceMap: true, 85 | compress: { 86 | unused: true, 87 | warnings: false, 88 | comparisons: true, 89 | conditionals: true, 90 | // For `v8LazyParse()` 91 | negate_iife: false, 92 | dead_code: true, 93 | if_return: true, 94 | join_vars: true, 95 | evaluate: true, 96 | screw_ie8: true, 97 | }, 98 | })), 99 | ]), 100 | devtool: 'source-map', 101 | stats: { children: false }, // Hide log spams from child plugins 102 | }; 103 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 1. NB!! Do not publish v1.0.0 until ruleset has stabilised. Each rule changed constitutes a breaking change - continue 3 | * to publish minor versions on the 0.x release line 4 | * "error". Maintain alphabetical ordering of keys in the rules object - allows easier lookup / duplication spotting 5 | * 2. Please keep rules sorted alphabetically. 6 | */ 7 | 8 | module.exports = { 9 | "extends": "eslint:recommended", 10 | "rules": { 11 | "arrow-body-style": ["error", "as-needed"], 12 | "brace-style": ["error", "1tbs", {allowSingleLine: true}], 13 | "comma-dangle": ["error", "always-multiline"], 14 | "comma-spacing": ["error", {"before": false, "after": true}], 15 | "curly": ["error", "all"], 16 | "eol-last": "error", 17 | "eqeqeq": ["error", "always", {"null": "ignore"}], 18 | "guard-for-in": "error", 19 | "indent": ["error", "tab", {SwitchCase: 1}], 20 | "key-spacing": ["error", {beforeColon: false, afterColon: true}], 21 | "keyword-spacing": ["error", {after: true}], 22 | "newline-before-return": "error", 23 | "no-bitwise": "error", 24 | "no-caller": "error", 25 | "no-confusing-arrow": ["error", {allowParens: true}], 26 | "no-console": "warn", 27 | "no-duplicate-imports": "error", 28 | "no-empty": "error", 29 | "no-implicit-coercion": ["error", {allow: ["!!"]}], 30 | "no-mixed-spaces-and-tabs": ["error", "smart-tabs"], 31 | "no-multiple-empty-lines": ["error", {max: 1}], 32 | "no-shadow": "off", 33 | "no-spaced-func": "error", 34 | "no-trailing-spaces": "error", 35 | "no-undef": "error", 36 | "no-underscore-dangle": "off", 37 | "no-unexpected-multiline": "error", 38 | "no-unused-vars": "error", 39 | "no-use-before-define": "off", 40 | "no-useless-computed-key": "error", 41 | "no-useless-constructor": "error", 42 | "no-useless-rename": "error", 43 | "no-with": "error", 44 | "object-curly-spacing": ["error", "always"], 45 | "object-shorthand": "error", 46 | "one-var": ["error", "never"], 47 | "padded-blocks": ["error", "never"], 48 | "prefer-const": "error", 49 | "prefer-rest-params": "error", 50 | "prefer-spread": "error", 51 | "prefer-template": "error", 52 | "quotes": ["error", "single"], 53 | "rest-spread-spacing": "error", 54 | "semi": ["error", "always"], 55 | "space-before-blocks": ["error", "always"], 56 | "space-before-function-paren": ["error", "never"], 57 | "space-in-parens": ["error", "never"], 58 | "space-infix-ops": "error", 59 | "space-unary-ops": ["error", {words: false, nonwords: false}], 60 | "strict": "off", 61 | "symbol-description": "error", 62 | "template-curly-spacing": "error", 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-include-react", 3 | "version": "0.1.0", 4 | "description": "Include React container components into a AngularJS+Redux SPA", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "babel src --out-dir dist", 8 | "lint": "eslint src demo/src", 9 | "lint:fix": "npm run -s lint -- --fix", 10 | "start": "npm run -s build && npm run -s build:demo && npm run -s serve", 11 | "serve": "node demo/server.js", 12 | "build:demo": "(cd demo; rm -rf dist && mkdir dist && NODE_ENV=development webpack --progress)", 13 | "test": "mocha --require babel-register 'src/**/*.spec.js'" 14 | }, 15 | "files": [ 16 | "dist/" 17 | ], 18 | "author": "Medallia", 19 | "license": "MIT", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/medallia/angular-include-react" 23 | }, 24 | "dependencies": { 25 | "prop-types": "^15.5.8" 26 | }, 27 | "devDependencies": { 28 | "angular": "^1.5.0", 29 | "babel-cli": "^6.22.2", 30 | "babel-core": "^6.22.1", 31 | "babel-eslint": "^10.0.2", 32 | "babel-loader": "^6.3.2", 33 | "babel-plugin-transform-async-to-generator": "^6.22.0", 34 | "babel-plugin-transform-object-rest-spread": "^6.22.0", 35 | "babel-preset-es2015": "^6.22.0", 36 | "babel-preset-react": "^6.23.0", 37 | "babel-register": "^6.23.0", 38 | "chai": "^4.0.0-canary.2", 39 | "chai-enzyme": "^0.6.1", 40 | "css-loader": "^0.26.1", 41 | "enzyme": "^2.7.1", 42 | "eslint": "^6.0.1", 43 | "eslint-plugin-react": "^7.14.2", 44 | "express": "^4.14.1", 45 | "extract-text-webpack-plugin": "^2.0.0-rc.3", 46 | "mocha": "^3.2.0", 47 | "ng-annotate-loader": "^0.2.0", 48 | "ng-redux": "^3.4.0-beta.1", 49 | "node-sass": "^4.5.0", 50 | "react": "^16.0.0", 51 | "react-dom": "^16.0.0", 52 | "react-redux": "^5.0.4", 53 | "react-test-renderer": "^16.0.0", 54 | "redux": "^3.6.0", 55 | "sass-loader": "^6.0.0", 56 | "sinon": "^2.2.0", 57 | "sinon-chai": "^2.8.0", 58 | "webpack": "^2.2.1", 59 | "webpack-config-utils": "^2.3.0" 60 | }, 61 | "peerDependencies": { 62 | "angular": "^1.5.0", 63 | "ng-redux": "^3.4.0-beta.1", 64 | "react": "^15.0.0 || ^16.0.0-0", 65 | "react-dom": "^15.0.0 || ^16.0.0-0", 66 | "react-redux": "^5.0.4" 67 | }, 68 | "keywords": [ 69 | "angular", 70 | "include", 71 | "react", 72 | "redux" 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /src/include-react-angular-component.js: -------------------------------------------------------------------------------- 1 | import IncludeReactController from './include-react-controller'; 2 | 3 | /** 4 | * This angular component renders a React component inside. The component to be rendered should have been pre-registered 5 | * by name into the IncludeReactService. The component rendered will have access to the redux store, that will be 6 | * obtained through $ngRedux service. 7 | */ 8 | export default { 9 | bindings: { 10 | component: '@', 11 | }, 12 | controller: IncludeReactController, 13 | }; 14 | -------------------------------------------------------------------------------- /src/include-react-controller.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import React from 'react'; 3 | import IncludeReactReactComponent from './include-react-react-component'; 4 | 5 | /** 6 | * This controller is responsible for rendering the React component into an angular component, and doing the 7 | * corresponding cleanup when the angular component is destroyed. 8 | */ 9 | export default class IncludeReactController { 10 | constructor($element, includeReact, $ngRedux) { 11 | this.$element = $element; 12 | this.includeReactService = includeReact; 13 | this.$ngRedux = $ngRedux; 14 | } 15 | 16 | $postLink() { 17 | // $ngRedux API is fully compatible with redux store API 18 | this.mountComponent(this.includeReactService.getComponent(this.component), this.$element, this.$ngRedux); 19 | } 20 | 21 | $onDestroy() { 22 | this.unmountComponent(this.$element); 23 | } 24 | 25 | mountComponent(component, element, store) { 26 | const props = { store, component }; 27 | ReactDOM.render(React.createElement(IncludeReactReactComponent, props), element[0]); 28 | } 29 | 30 | unmountComponent(element) { 31 | ReactDOM.unmountComponentAtNode(element[0]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/include-react-controller.spec.js: -------------------------------------------------------------------------------- 1 | import IncludeReactController from './include-react-controller'; 2 | import chai, { expect } from 'chai'; 3 | import sinon from 'sinon'; 4 | import sinonChai from 'sinon-chai'; 5 | import ReactDOM from 'react-dom'; 6 | import IncludeReactReactComponent from './include-react-react-component'; 7 | 8 | chai.use(sinonChai); 9 | 10 | describe('IncludeReact controller', function() { 11 | let sandbox; 12 | let includeReactController; 13 | let mockElement; 14 | let getComponentMock; 15 | let mockRender; 16 | let mockUnmount; 17 | let mockComponent; 18 | let mockNgRedux; 19 | 20 | beforeEach(function() { 21 | sandbox = sinon.sandbox.create(); 22 | mockElement = [{}]; 23 | mockComponent = function MyComponent() {}; 24 | mockNgRedux = {}; 25 | mockRender = sandbox.stub(ReactDOM, 'render'); 26 | mockUnmount = sandbox.stub(ReactDOM, 'unmountComponentAtNode'); 27 | getComponentMock = sandbox.stub(); 28 | includeReactController = new IncludeReactController( 29 | mockElement, 30 | { 31 | getComponent: getComponentMock, 32 | }, 33 | mockNgRedux 34 | ); 35 | }); 36 | 37 | it('should mount component on $postLink', function() { 38 | getComponentMock.withArgs('myComponent').returns(mockComponent); 39 | includeReactController.component = 'myComponent'; 40 | includeReactController.$postLink(); 41 | expect(mockRender).to.have.been.calledOnce; 42 | const { type, props } = mockRender.firstCall.args[0]; 43 | const domContainerNode = mockRender.firstCall.args[1]; 44 | expect(type).to.equal(IncludeReactReactComponent); 45 | expect(props).to.have.property('store').that.equals(mockNgRedux); 46 | expect(props).to.have.property('component').that.equals(mockComponent); 47 | expect(domContainerNode).to.equal(mockElement[0]); 48 | }); 49 | 50 | it('should unmount component on destroy', function() { 51 | includeReactController.$onDestroy(); 52 | expect(mockUnmount).to.have.been.calledOnce; 53 | expect(mockUnmount).to.have.been.calledWith(mockElement[0]); 54 | }); 55 | 56 | afterEach(function() { 57 | sandbox.restore(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/include-react-react-component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Provider } from 'react-redux'; 4 | 5 | /** This HOC wraps the given component into a react-redux's Provide component, so it can access the given store. */ 6 | function IncludeReactReactComponent(props) { 7 | const Component = props.component; 8 | 9 | return (); 10 | } 11 | 12 | IncludeReactReactComponent.propTypes = { 13 | component: PropTypes.func, 14 | store: PropTypes.object, 15 | }; 16 | 17 | export default IncludeReactReactComponent; 18 | -------------------------------------------------------------------------------- /src/include-react-react-component.spec.js: -------------------------------------------------------------------------------- 1 | import IncludeReactReactComponent from './include-react-react-component'; 2 | import chai, { expect } from 'chai'; 3 | import chaiEnzyme from 'chai-enzyme'; 4 | import React from 'react'; 5 | import { shallow } from 'enzyme'; 6 | import { Provider } from 'react-redux'; 7 | 8 | chai.use(chaiEnzyme()); 9 | 10 | describe('IncludeReact react component', function() { 11 | let MockComponent; 12 | let mockStore; 13 | 14 | beforeEach(function() { 15 | MockComponent = function MyComponent() { }; 16 | mockStore = { }; 17 | }); 18 | 19 | it('should wrapp component with store provider', function() { 20 | // this test almost duplicates the component implementation, but it clearly shows its API 21 | const wrapper = shallow( 22 | 23 | ); 24 | 25 | expect(wrapper).to.contain(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/include-react-service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This service is used to pre-register React components under certain name, so they can be rendered with the 3 | * include-react directive. 4 | */ 5 | export default function includeReactServiceFactory($cacheFactory) { 6 | const cache = $cacheFactory('includeReactComponents'); 7 | 8 | return { 9 | registerComponent: (name, component) => cache.put(name, component), 10 | getComponent: (name) => cache.get(name), 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/include-react-service.spec.js: -------------------------------------------------------------------------------- 1 | import includeReactServiceFactory from './include-react-service'; 2 | import { expect } from 'chai'; 3 | 4 | describe('IncludeReact service', function() { 5 | let ReactComponent; 6 | let includeReact; 7 | 8 | beforeEach(function() { 9 | ReactComponent = function MyComponent() {}; 10 | const cacheFactoryMock = function() { 11 | const cache = {}; 12 | 13 | return { 14 | get: (key) => cache[key], 15 | put: (key, value) => cache[key] = value, 16 | }; 17 | }; 18 | 19 | includeReact = includeReactServiceFactory(cacheFactoryMock); 20 | }); 21 | 22 | it('should register component', function() { 23 | includeReact.registerComponent('myComponent', ReactComponent); 24 | expect(includeReact.getComponent('myComponent')).to.equal(ReactComponent); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import includeReactServiceFactory from './include-react-service'; 3 | import includeReactAngularComponent from './include-react-angular-component'; 4 | 5 | const ngModule = angular.module('include-react', []); 6 | 7 | ngModule.factory('includeReact', includeReactServiceFactory); 8 | 9 | ngModule.component('includeReact', includeReactAngularComponent); 10 | 11 | export default ngModule.name; 12 | --------------------------------------------------------------------------------