├── .editorconfig ├── .ember-cli ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .lintstagedrc ├── .npmignore ├── .prettierrc ├── .template-lintrc.js ├── .travis.yml ├── .watchmanconfig ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── addon ├── -private │ ├── component-is-functional.js │ ├── grant-owner-access.js │ └── yield-wrapper.js └── index.js ├── blueprints └── react-component │ ├── files │ └── app │ │ └── components │ │ └── __name__.js │ └── index.js ├── commitlint.config.js ├── config ├── addon-docs.js ├── deploy.js ├── ember-try.js └── environment.js ├── ember-cli-build.js ├── index.js ├── lib ├── add-jsx-extension-support.js └── configure-jsx-transform.js ├── package.json ├── testem.js ├── tests ├── dummy │ ├── app │ │ ├── app.js │ │ ├── components │ │ │ ├── arrow-function-component.js │ │ │ ├── basic-component.js │ │ │ ├── invoke-action.js │ │ │ ├── set-state.js │ │ │ ├── traditional-functional-component.js │ │ │ ├── using-service.js │ │ │ ├── with-properties.js │ │ │ └── yield-to-children.js │ │ ├── config │ │ │ └── environment.d.ts │ │ ├── docs │ │ │ ├── demo │ │ │ │ └── basic-component │ │ │ │ │ └── template.hbs │ │ │ ├── features │ │ │ │ ├── children │ │ │ │ │ ├── render-children │ │ │ │ │ │ └── template.hbs │ │ │ │ │ └── template.md │ │ │ │ ├── functional │ │ │ │ │ ├── demo-functional-component │ │ │ │ │ │ └── template.hbs │ │ │ │ │ └── template.md │ │ │ │ ├── generator │ │ │ │ │ └── template.md │ │ │ │ └── services │ │ │ │ │ ├── demo-service-injection │ │ │ │ │ └── template.hbs │ │ │ │ │ └── template.md │ │ │ ├── index │ │ │ │ └── template.md │ │ │ ├── installation │ │ │ │ └── template.md │ │ │ ├── options │ │ │ │ └── template.md │ │ │ └── template.hbs │ │ ├── index.html │ │ ├── instance-initializers │ │ │ └── setup-metrics-context.js │ │ ├── resolver.js │ │ ├── router.js │ │ ├── routes │ │ │ ├── .gitkeep │ │ │ └── index.js │ │ ├── services │ │ │ └── session.js │ │ ├── styles │ │ │ └── app.css │ │ └── templates │ │ │ └── application.hbs │ ├── config │ │ ├── environment.js │ │ ├── optional-features.json │ │ └── targets.js │ └── public │ │ └── robots.txt ├── helpers │ └── .gitkeep ├── index.html ├── integration │ ├── .gitkeep │ └── utils │ │ └── with-ember-support-test.js ├── test-helper.js └── unit │ └── .gitkeep ├── tsconfig.json ├── vendor └── .gitkeep └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | 17 | # ember-try 18 | /.node_modules.ember-try/ 19 | /bower.json.ember-try 20 | /package.json.ember-try 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | ecmaVersion: 2017, 6 | sourceType: 'module', 7 | ecmaFeatures: { 8 | jsx: true, 9 | legacyDecorators: true 10 | } 11 | }, 12 | plugins: ['babel', 'ember', 'react', 'prettier'], 13 | extends: [ 14 | 'eslint:recommended', 15 | 'plugin:ember/recommended', 16 | 'prettier', 17 | 'plugin:prettier/recommended' 18 | ], 19 | env: { 20 | browser: true 21 | }, 22 | rules: { 23 | 'react/jsx-uses-react': 'error', 24 | 'react/jsx-uses-vars': 'error', 25 | 26 | 'ember/no-attrs-in-components': 'off' 27 | }, 28 | overrides: [ 29 | // TypeScript files 30 | { 31 | parser: 'typescript-eslint-parser', 32 | files: ['addon/**/*.ts', 'tests/**/*.ts'], 33 | rules: { 34 | 'no-undef': 'off', 35 | 'no-unused-vars': 'off' 36 | } 37 | }, 38 | // node files 39 | { 40 | files: [ 41 | '.eslintrc.js', 42 | '.template-lintrc.js', 43 | 'commitlint.config.js', 44 | 'index.js', 45 | 'testem.js', 46 | 'ember-cli-build.js', 47 | 'config/**/*.js', 48 | 'lib/**/*.js', 49 | 'tests/dummy/config/**/*.js' 50 | ], 51 | excludedFiles: ['app/**', 'addon/**', 'tests/dummy/app/**'], 52 | parserOptions: { 53 | sourceType: 'script', 54 | ecmaVersion: 2015 55 | }, 56 | env: { 57 | browser: false, 58 | node: true 59 | }, 60 | plugins: ['node'], 61 | rules: Object.assign( 62 | {}, 63 | require('eslint-plugin-node').configs.recommended.rules, 64 | { 65 | // add your custom rules and overrides for node files here 66 | } 67 | ) 68 | } 69 | ] 70 | }; 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.sass-cache 14 | /connect.lock 15 | /coverage/ 16 | /libpeerconnection.log 17 | /npm-debug.log* 18 | /testem.log 19 | /yarn-error.log 20 | 21 | # ember-try 22 | /.node_modules.ember-try/ 23 | /bower.json.ember-try 24 | /package.json.ember-try 25 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts}": ["eslint --fix", "git add"] 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /config/ember-try.js 3 | /dist 4 | /tests 5 | /tmp 6 | **/.gitkeep 7 | .bowerrc 8 | .editorconfig 9 | .ember-cli 10 | .eslintrc.js 11 | .gitignore 12 | .watchmanconfig 13 | .travis.yml 14 | bower.json 15 | ember-cli-build.js 16 | testem.js 17 | yarn.lock 18 | 19 | # ember-try 20 | .node_modules.ember-try/ 21 | bower.json.ember-try 22 | package.json.ember-try 23 | /config/addon-docs.js -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended' 5 | }; 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | # we recommend testing addons with the same minimum supported node version as Ember CLI 5 | # so that your addon works for all apps 6 | - '8' 7 | 8 | sudo: false 9 | dist: trusty 10 | 11 | addons: 12 | chrome: stable 13 | 14 | cache: 15 | directories: 16 | - $HOME/.npm 17 | 18 | env: 19 | global: 20 | # See https://git.io/vdao3 for details. 21 | - JOBS=1 22 | 23 | branches: 24 | only: 25 | - master 26 | # npm version tags 27 | - /^v\d+\.\d+\.\d+/ 28 | 29 | jobs: 30 | fail_fast: true 31 | allow_failures: 32 | - env: EMBER_TRY_SCENARIO=ember-canary 33 | 34 | include: 35 | # runs linting and tests with current locked deps 36 | 37 | - stage: 'Tests' 38 | name: 'Tests' 39 | script: 40 | - npm run lint:js 41 | - npm test 42 | 43 | # we recommend new addons test the current and previous LTS 44 | # as well as latest stable release (bonus points to beta/canary) 45 | - stage: 'Additional Tests' 46 | env: EMBER_TRY_SCENARIO=ember-lts-2.18 47 | - env: EMBER_TRY_SCENARIO=ember-lts-3.4 48 | - env: EMBER_TRY_SCENARIO=ember-release 49 | - env: EMBER_TRY_SCENARIO=ember-beta 50 | - env: EMBER_TRY_SCENARIO=ember-canary 51 | - env: EMBER_TRY_SCENARIO=stage-1-decorators 52 | 53 | script: 54 | - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO 55 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | ## [3.0.2](https://github.com/alexlafroscia/ember-react-components/compare/v3.0.1...v3.0.2) (2019-11-12) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * add jsx extension support ([3c25a94](https://github.com/alexlafroscia/ember-react-components/commit/3c25a94)) 12 | 13 | 14 | 15 | 16 | ## [3.0.1](https://github.com/alexlafroscia/ember-react-components/compare/v3.0.0...v3.0.1) (2019-03-06) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * better detection of existing JSX plugin ([cc55646](https://github.com/alexlafroscia/ember-react-components/commit/cc55646)) 22 | 23 | 24 | 25 | 26 | # [3.0.0](https://github.com/alexlafroscia/ember-react-components/compare/v2.0.4...v3.0.0) (2019-02-02) 27 | 28 | 29 | ### Code Refactoring 30 | 31 | * remove Babel 6 support ([b076935](https://github.com/alexlafroscia/ember-react-components/commit/b076935)) 32 | 33 | 34 | ### Features 35 | 36 | * support Stage 2 decorators ([529e6f4](https://github.com/alexlafroscia/ember-react-components/commit/529e6f4)), closes [#29](https://github.com/alexlafroscia/ember-react-components/issues/29) 37 | 38 | 39 | ### BREAKING CHANGES 40 | 41 | * Remove automatic Babel 6 support. For continued support, the React JSX Babel transform can be supplied manually by the application configuration. 42 | 43 | 44 | 45 | 46 | ## [2.0.4](https://github.com/alexlafroscia/ember-react-components/compare/v2.0.3...v2.0.4) (2019-02-01) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * remove TypeScript compilation hooks ([0eccca6](https://github.com/alexlafroscia/ember-react-components/commit/0eccca6)) 52 | * support arrow functions for components ([b9a809b](https://github.com/alexlafroscia/ember-react-components/commit/b9a809b)), closes [#27](https://github.com/alexlafroscia/ember-react-components/issues/27) 53 | 54 | 55 | 56 | 57 | ## [2.0.3](https://github.com/alexlafroscia/ember-react-components/compare/v2.0.2...v2.0.3) (2019-02-01) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * install React, React DOM through `ember-auto-import` ([4f99ad8](https://github.com/alexlafroscia/ember-react-components/commit/4f99ad8)), closes [#23](https://github.com/alexlafroscia/ember-react-components/issues/23) 63 | * remove `ember-cli-typescript` ([6853e63](https://github.com/alexlafroscia/ember-react-components/commit/6853e63)) 64 | 65 | 66 | 67 | 68 | ## [2.0.2](https://github.com/alexlafroscia/ember-react-components/compare/v2.0.1...v2.0.2) (2018-07-06) 69 | 70 | 71 | ### Bug Fixes 72 | 73 | * add ember-cli option to not import react ([d1bdd31](https://github.com/alexlafroscia/ember-react-components/commit/d1bdd31)) 74 | 75 | 76 | 77 | 78 | ## [2.0.1](https://github.com/alexlafroscia/ember-react-components/compare/v2.0.0...v2.0.1) (2018-06-15) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * update name of default blueprint ([3da2176](https://github.com/alexlafroscia/ember-react-components/commit/3da2176)) 84 | 85 | 86 | 87 | 88 | # [2.0.0](https://github.com/alexlafroscia/ember-react-components/compare/v1.1.0...v2.0.0) (2018-06-15) 89 | 90 | 91 | ### Code Refactoring 92 | 93 | * rename the package to `ember-react-components` ([0bec444](https://github.com/alexlafroscia/ember-react-components/commit/0bec444)) 94 | 95 | 96 | ### BREAKING CHANGES 97 | 98 | * New package name and installation instructions 99 | 100 | 101 | 102 | 103 | # [1.1.0](https://github.com/alexlafroscia/ember-cli-react/compare/v1.0.2...v1.1.0) (2018-06-14) 104 | 105 | 106 | ### Bug Fixes 107 | 108 | * add ember-cli >=2.15 as a peer dependency ([51f3249](https://github.com/alexlafroscia/ember-cli-react/commit/51f3249)) 109 | * be explicit about exported types so declaration emission works ([cf2ba4b](https://github.com/alexlafroscia/ember-cli-react/commit/cf2ba4b)) 110 | * make `render` return type explicit ([1711900](https://github.com/alexlafroscia/ember-cli-react/commit/1711900)) 111 | 112 | 113 | ### Features 114 | 115 | * support stateless, functional components ([2f113e1](https://github.com/alexlafroscia/ember-cli-react/commit/2f113e1)), closes [#5](https://github.com/alexlafroscia/ember-cli-react/issues/5) 116 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Installation 4 | 5 | * `git clone ` 6 | * `cd my-addon` 7 | * `npm install` 8 | 9 | ## Linting 10 | 11 | * `npm run lint:hbs` 12 | * `npm run lint:js` 13 | * `npm run lint:js -- --fix` 14 | 15 | ## Running tests 16 | 17 | * `ember test` – Runs the test suite on the current Ember version 18 | * `ember test --server` – Runs the test suite in "watch mode" 19 | * `ember try:each` – Runs the test suite against multiple Ember versions 20 | 21 | ## Running the dummy application 22 | 23 | * `ember serve` 24 | * Visit the dummy application at [http://localhost:4200](http://localhost:4200). 25 | 26 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ember-react-components 2 | ============================================================================== 3 | 4 | [![Build Status](https://travis-ci.org/alexlafroscia/ember-react-components.svg?branch=master)](https://travis-ci.org/alexlafroscia/ember-react-components) 5 | 6 | > Consume React components in Ember 7 | 8 | This addon is a proof-of-concept for an approach to rendering React components in Ember. It is almost entirely inspired by [a blog post][blog-post] by [Sivakumar Kailasam][sivakumar], from which the general idea was borrowed. 9 | 10 | Installation 11 | ------------------------------------------------------------------------------ 12 | 13 | ```bash 14 | ember install ember-react-components @ember-decorators/babel-transforms 15 | ``` 16 | 17 | Compatibility 18 | ------------------------------------------------------------------------------ 19 | 20 | This addon requires Ember CLI 2.15 or higher. 21 | 22 | Usage 23 | ------------------------------------------------------------------------------ 24 | 25 | This addon provides an ES6 class decorator that allows a React element to be rendered in Ember. 26 | 27 | As an example, you can create a component like this: 28 | 29 | ```javascript 30 | // app/components/my-react-component.js 31 | import React from 'react'; 32 | import WithEmberSupport from 'ember-react-components'; 33 | 34 | @WithEmberSupport 35 | export default class extends React.Component { 36 | render() { 37 | const { name } = this.props; 38 | 39 | return ( 40 |

