├── .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 | 
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 |
--------------------------------------------------------------------------------