Hello, {name}

41 | ); 42 | } 43 | } 44 | ``` 45 | 46 | And render it like this: 47 | 48 | ```handlebars 49 | {{my-react-component name='Alex'}} 50 | ``` 51 | 52 | That would create a component that renders `Hello, Alex`. 53 | 54 | Options 55 | ------------------------------------------------------------------------------ 56 | 57 | * `outputFile` option imports `react` and `react-dom` into a separate file than `/assets/vendor.js`. This is useful if your entire Ember application doesn't require `react`. The separate file containing `react` and `react-dom` can be imported via a template or initializer. 58 | 59 | ```javascript 60 | // ember-cli-build.js 61 | let app = new EmberApp(defaults, { 62 | 'ember-react-components': { 63 | outputFile: '/assets/react.js' 64 | } 65 | }); 66 | ``` 67 | 68 | What all is this addon doing? 69 | ------------------------------------------------------------------------------ 70 | 71 | * Provides imports for `react` and `react-dom` 72 | * Hooks up a bunch of necessary `babel` transforms 73 | * Provides a decorator for creating a thin wrapper around your React components that bridge the gap between the two libraries 74 | 75 | Is this production ready? 76 | ------------------------------------------------------------------------------ 77 | 78 | It _does_ work, but you should be really careful about including both the Ember _and_ React libraries in your application since that's quite a lot of JavaScript to ship to your users. 79 | 80 | License 81 | ------------------------------------------------------------------------------ 82 | 83 | This project is licensed under the [MIT License](LICENSE.md). 84 | 85 | [blog-post]: https://medium.com/@sivakumar_k/using-react-components-in-your-ember-app-8f7805d409b0 86 | [sivakumar]: https://github.com/sivakumar-kailasam 87 | -------------------------------------------------------------------------------- /addon/-private/component-is-functional.js: -------------------------------------------------------------------------------- 1 | export default function componentIsFunctional(arg) { 2 | return !(arg.prototype && typeof arg.prototype.render === 'function'); 3 | } 4 | -------------------------------------------------------------------------------- /addon/-private/grant-owner-access.js: -------------------------------------------------------------------------------- 1 | import { setOwner } from '@ember/application'; 2 | 3 | const klassMap = new WeakMap(); 4 | 5 | function ensureMapHasOwner(owner) { 6 | if (!klassMap.has(owner)) { 7 | klassMap.set(owner, new WeakMap()); 8 | } 9 | } 10 | 11 | /** 12 | * Memoizes the "KlassWithOwner" classes, so that only one is generated 13 | * for any given pair of Klass and owner. Otherwise, we generate a new 14 | * class for every render, which not only is bad for performance but also 15 | * breaks the way that the wrapper translates updates to props down to 16 | * the underlying React component. 17 | */ 18 | export default function grantOwnerAccess(Klass, owner) { 19 | ensureMapHasOwner(owner); 20 | 21 | const mapForOwner = klassMap.get(owner); 22 | 23 | // Re-use the class we already created if possible 24 | if (mapForOwner.has(Klass)) { 25 | return mapForOwner.get(Klass); 26 | } 27 | 28 | const KlassWithOwner = class extends Klass { 29 | constructor(...args) { 30 | super(...args); 31 | 32 | setOwner(this, owner); 33 | } 34 | }; 35 | 36 | mapForOwner.set(Klass, KlassWithOwner); 37 | 38 | return KlassWithOwner; 39 | } 40 | -------------------------------------------------------------------------------- /addon/-private/yield-wrapper.js: -------------------------------------------------------------------------------- 1 | import { Component as ReactComponent, createElement } from 'react'; 2 | 3 | /** 4 | * @see https://github.com/AltSchool/ember-cli-react/blob/78f8d09b1eab2d0b12cb4923a3dfd84d46b86f1d/addon/components/react-component.js 5 | * @hide 6 | */ 7 | export default class YieldWrapper extends ReactComponent { 8 | componentDidMount() { 9 | // Different with the integration guide, we avoid jQuery here 10 | const fragment = document.createDocumentFragment(); 11 | for (let node of this.props.nodes) { 12 | fragment.appendChild(node); 13 | } 14 | 15 | // This replace the original DOM element 16 | const element = this.el; 17 | element.parentNode.replaceChild(fragment, element); 18 | } 19 | 20 | render() { 21 | // This element is temporary. When this is mounted, 22 | // it will be replaced by the children nodes, handled by Ember. 23 | return createElement('span', { 24 | ref: el => (this.el = el) 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /addon/index.js: -------------------------------------------------------------------------------- 1 | import EmberComponent from '@ember/component'; 2 | import { get } from '@ember/object'; 3 | import { schedule } from '@ember/runloop'; 4 | import { getOwner } from '@ember/application'; 5 | 6 | import React from 'react'; 7 | import ReactDOM from 'react-dom'; 8 | 9 | import YieldWrapper from './-private/yield-wrapper'; 10 | import grantOwnerAccess from './-private/grant-owner-access'; 11 | import componentIsFunctional from './-private/component-is-functional'; 12 | 13 | const wrapReactComponent = Klass => 14 | class extends EmberComponent { 15 | /* Add type annotation for private `attrs` property on component */ 16 | getPropsForReact() { 17 | return Object.keys(this.attrs).reduce((acc, key) => { 18 | const value = get(this, key); 19 | 20 | acc[key] = value; 21 | 22 | return acc; 23 | }, {}); 24 | } 25 | 26 | mountElement() { 27 | const props = this.getPropsForReact(); 28 | let { children } = props; 29 | 30 | if (!children) { 31 | const childNodes = this.element.childNodes; 32 | children = [ 33 | React.createElement(YieldWrapper, { 34 | key: get(this, 'elementId'), 35 | nodes: [...childNodes] 36 | }) 37 | ]; 38 | } 39 | 40 | let KlassToRender; 41 | 42 | if (componentIsFunctional(Klass)) { 43 | KlassToRender = Klass; 44 | } else { 45 | const owner = getOwner(this); 46 | KlassToRender = grantOwnerAccess(Klass, owner); 47 | } 48 | 49 | ReactDOM.render( 50 | React.createElement(KlassToRender, props, children), 51 | this.element 52 | ); 53 | } 54 | 55 | didUpdateAttrs() { 56 | schedule('render', () => this.mountElement()); 57 | } 58 | 59 | didInsertElement() { 60 | super.didInsertElement(); 61 | 62 | this.mountElement(); 63 | } 64 | 65 | willDestroyElement() { 66 | ReactDOM.unmountComponentAtNode(this.element); 67 | 68 | super.willDestroyElement(); 69 | } 70 | }; 71 | 72 | export default function WithEmberSupport(descriptor) { 73 | return descriptor.toString() === '[object Descriptor]' 74 | ? Object.assign({}, descriptor, { 75 | finisher(Klass) { 76 | return wrapReactComponent(Klass); 77 | } 78 | }) 79 | : wrapReactComponent(descriptor); 80 | } 81 | -------------------------------------------------------------------------------- /blueprints/react-component/files/app/components/__name__.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import WithEmberSupport from 'ember-react-components'; 3 | 4 | @WithEmberSupport 5 | export default class <%= classifiedModuleName %> extends React.Component { 6 | render() { 7 | return

Hello from React

; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /blueprints/react-component/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = { 4 | description: 'Generate a React component' 5 | }; 6 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'] 3 | }; 4 | -------------------------------------------------------------------------------- /config/addon-docs.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | const AddonDocsConfig = require('ember-cli-addon-docs/lib/config'); 5 | 6 | module.exports = class extends AddonDocsConfig { 7 | // See https://ember-learn.github.io/ember-cli-addon-docs/latest/docs/deploying 8 | // for details on configuration you can override here. 9 | }; 10 | -------------------------------------------------------------------------------- /config/deploy.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | module.exports = function(deployTarget) { 5 | let ENV = { 6 | build: {} 7 | // include other plugin configuration that applies to all deploy targets here 8 | }; 9 | 10 | if (deployTarget === 'development') { 11 | ENV.build.environment = 'development'; 12 | // configure other plugins for development deploy target here 13 | } 14 | 15 | if (deployTarget === 'staging') { 16 | ENV.build.environment = 'production'; 17 | // configure other plugins for staging deploy target here 18 | } 19 | 20 | if (deployTarget === 'production') { 21 | ENV.build.environment = 'production'; 22 | // configure other plugins for production deploy target here 23 | } 24 | 25 | // Note: if you need to build some configuration asynchronously, you can return 26 | // a promise that resolves with the ENV object instead of returning the 27 | // ENV object synchronously. 28 | return ENV; 29 | }; 30 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | 5 | module.exports = function() { 6 | return Promise.all([ 7 | getChannelURL('release'), 8 | getChannelURL('beta'), 9 | getChannelURL('canary') 10 | ]).then(urls => { 11 | return { 12 | useYarn: true, 13 | scenarios: [ 14 | { 15 | name: 'ember-lts-2.18', 16 | env: { 17 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 18 | 'jquery-integration': true 19 | }) 20 | }, 21 | npm: { 22 | devDependencies: { 23 | '@ember/jquery': '^0.5.1', 24 | 'ember-source': '~2.18.0' 25 | } 26 | } 27 | }, 28 | { 29 | name: 'ember-lts-3.4', 30 | npm: { 31 | devDependencies: { 32 | 'ember-source': '~3.4.0' 33 | } 34 | } 35 | }, 36 | { 37 | name: 'ember-release', 38 | npm: { 39 | devDependencies: { 40 | 'ember-source': urls[0] 41 | } 42 | } 43 | }, 44 | { 45 | name: 'ember-beta', 46 | npm: { 47 | devDependencies: { 48 | 'ember-source': urls[1] 49 | } 50 | } 51 | }, 52 | { 53 | name: 'ember-canary', 54 | npm: { 55 | devDependencies: { 56 | 'ember-source': urls[2] 57 | } 58 | } 59 | }, 60 | { 61 | name: 'stage-1-decorators', 62 | npm: { 63 | devDependencies: { 64 | '@ember-decorators/babel-transforms': '^2.0.0', 65 | 'ember-decorators': '^2.0.0' 66 | } 67 | } 68 | }, 69 | // The default `.travis.yml` runs this scenario via `npm test`, 70 | // not via `ember try`. It's still included here so that running 71 | // `ember try:each` manually or from a customized CI config will run it 72 | // along with all the other scenarios. 73 | { 74 | name: 'ember-default', 75 | npm: { 76 | devDependencies: {} 77 | } 78 | } 79 | ] 80 | }; 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/* environment, appConfig */) { 4 | return {}; 5 | }; 6 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function(defaults) { 6 | let app = new EmberAddon(defaults, { 7 | // Add options here 8 | }); 9 | 10 | /* 11 | This build file specifies the options for the dummy test app of this 12 | addon, located in `/tests/dummy` 13 | This build file does *not* influence how the addon or the app using it 14 | behave. You most likely want to be modifying `./index.js` or app's build file 15 | */ 16 | 17 | return app.toTree(); 18 | }; 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const configureJsxTransform = require('./lib/configure-jsx-transform'); 4 | const addJsxExtensionSupport = require('./lib/add-jsx-extension-support'); 5 | 6 | module.exports = { 7 | name: require('./package').name, 8 | 9 | included(parent) { 10 | this._super.included.apply(this, arguments); 11 | 12 | configureJsxTransform(parent); 13 | addJsxExtensionSupport(parent); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /lib/add-jsx-extension-support.js: -------------------------------------------------------------------------------- 1 | module.exports = function addJsxExtensionSupport(parent) { 2 | if (parent.options['ember-cli-babel']['extensions']) { 3 | if (!parent.options['ember-cli-babel']['extensions'].includes('js')) { 4 | parent.options['ember-cli-babel']['extensions'].push('js'); 5 | } 6 | if (!parent.options['ember-cli-babel']['extensions'].includes('jsx')) { 7 | parent.options['ember-cli-babel']['extensions'].push('jsx'); 8 | } 9 | } else { 10 | parent.options['ember-cli-babel']['extensions'] = ['js', 'jsx']; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /lib/configure-jsx-transform.js: -------------------------------------------------------------------------------- 1 | const VersionChecker = require('ember-cli-version-checker'); 2 | const { hasPlugin, addPlugin } = require('ember-cli-babel-plugin-helpers'); 3 | 4 | function requireTransform(transformName) { 5 | return require.resolve(transformName); 6 | } 7 | 8 | module.exports = function configureJsxTransform(parent) { 9 | const checker = new VersionChecker(parent).for('ember-cli-babel', 'npm'); 10 | 11 | if (checker.gte('7.0.0')) { 12 | if (!hasPlugin(parent, '@babel/plugin-transform-react-jsx')) { 13 | addPlugin(parent, requireTransform('@babel/plugin-transform-react-jsx')); 14 | } 15 | } else { 16 | parent.project.ui.writeWarnLine( 17 | 'ember-react-components: You are using an unsupported ember-cli-babel version, JSX transforms will not be included automatically' 18 | ); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-react-components", 3 | "version": "3.0.2", 4 | "description": "React component support in Ember", 5 | "keywords": [ 6 | "ember-addon", 7 | "decorator", 8 | "react" 9 | ], 10 | "repository": "https://github.com/alexlafroscia/ember-react-components", 11 | "license": "MIT", 12 | "author": "", 13 | "directories": { 14 | "doc": "doc", 15 | "test": "tests" 16 | }, 17 | "homepage": "https://alexlafroscia.com/ember-react-components", 18 | "scripts": { 19 | "build": "ember build", 20 | "lint:js": "eslint --ext js .", 21 | "start": "ember serve", 22 | "test": "ember test", 23 | "test:all": "ember try:each", 24 | "release": "standard-version" 25 | }, 26 | "husky": { 27 | "hooks": { 28 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 29 | "pre-commit": "lint-staged" 30 | } 31 | }, 32 | "dependencies": { 33 | "@babel/plugin-transform-react-jsx": "^7.3.0", 34 | "ember-auto-import": "^1.2.19", 35 | "ember-cli-babel": "^7.4.1", 36 | "ember-cli-babel-plugin-helpers": "^1.0.2", 37 | "ember-cli-version-checker": "^3.0.1", 38 | "react": "^16.7.0", 39 | "react-dom": "^16.7.0" 40 | }, 41 | "devDependencies": { 42 | "@commitlint/cli": "^7.0.0", 43 | "@commitlint/config-conventional": "^7.0.1", 44 | "@ember-decorators/babel-transforms": "^5.1.2", 45 | "babel-eslint": "^9.0.0", 46 | "broccoli-asset-rev": "^3.0.0", 47 | "ember-cli": "~3.7.1", 48 | "ember-cli-addon-docs": "^0.6.2", 49 | "ember-cli-addon-docs-yuidoc": "^0.2.1", 50 | "ember-cli-dependency-checker": "^3.0.0", 51 | "ember-cli-deploy": "^1.0.2", 52 | "ember-cli-deploy-build": "^1.1.1", 53 | "ember-cli-deploy-git": "^1.3.3", 54 | "ember-cli-deploy-git-ci": "^1.0.1", 55 | "ember-cli-htmlbars": "^3.0.1", 56 | "ember-cli-htmlbars-inline-precompile": "^1.0.3", 57 | "ember-cli-inject-live-reload": "^1.8.2", 58 | "ember-cli-shims": "^1.2.0", 59 | "ember-cli-sri": "^2.1.0", 60 | "ember-cli-testdouble": "^0.1.3", 61 | "ember-cli-uglify": "^2.1.0", 62 | "ember-decorators": "^5.1.2", 63 | "ember-disable-prototype-extensions": "^1.1.2", 64 | "ember-export-application-global": "^2.0.0", 65 | "ember-load-initializers": "^1.1.0", 66 | "ember-maybe-import-regenerator": "^0.1.6", 67 | "ember-metrics": "^0.12.1", 68 | "ember-qunit": "^4.2.0", 69 | "ember-resolver": "^5.0.1", 70 | "ember-source": "~3.4.1", 71 | "ember-source-channel-url": "^1.1.0", 72 | "ember-try": "^1.0.0", 73 | "eslint": "^5.12.1", 74 | "eslint-config-prettier": "^4.0.0", 75 | "eslint-plugin-babel": "^5.3.0", 76 | "eslint-plugin-ember": "^6.2.0", 77 | "eslint-plugin-node": "^8.0.1", 78 | "eslint-plugin-prettier": "^3.0.1", 79 | "eslint-plugin-react": "^7.12.4", 80 | "husky": "^1.3.1", 81 | "lint-staged": "^7.2.0", 82 | "loader.js": "^4.7.0", 83 | "prettier": "^1.16.3", 84 | "qunit-dom": "^0.8.0", 85 | "standard-version": "^8.0.1", 86 | "testdouble": "^2.0.0" 87 | }, 88 | "engines": { 89 | "node": "6.* || 8.* || >= 10.*" 90 | }, 91 | "ember-addon": { 92 | "configPath": "tests/dummy/config" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test_page: 'tests/index.html?hidepassed', 3 | disable_watching: true, 4 | launch_in_ci: ['Chrome'], 5 | launch_in_dev: ['Chrome'], 6 | browser_args: { 7 | Chrome: { 8 | ci: [ 9 | // --no-sandbox is needed when running Chrome inside a container 10 | process.env.CI ? '--no-sandbox' : null, 11 | '--headless', 12 | '--disable-gpu', 13 | '--disable-dev-shm-usage', 14 | '--disable-software-rasterizer', 15 | '--mute-audio', 16 | '--remote-debugging-port=0', 17 | '--window-size=1440,900' 18 | ].filter(Boolean) 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | const App = Application.extend({ 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix, 9 | Resolver 10 | }); 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /tests/dummy/app/components/arrow-function-component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import WithEmberSupport from 'ember-react-components'; 3 | 4 | const ArrowFunctionComponent = ({ children, name }) => { 5 | return ( 6 |
7 |
{name}
8 | {children} 9 |
10 | ); 11 | }; 12 | 13 | export default WithEmberSupport(ArrowFunctionComponent); 14 | -------------------------------------------------------------------------------- /tests/dummy/app/components/basic-component.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET basic-component.js 2 | import React from 'react'; 3 | import WithEmberSupport from 'ember-react-components'; 4 | 5 | @WithEmberSupport 6 | export default class BasicComponent extends React.Component { 7 | render() { 8 | return

Hello from React

; 9 | } 10 | } 11 | // END-SNIPPET 12 | -------------------------------------------------------------------------------- /tests/dummy/app/components/invoke-action.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import WithEmberSupport from 'ember-react-components'; 3 | 4 | @WithEmberSupport 5 | export default class InvokeAction extends React.Component { 6 | render() { 7 | const { action } = this.props; 8 | 9 | return ; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/dummy/app/components/set-state.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import WithEmberSupport from 'ember-react-components'; 3 | 4 | @WithEmberSupport 5 | export default class WithSetState extends React.Component { 6 | constructor() { 7 | super(...arguments); 8 | 9 | const { someValue } = this.props; 10 | 11 | this.state = { 12 | initialValue: someValue 13 | }; 14 | } 15 | 16 | render() { 17 | const { initialValue } = this.state; 18 | const { someValue } = this.props; 19 | 20 | return ( 21 | 22 |

{initialValue}

23 |

{someValue}

24 |
25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/dummy/app/components/traditional-functional-component.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET functional-component.js 2 | import React from 'react'; 3 | import WithEmberSupport from 'ember-react-components'; 4 | 5 | export default WithEmberSupport(function FunctionalComponent(props) { 6 | const { name, children } = props; 7 | 8 | return ( 9 |
10 |
{name}
11 | {children} 12 |
13 | ); 14 | }); 15 | // END-SNIPPET 16 | -------------------------------------------------------------------------------- /tests/dummy/app/components/using-service.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET using-service.js 2 | import React from 'react'; 3 | import WithEmberSupport from 'ember-react-components'; 4 | import { get } from '@ember/object'; 5 | import { 6 | inject as injectService, 7 | service as oldInjectService 8 | } from '@ember-decorators/service'; 9 | 10 | const service = oldInjectService || injectService; 11 | 12 | @WithEmberSupport 13 | export default class UsingService extends React.Component { 14 | @service session; 15 | 16 | render() { 17 | const session = get(this, 'session'); 18 | 19 | return

Hello, {session.userName}

; 20 | } 21 | } 22 | // END-SNIPPET 23 | -------------------------------------------------------------------------------- /tests/dummy/app/components/with-properties.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import WithEmberSupport from 'ember-react-components'; 3 | 4 | @WithEmberSupport 5 | export default class WithProperties extends React.Component { 6 | state = { 7 | updated: false 8 | }; 9 | 10 | render() { 11 | const { foo } = this.props; 12 | 13 | return ( 14 | 15 |

foo equals {foo}

16 | 19 |
20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/dummy/app/components/yield-to-children.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET yield-to-children.js 2 | import React from 'react'; 3 | import WithEmberSupport from 'ember-react-components'; 4 | 5 | @WithEmberSupport 6 | export default class YieldToChildren extends React.Component { 7 | state = { 8 | updated: false 9 | }; 10 | 11 | render() { 12 | const { children } = this.props; 13 | 14 | return
{children}
; 15 | } 16 | } 17 | // END-SNIPPET 18 | -------------------------------------------------------------------------------- /tests/dummy/app/config/environment.d.ts: -------------------------------------------------------------------------------- 1 | export default config; 2 | 3 | /** 4 | * Type declarations for 5 | * import config from './config/environment' 6 | * 7 | * For now these need to be managed by the developer 8 | * since different ember addons can materialize new entries. 9 | */ 10 | declare const config: { 11 | environment: any; 12 | modulePrefix: string; 13 | podModulePrefix: string; 14 | locationType: string; 15 | rootURL: string; 16 | }; 17 | -------------------------------------------------------------------------------- /tests/dummy/app/docs/demo/basic-component/template.hbs: -------------------------------------------------------------------------------- 1 | {{#docs-demo as |demo|}} 2 | {{#demo.example name='demo-basic-component.hbs'}} 3 | {{basic-component}} 4 | {{/demo.example}} 5 | 6 | {{demo.snippet 'basic-component.js' label='components/basic-component.js'}} 7 | {{demo.snippet 'demo-basic-component.hbs'}} 8 | {{/docs-demo}} 9 | -------------------------------------------------------------------------------- /tests/dummy/app/docs/features/children/render-children/template.hbs: -------------------------------------------------------------------------------- 1 | {{#docs-demo as |demo|}} 2 | {{#demo.example name='demo-yield-children.hbs'}} 3 | {{#yield-to-children}} 4 |

This is yielded into the React children

5 | {{/yield-to-children}} 6 | {{/demo.example}} 7 | 8 | {{demo.snippet 'yield-to-children.js' label='yield-to-children.js'}} 9 | {{demo.snippet 'demo-yield-children.hbs'}} 10 | {{/docs-demo}} 11 | 12 | -------------------------------------------------------------------------------- /tests/dummy/app/docs/features/children/template.md: -------------------------------------------------------------------------------- 1 | # Yield to Children 2 | 3 | Basic support for block-style rendering is supported. The yielded content will be rendered to the `children` of the component. 4 | 5 | {{docs/features/children/render-children}} 6 | 7 | One thing to note, however, is that if any elements are dynamically rendered, that _must_ be done within a "stable" element. Something like this _will not_ work. 8 | 9 | ```handlebars 10 | {{#yield-to-children}} 11 | {{#if someCondition}} 12 |

The thing is true

13 | {{else}} 14 |

The thing is not true

15 | {{/if}} 16 | {{/yield-to-children}} 17 | ``` 18 | 19 | This is because the Glimmer engine can get confused due to the way we have to re-located some of the DOM nodes to make this feature work. The conditional must have a parent element that Glimmer knows about. 20 | 21 | ```handlebars 22 | {{#yield-to-children}} 23 |
24 | {{#if someCondition}} 25 |

The thing is true

26 | {{else}} 27 |

The thing is not true

28 | {{/if}} 29 |
30 | {{/yield-to-children}} 31 | ``` 32 | -------------------------------------------------------------------------------- /tests/dummy/app/docs/features/functional/demo-functional-component/template.hbs: -------------------------------------------------------------------------------- 1 | {{#docs-demo as |demo|}} 2 | {{#demo.example name='demo-using-functional-component.hbs'}} 3 | {{#functional-component}} 4 |

This is yielded into the React children

5 | {{/functional-component}} 6 | {{/demo.example}} 7 | 8 | {{demo.snippet 'functional-component.js' label='component.js'}} 9 | {{demo.snippet 'demo-using-functional-component.hbs'}} 10 | {{/docs-demo}} 11 | -------------------------------------------------------------------------------- /tests/dummy/app/docs/features/functional/template.md: -------------------------------------------------------------------------------- 1 | # Stateless, Functional Components 2 | 3 | If you don't want to create a normal, `Class`-based component, `ember-react-components` also supports functional components. 4 | 5 | Instead of applying `WithEmberSupport` as a decorator, you pass a function directly into it and export the returned value. 6 | 7 | {{docs/features/functional/demo-functional-component}} 8 | 9 | Note that if you need service injection, you'll need to use a `Class`-based component instead. 10 | -------------------------------------------------------------------------------- /tests/dummy/app/docs/features/generator/template.md: -------------------------------------------------------------------------------- 1 | # Component Generator 2 | 3 | `ember-react-components` ships with a blueprint called `react-component` for generating new React components in your Ember app. You can run the following: 4 | 5 | ```bash 6 | ember g react-component my-cool-component 7 | ``` 8 | 9 | to generate the following in your application 10 | 11 | ```javascript 12 | // app/components/my-cool-component.js 13 | import React from 'react'; 14 | import WithEmberSupport from 'ember-react-components'; 15 | 16 | @WithEmberSupport 17 | export default class MyCoolComponent extends React.Component { 18 | render() { 19 | return

Hello from React

; 20 | } 21 | } 22 | ``` 23 | -------------------------------------------------------------------------------- /tests/dummy/app/docs/features/services/demo-service-injection/template.hbs: -------------------------------------------------------------------------------- 1 | {{#docs-demo as |demo|}} 2 | {{#demo.example name='demo-using-service.hbs'}} 3 | {{using-service}} 4 | {{/demo.example}} 5 | 6 | {{demo.snippet 'using-service.js' label='component.js'}} 7 | {{demo.snippet 'session-service.js' label='services/session.js'}} 8 | {{/docs-demo}} 9 | -------------------------------------------------------------------------------- /tests/dummy/app/docs/features/services/template.md: -------------------------------------------------------------------------------- 1 | # Using Services 2 | 3 | Services can be used through the [`@ember-decorators`][ember-decorators] addon. Start off by installing it 4 | 5 | ```bash 6 | ember install ember-decorators@next 7 | ``` 8 | 9 | With that set up, you can inject references to a service the same way you can with an Ember component 10 | 11 | {{docs/features/services/demo-service-injection}} 12 | 13 | Note that it's important to use `Ember.get` to access the service if you're working with an Ember version below `3.1`. Above that version, thanks to ES5 getters on computed properties, you should be able to access an injected service just like another other property (without `Ember.get`). 14 | 15 | [ember-decorators]: https://github.com/ember-decorators/ember-decorators 16 | -------------------------------------------------------------------------------- /tests/dummy/app/docs/index/template.md: -------------------------------------------------------------------------------- 1 | ember-react-components 2 | ============================================================================== 3 | 4 | [![Build Status](https://travis-ci.org/alexlafroscia/ember-react-components.svg?branch=master)](https://travis-ci.org/alexlafroscia/ember-react-components) 5 | 6 | > Consume React components in Ember 7 | 8 | This addon is a proof-of-concept for an approach to rendering React components in Ember. It is almost entirely inspired by [a blog post][blog-post] by [Sivakumar Kailasam][sivakumar], from which the general idea was mostly borrowed. 9 | 10 | Usage 11 | ------------------------------------------------------------------------------ 12 | 13 | This addon provides an ES6 class decorator that allows a React element to be rendered in Ember. 14 | 15 | As an example, you can create a component like this: 16 | 17 | {{docs/demo/basic-component}} 18 | 19 | [blog-post]: https://medium.com/@sivakumar_k/using-react-components-in-your-ember-app-8f7805d409b0 20 | [sivakumar]: https://github.com/sivakumar-kailasam 21 | -------------------------------------------------------------------------------- /tests/dummy/app/docs/installation/template.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | To start out, install the following packages 4 | 5 | ```bash 6 | ember install ember-react-components @ember-decorators/babel-transforms 7 | ``` 8 | 9 | One thing to note is that, since this addon uses [decorators][decorators], ESLint might fail to parse your files. By setting [`babel-eslint`][babel-eslint] as the parser, this can be fixed. 10 | 11 | [decorators]: https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy 12 | [babel-eslint]: https://github.com/babel/babel-eslint 13 | -------------------------------------------------------------------------------- /tests/dummy/app/docs/options/template.md: -------------------------------------------------------------------------------- 1 | Options 2 | ------------------------------------------------------------------------------ 3 | 4 | `outputFile` - imports `react` and `react-dom` into a separate file than `/assets/vendor.js`. This is useful if your entire Ember application doesn't require `react`. The separate file containing `react` and `react-dom` can be imported via a template or initializer. 5 | 6 | Update your `EmberApp` in `ember-cli-build` by setting the file you wish `react` and `react-dom` to be imported using the `outputFile` option. 7 | 8 | ```javascript 9 | // ember-cli-build.js 10 | let app = new EmberApp(defaults, { 11 | 'ember-react-components': { 12 | outputFile: '/assets/react.js' 13 | } 14 | }); 15 | ``` 16 | 17 | If you are migrating your Ember application to React you can decide to only include React if a user has a feature flag enabled or when an environment variable is set in a build. Then using an initializer, we can determine whether to include `react` and `react-dom` on the page. 18 | 19 | ```javascript 20 | import config from '../config/environment'; 21 | 22 | export function initialize(App) { 23 | // check the environment variable set in the config/environment file 24 | // or check if we have the isUsingReact feature flag enabled 25 | const isUsingReact = config.isUsingReact || window.featureFlag.isUsingReact; 26 | 27 | if (isUsingReact) { 28 | 29 | App.deferReadiness(); 30 | 31 | let script = document.createElement('script'); 32 | script.type = 'text/javascript'; 33 | script.src = '/assets/react.js'; 34 | script.onload = function() { 35 | App.advanceReadiness(); 36 | }; 37 | 38 | document.body.appendChild(script); 39 | } 40 | } 41 | 42 | export default { 43 | name: 'react', 44 | initialize 45 | }; 46 | ``` 47 | -------------------------------------------------------------------------------- /tests/dummy/app/docs/template.hbs: -------------------------------------------------------------------------------- 1 | {{docs-header logo='ember-cli' addonLogo='ember-cli'}} 2 | 3 | {{#docs-viewer as |viewer|}} 4 | {{#viewer.nav as |nav|}} 5 | {{nav.item 'Overview' 'docs.index'}} 6 | {{nav.item 'Installation' 'docs.installation'}} 7 | {{nav.item 'Options' 'docs.options'}} 8 | 9 | {{nav.section 'Addon Features'}} 10 | {{nav.item 'Component Generator' 'docs.features.generator'}} 11 | 12 | {{nav.section 'Component Features'}} 13 | {{nav.item 'Accessing Services' 'docs.features.services'}} 14 | {{nav.item 'Yield to Children' 'docs.features.children'}} 15 | {{nav.item 'Functional Components' 'docs.features.functional'}} 16 | {{/viewer.nav}} 17 | 18 | {{#viewer.main}} 19 |
20 |
21 | {{outlet}} 22 |
23 |
24 | {{/viewer.main}} 25 | {{/docs-viewer}} 26 | -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ember-react-components 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/instance-initializers/setup-metrics-context.js: -------------------------------------------------------------------------------- 1 | export function initialize(appInstance) { 2 | const metrics = appInstance.lookup('service:metrics'); 3 | const versions = appInstance.lookup('service:project-version'); 4 | 5 | metrics.set('context.appVersion', versions.current); 6 | } 7 | 8 | export default { 9 | initialize 10 | }; 11 | -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import AddonDocsRouter, { docsRoute } from 'ember-cli-addon-docs/router'; 2 | import { get, getWithDefault } from '@ember/object'; 3 | import { inject as service } from '@ember/service'; 4 | import { scheduleOnce } from '@ember/runloop'; 5 | 6 | import config from './config/environment'; 7 | 8 | const Router = AddonDocsRouter.extend({ 9 | metrics: service(), 10 | 11 | location: config.locationType, 12 | rootURL: config.rootURL, 13 | 14 | didTransition() { 15 | this._super(...arguments); 16 | this._trackPage(); 17 | }, 18 | 19 | _trackPage() { 20 | scheduleOnce('afterRender', this, () => { 21 | const page = get(this, 'url'); 22 | const title = getWithDefault(this, 'currentRouteName', 'unknown'); 23 | 24 | get(this, 'metrics').trackPage({ page, title }); 25 | }); 26 | } 27 | }); 28 | 29 | Router.map(function() { 30 | docsRoute(this, function() { 31 | this.route('installation'); 32 | this.route('options'); 33 | 34 | this.route('features', function() { 35 | this.route('generator'); 36 | this.route('services'); 37 | this.route('children'); 38 | this.route('functional'); 39 | }); 40 | }); 41 | }); 42 | 43 | export default Router; 44 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexlafroscia/ember-react-components/c2048e6b06c8fb7f25f40150d33d60209341b309/tests/dummy/app/routes/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/routes/index.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default Route.extend({ 4 | beforeModel() { 5 | this.replaceWith('docs'); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/services/session.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET session-service.js 2 | import Service from '@ember/service'; 3 | 4 | export default Service.extend({ 5 | userName: 'Alex' 6 | }); 7 | // END-SNIPPET 8 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexlafroscia/ember-react-components/c2048e6b06c8fb7f25f40150d33d60209341b309/tests/dummy/app/styles/app.css -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(environment) { 4 | let ENV = { 5 | modulePrefix: 'dummy', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false 17 | } 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | }, 24 | 25 | metricsAdapters: [ 26 | { 27 | name: 'GoogleAnalytics', 28 | environments: ['development', 'production'], 29 | config: { 30 | id: 'UA-50234610-6', 31 | // Use `analytics_debug.js` in development 32 | debug: environment === 'development', 33 | // Use verbose tracing of GA events 34 | trace: environment === 'development', 35 | // Ensure development env hits aren't sent to GA 36 | sendHitTask: environment !== 'development' 37 | } 38 | } 39 | ] 40 | }; 41 | 42 | if (environment === 'development') { 43 | // ENV.APP.LOG_RESOLVER = true; 44 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 45 | // ENV.APP.LOG_TRANSITIONS = true; 46 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 47 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 48 | } 49 | 50 | if (environment === 'test') { 51 | // Testem prefers this... 52 | ENV.locationType = 'none'; 53 | 54 | // keep test console output quieter 55 | ENV.APP.LOG_ACTIVE_GENERATION = false; 56 | ENV.APP.LOG_VIEW_LOOKUPS = false; 57 | 58 | ENV.APP.rootElement = '#ember-testing'; 59 | ENV.APP.autoboot = false; 60 | } 61 | 62 | if (environment === 'production') { 63 | // Allow ember-cli-addon-docs to update the rootURL in compiled assets 64 | ENV.rootURL = 'ADDON_DOCS_ROOT_URL'; 65 | // here you can enable a production-specific feature 66 | } 67 | 68 | return ENV; 69 | }; 70 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "jquery-integration": false 3 | } 4 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions' 7 | ]; 8 | 9 | const isCI = !!process.env.CI; 10 | const isProduction = process.env.EMBER_ENV === 'production'; 11 | 12 | if (isCI || isProduction) { 13 | browsers.push('ie 11'); 14 | } 15 | 16 | module.exports = { 17 | browsers 18 | }; 19 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexlafroscia/ember-react-components/c2048e6b06c8fb7f25f40150d33d60209341b309/tests/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexlafroscia/ember-react-components/c2048e6b06c8fb7f25f40150d33d60209341b309/tests/integration/.gitkeep -------------------------------------------------------------------------------- /tests/integration/utils/with-ember-support-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import { setupRenderingTest } from 'ember-qunit'; 4 | import { click, render } from '@ember/test-helpers'; 5 | import td from 'testdouble'; 6 | 7 | module('Integration | Utility | with-ember-support', function(hooks) { 8 | setupRenderingTest(hooks); 9 | 10 | test('it can render a React component', async function(assert) { 11 | await render(hbs` 12 | {{basic-component}} 13 | `); 14 | 15 | assert 16 | .dom('h1') 17 | .hasText('Hello from React', 'Renders content from a React component'); 18 | }); 19 | 20 | test('it can pass properties to a React component', async function(assert) { 21 | this.set('foo', 'bar'); 22 | 23 | await render(hbs` 24 | {{with-properties foo=foo}} 25 | `); 26 | 27 | assert.dom('button').hasText('Updated is false', 'Has the initial state'); 28 | assert.dom('p').hasText('foo equals bar', 'Renders passed in properties'); 29 | 30 | await click('button'); 31 | 32 | assert.dom('button').hasText('Updated is true', 'Has the updated state'); 33 | 34 | this.set('foo', 'some new value'); 35 | 36 | assert 37 | .dom('p') 38 | .hasText('foo equals some new value', 'Updates when properties change'); 39 | assert 40 | .dom('button') 41 | .hasText('Updated is true', 'Maintains the updated state'); 42 | }); 43 | 44 | test('an action passed into the component can be called', async function(assert) { 45 | const action = td.function(); 46 | this.set('action', action); 47 | 48 | await render(hbs` 49 | {{invoke-action action=action}} 50 | `); 51 | 52 | await click('button'); 53 | 54 | assert.equal( 55 | td.explain(action).callCount, 56 | 1, 57 | 'Invoked the passed in action' 58 | ); 59 | }); 60 | 61 | test('state is persisted through updated props', async function(assert) { 62 | this.set('prop', 'foo'); 63 | 64 | await render(hbs` 65 | {{set-state someValue=prop}} 66 | `); 67 | 68 | assert 69 | .dom('[data-test="state"]') 70 | .hasText('foo', 'Sets the state to the initial value'); 71 | assert 72 | .dom('[data-test="prop"]') 73 | .hasText('foo', 'The initial prop value is displayed'); 74 | 75 | this.set('prop', 'bar'); 76 | 77 | assert 78 | .dom('[data-test="state"]') 79 | .hasText('foo', 'Has the initial state after props change'); 80 | assert 81 | .dom('[data-test="prop"]') 82 | .hasText('bar', 'The prop has actually updated'); 83 | }); 84 | 85 | module('usage with `ember-decorators`', function() { 86 | test('it works with services', async function(assert) { 87 | this.owner.lookup('service:session').set('userName', 'Alex'); 88 | 89 | await render(hbs`{{using-service}}`); 90 | 91 | assert.dom('p').hasText('Hello, Alex'); 92 | }); 93 | }); 94 | 95 | module('supporting yields and children', function() { 96 | test('it can yield the block to the React children', async function(assert) { 97 | await render(hbs` 98 | {{#yield-to-children}} 99 |

Child content

100 | {{/yield-to-children}} 101 | `); 102 | 103 | assert.dom('#wrapper').exists(); 104 | assert.dom('h1').exists(); 105 | }); 106 | 107 | test('the yield can have multiple children', async function(assert) { 108 | await render(hbs` 109 | {{#yield-to-children}} 110 |

Foo

111 |

Bar

112 | {{/yield-to-children}} 113 | `); 114 | 115 | assert.dom('#wrapper').exists(); 116 | assert.dom('[data-test="foo"]').exists(); 117 | assert.dom('[data-test="bar"]').exists(); 118 | }); 119 | }); 120 | 121 | module('supporting functional components', function() { 122 | module('with traditional functions', function() { 123 | test('it can render them inline', async function(assert) { 124 | await render(hbs` 125 | {{traditional-functional-component name='Alex'}} 126 | `); 127 | 128 | assert.dom('[data-test-name]').hasText('Alex'); 129 | }); 130 | 131 | test('it can render children', async function(assert) { 132 | await render(hbs` 133 | {{#traditional-functional-component}} 134 |
Foo
135 | {{/traditional-functional-component}} 136 | `); 137 | 138 | assert.dom('[data-test="foo"]').hasText('Foo'); 139 | }); 140 | }); 141 | 142 | module('with arrow functions', function() { 143 | test('it can render them inline', async function(assert) { 144 | await render(hbs` 145 | {{arrow-function-component name='Alex'}} 146 | `); 147 | 148 | assert.dom('[data-test-name]').hasText('Alex'); 149 | }); 150 | 151 | test('it can render children', async function(assert) { 152 | await render(hbs` 153 | {{#arrow-function-component}} 154 |
Foo
155 | {{/arrow-function-component}} 156 | `); 157 | 158 | assert.dom('[data-test="foo"]').hasText('Foo'); 159 | }); 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from '../app'; 2 | import config from '../config/environment'; 3 | import { setApplication } from '@ember/test-helpers'; 4 | import { start } from 'ember-qunit'; 5 | 6 | setApplication(Application.create(config.APP)); 7 | 8 | start(); 9 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexlafroscia/ember-react-components/c2048e6b06c8fb7f25f40150d33d60209341b309/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "allowJs": true, 5 | "moduleResolution": "node", 6 | "allowSyntheticDefaultImports": true, 7 | "noImplicitAny": true, 8 | "noImplicitThis": true, 9 | "alwaysStrict": true, 10 | "strictNullChecks": true, 11 | "strictPropertyInitialization": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noEmitOnError": false, 17 | "noEmit": true, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "baseUrl": ".", 21 | "module": "es6", 22 | "experimentalDecorators": true, 23 | "paths": { 24 | "dummy/tests/*": [ 25 | "tests/*" 26 | ], 27 | "dummy/*": [ 28 | "tests/dummy/app/*", 29 | "app/*" 30 | ], 31 | "ember-react-components": [ 32 | "addon" 33 | ], 34 | "ember-react-components/*": [ 35 | "addon/*" 36 | ], 37 | "*": [ 38 | "types/*" 39 | ] 40 | } 41 | }, 42 | "include": [ 43 | "app", 44 | "addon", 45 | "tests", 46 | "types" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexlafroscia/ember-react-components/c2048e6b06c8fb7f25f40150d33d60209341b309/vendor/.gitkeep --------------------------------------------------------------------------------