diff --git a/README.md b/README.md
index 3a53895..bf56ad3 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,10 @@
**Experimental Addon**
-This was built as a prototype to evaluate using react inside of our Ember apps. We are not yet using it in production. PRs and constructive questions and comments via [GitHub issues](https://github.com/AltSchool/ember-cli-react/issues/new) are highly encouraged.
+This was built as a prototype to evaluate using React inside of our Ember apps.
+We are not yet using it in production. PRs and constructive questions and
+comments via [GitHub
+issues](https://github.com/AltSchool/ember-cli-react/issues/new) are highly
+encouraged.
# ember-cli-react
@@ -26,15 +30,16 @@ yarn add --dev ember-cli-react
ember generate ember-cli-react
```
-**NOTE**:
-`ember-cli-react` relies on a custom resolver to discover components. If you have
-installed `ember-cli-react` with the standard way then you should be fine. Otherwise, you will need to manually update the first line of `app/resolver.js` to `import Resolver from 'ember-cli-react/resolver';`.
+**NOTE**: `ember-cli-react` relies on a custom resolver to discover components.
+If you have installed `ember-cli-react` with the standard way then you should be
+fine. Otherwise, you will need to manually update the first line of
+`app/resolver.js` to `import Resolver from 'ember-cli-react/resolver';`.
## Usage
Write your React component as usual:
-```javascript
+```jsx
// app/components/say-hi.jsx
import React from 'react';
@@ -49,11 +54,13 @@ Then render your component in a handlebars template:
{{say-hi name="Alex"}}
```
-**NOTE**: Currently, `ember-cli-react` recognizes React components with `.jsx` extension only.
+**NOTE**: Currently, `ember-cli-react` recognizes React components with `.jsx`
+extension only.
## Block Form
-Your React component can be used in block form to allow composition with existing Ember or React components.
+Your React component can be used in block form to allow composition with
+existing Ember or React components.
```handlebars
{{#react-panel}}
@@ -63,8 +70,9 @@ Your React component can be used in block form to allow composition with existin
The children of `react-panel` will be populated to `props.children`.
-Note that if the children contains mutating structure (e.g. `{{if}}`, `{{each}}`),
-you need to wrap them in a stable tag to work around [this Glimmer issue](https://github.com/yapplabs/ember-wormhole/issues/66#issuecomment-263575168).
+Note that if the children contains mutating structure (e.g. `{{if}}`,
+`{{each}}`), you need to wrap them in a stable tag to work around [this Glimmer
+issue](https://github.com/yapplabs/ember-wormhole/issues/66#issuecomment-263575168).
```handlebars
{{#react-panel}}
@@ -78,8 +86,80 @@ you need to wrap them in a stable tag to work around [this Glimmer issue](https:
{{/react-panel}}
```
-Although this is possible, block form should be used as a tool to migrate Ember to React
-without the hard requirement to start with leaf components. It is highly recommended to have clean React component tree whenever possible for best performance.
+Although this is possible, block form should be used as a tool to migrate Ember
+to React without the hard requirement to start with leaf components. It is
+highly recommended to have clean React component tree whenever possible for best
+performance.
+
+## Using File Name Convention for React
+
+React is unopinionated with file name convention. However, the majority of the
+community has still developed some conventions over time.
+
+For React component files, the widely adopted convention is PascalCase,
+including
+[Airbnb](https://github.com/airbnb/javascript/tree/master/react#naming). So we
+have added support for this convention.
+
+In short, you can name your JSX files in `PascalCase`, in addition to
+`snake-case`.
+
+```handlebars
+{{!-- Both `user-avatar.jsx` and `UserAvatar.jsx` work --}}
+{{user-avatar}}
+```
+
+### Rendering in Template
+
+When using the `react-component` component, referencing your React components
+with `PascalCase` is also supported. However, due to the "at least one dash"
+policy, it won't work if the component name is used directly.
+
+```handlebars
+{{!-- OK! --}}
+{{react-component "user-avatar"}}
+
+{{!-- OK! --}}
+{{react-component "UserAvatar"}}
+
+{{!-- OK! --}}
+{{user-avatar}}
+
+{{!-- NOT OK! --}}
+{{UserAvatar}}
+```
+
+### Single-worded Component
+
+Ember requires at least a dash for component names. So single-worded component
+(e.g. `Avatar`) cannot be used directly in Handlebars. However, you can still
+use single-worded component with `react-component` component.
+
+```handlebars
+{{!-- This won't work because Ember requires a dash for component --}}
+{{avatar}}
+
+{{!-- This works --}}
+{{react-component 'Avatar'}}
+```
+
+### React Components are Prioritised
+
+Whenever there is a conflict, component files with React-style convention will
+be used.
+
+Examples:
+
+- When both `SameName.jsx` and `same-name.jsx` exist, `SameName.jsx` will be
+ used
+- When both `SameName.jsx` and `same-name.js` (Ember) exist, `SameName.jsx` will
+ be used
+
+#### Known issue
+
+If an Ember component and a React component has exactly the same name but different extension (`same-name.js` and
+`same-name.jsx`), the file with `.js` extension will be overwritten with the
+output of `same-name.jsx`. We are still looking at ways to resolve this.
## Mini Todo List Example
@@ -176,6 +256,9 @@ export default class TodoItem extends React.Component {
## What's Missing
-There is no React `link-to` equivalent for linking to Ember routes inside of your React code. Instead pass action handlers that call `transitionTo` from an Ember route or component.
+There is no React `link-to` equivalent for linking to Ember routes inside of
+your React code. Instead pass action handlers that call `transitionTo` from an
+Ember route or component.
-In order to create minified production builds of React you must set `NODE_ENV=production`.
+In order to create minified production builds of React you must set
+`NODE_ENV=production`.
diff --git a/addon/resolver.js b/addon/resolver.js
index d93231d..ac240b9 100644
--- a/addon/resolver.js
+++ b/addon/resolver.js
@@ -6,26 +6,58 @@ import ReactComponent from 'ember-cli-react/components/react-component';
const { get } = Ember;
export default Resolver.extend({
+ // `resolveComponent` is triggered when rendering a component in template.
+ // For example, having `{{foo-bar}}` in a template will trigger `resolveComponent`
+ // with the name full name of `component:foo-bar`.
resolveComponent(parsedName) {
- const result = this.resolveOther(parsedName);
+ // First try to resolve with React-styled file name (e.g. SayHi).
+ // If nothing is found, try again with original convention via `resolveOther`.
+ let result =
+ this._resolveReactStyleFile(parsedName) || this.resolveOther(parsedName);
+ // If there is no result found after all, return nothing
if (!result) {
return;
}
+ // If there is an Ember component found, return it.
+ // This includes the `react-component` Ember component.
if (get(result, 'isComponentFactory')) {
return result;
} else {
+ // This enables using React Components directly in template
return ReactComponent.extend({
reactComponent: result,
});
}
},
+ // This resolver method is defined when we try to lookup from `react-component`.
+ // We create a new namespace `react-component:the-component` for them.
resolveReactComponent(parsedName) {
parsedName.type = 'component';
- const result = this.resolveOther(parsedName);
+ const result =
+ this._resolveReactStyleFile(parsedName) || this.resolveOther(parsedName);
parsedName.type = 'react-component';
return result;
},
+
+ // This resolver method attempt to find a file with React-style file name.
+ // A React-style file name is in PascalCase.
+ // This is made a private method to prevent creation of "react-style-file:*"
+ // factory.
+ _resolveReactStyleFile(parsedName) {
+ const originalName = parsedName.fullNameWithoutType;
+
+ // Convert the compnent name while preserving namespaces
+ const parts = originalName.split('/');
+ parts[parts.length - 1] = Ember.String.classify(parts[parts.length - 1]);
+ const newName = parts.join('/');
+
+ const parsedNameWithPascalCase = Object.assign({}, parsedName, {
+ fullNameWithoutType: newName,
+ });
+ const result = this.resolveOther(parsedNameWithPascalCase);
+ return result;
+ },
});
diff --git a/package.json b/package.json
index 88843a2..8818f72 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
"eslint --fix",
"git add"
],
- "+(*.{json,css}|.prettierrc|.watchmanconfig)": [
+ "+(*.{json,css,md}|.prettierrc|.watchmanconfig)": [
"prettier --write",
"git add"
]
diff --git a/tests/dummy/app/components/Card.jsx b/tests/dummy/app/components/Card.jsx
new file mode 100644
index 0000000..b752bc5
--- /dev/null
+++ b/tests/dummy/app/components/Card.jsx
@@ -0,0 +1,7 @@
+import React from 'react';
+
+const Card = () => {
+ return I am a Card component, I have no dash!;
+};
+
+export default Card;
diff --git a/tests/dummy/app/components/ReactStyleFileName.jsx b/tests/dummy/app/components/ReactStyleFileName.jsx
new file mode 100644
index 0000000..24afa8e
--- /dev/null
+++ b/tests/dummy/app/components/ReactStyleFileName.jsx
@@ -0,0 +1,7 @@
+import React from 'react';
+
+const ReactStyleFileName = () => {
+ return My file name is ReactStyleFileName;
+};
+
+export default ReactStyleFileName;
diff --git a/tests/dummy/app/components/SameNameDifferentCaseMixed.jsx b/tests/dummy/app/components/SameNameDifferentCaseMixed.jsx
new file mode 100644
index 0000000..1917eb3
--- /dev/null
+++ b/tests/dummy/app/components/SameNameDifferentCaseMixed.jsx
@@ -0,0 +1,7 @@
+import React from 'react';
+
+const SameNameDifferentCaseMixed = () => {
+ return My file name is "SameNameDifferentCaseMixed.jsx";
+};
+
+export default SameNameDifferentCaseMixed;
diff --git a/tests/dummy/app/components/SameNameJsx.jsx b/tests/dummy/app/components/SameNameJsx.jsx
new file mode 100644
index 0000000..b017324
--- /dev/null
+++ b/tests/dummy/app/components/SameNameJsx.jsx
@@ -0,0 +1,7 @@
+import React from 'react';
+
+const SameNameJsxWithPascalCase = () => {
+ return My file name is "SameNameJsx.jsx";
+};
+
+export default SameNameJsxWithPascalCase;
diff --git a/tests/dummy/app/components/namespace/InsideNamespace.jsx b/tests/dummy/app/components/namespace/InsideNamespace.jsx
new file mode 100644
index 0000000..5f92cd3
--- /dev/null
+++ b/tests/dummy/app/components/namespace/InsideNamespace.jsx
@@ -0,0 +1,7 @@
+import React from 'react';
+
+const InsideNamespace = () => {
+ return I am inside a namespace!;
+};
+
+export default InsideNamespace;
diff --git a/tests/dummy/app/components/same-name-different-case-mixed.js b/tests/dummy/app/components/same-name-different-case-mixed.js
new file mode 100644
index 0000000..a3e1d83
--- /dev/null
+++ b/tests/dummy/app/components/same-name-different-case-mixed.js
@@ -0,0 +1,3 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend();
diff --git a/tests/dummy/app/components/same-name-jsx.jsx b/tests/dummy/app/components/same-name-jsx.jsx
new file mode 100644
index 0000000..7e12e9c
--- /dev/null
+++ b/tests/dummy/app/components/same-name-jsx.jsx
@@ -0,0 +1,7 @@
+import React from 'react';
+
+const SameNameJsxWithSnakeCase = () => {
+ return My file name is "same-name-jsx.jsx";
+};
+
+export default SameNameJsxWithSnakeCase;
diff --git a/tests/dummy/app/templates/components/same-name-different-case-mixed.hbs b/tests/dummy/app/templates/components/same-name-different-case-mixed.hbs
new file mode 100644
index 0000000..11ab511
--- /dev/null
+++ b/tests/dummy/app/templates/components/same-name-different-case-mixed.hbs
@@ -0,0 +1 @@
+My file name is "same-name-different-case-mixed.js"
\ No newline at end of file
diff --git a/tests/integration/components/react-component-test.js b/tests/integration/components/react-component-test.js
index e9c596c..2148d28 100644
--- a/tests/integration/components/react-component-test.js
+++ b/tests/integration/components/react-component-test.js
@@ -323,6 +323,156 @@ describeComponent(
expect(this.$().text()).to.contain('Hello Morgan');
});
+
+ it('supports React-style component file name', function() {
+ this.render(hbs`{{react-component "ReactStyleFileName"}}`);
+
+ expect(
+ this.$()
+ .text()
+ .trim()
+ ).to.equal('My file name is ReactStyleFileName');
+ });
+
+ it('supports React-style component file name, but render with Ember style name', function() {
+ this.render(hbs`{{react-component "react-style-file-name"}}`);
+
+ expect(
+ this.$()
+ .text()
+ .trim()
+ ).to.equal('My file name is ReactStyleFileName');
+ });
+
+ it('supports React-style component file name even without dash', function() {
+ this.render(hbs`{{react-component "card"}}`);
+
+ expect(
+ this.$()
+ .text()
+ .trim()
+ ).to.equal('I am a Card component, I have no dash!');
+ });
+
+ it('supports React-style component file name when rendering directly', function() {
+ this.render(hbs`{{react-style-file-name}}`);
+
+ expect(
+ this.$()
+ .text()
+ .trim()
+ ).to.equal('My file name is ReactStyleFileName');
+ });
+
+ it('supports React-style component file name with namespace', function() {
+ this.render(hbs`{{react-component "namespace/InsideNamespace"}}`);
+
+ expect(
+ this.$()
+ .text()
+ .trim()
+ ).to.equal('I am inside a namespace!');
+ });
+
+ it('supports React-style component file name when rendering directly with namespace', function() {
+ this.render(hbs`{{namespace/inside-namespace}}`);
+
+ expect(
+ this.$()
+ .text()
+ .trim()
+ ).to.equal('I am inside a namespace!');
+ });
+
+ describe('when both `SameNameJsx.jsx` and `same-name-jsx.jsx` exist', function() {
+ it('prioritises React-style file name (SameNameJsx.jsx)', function() {
+ this.render(hbs`{{react-component "SameNameJsx"}}`);
+
+ expect(
+ this.$()
+ .text()
+ .trim()
+ ).to.equal('My file name is "SameNameJsx.jsx"');
+ });
+
+ it('prioritises React-style file name (SameNameJsx.jsx) when render with Ember-style name', function() {
+ this.render(hbs`{{react-component "same-name-jsx"}}`);
+
+ expect(
+ this.$()
+ .text()
+ .trim()
+ ).to.equal('My file name is "SameNameJsx.jsx"');
+ });
+
+ it('prioritises React-style file name (SameNameJsx.jsx) when rendering directly', function() {
+ this.render(hbs`{{same-name-jsx}}`);
+
+ expect(
+ this.$()
+ .text()
+ .trim()
+ ).to.equal('My file name is "SameNameJsx.jsx"');
+ });
+ });
+
+ describe('when both `SameNameDifferentCaseMixed.jsx` and `same-name-different-case-mixed.js` (Ember) exist', function() {
+ it('prioritises the React component (SameNameDifferentCaseMixed.jsx)', function() {
+ this.render(hbs`{{react-component "SameNameDifferentCaseMixed"}}`);
+
+ expect(
+ this.$()
+ .text()
+ .trim()
+ ).to.equal('My file name is "SameNameDifferentCaseMixed.jsx"');
+ });
+
+ it('prioritises the React component (SameNameDifferentCaseMixed.jsx) when render with Ember-style name', function() {
+ this.render(
+ hbs`{{react-component "same-name-different-case-mixed"}}`
+ );
+
+ expect(
+ this.$()
+ .text()
+ .trim()
+ ).to.equal('My file name is "SameNameDifferentCaseMixed.jsx"');
+ });
+
+ it('prioritises the React component (SameNameDifferentCaseMixed.jsx) when rendering directly', function() {
+ this.render(hbs`{{same-name-different-case-mixed}}`);
+
+ expect(
+ this.$()
+ .text()
+ .trim()
+ ).to.equal('My file name is "SameNameDifferentCaseMixed.jsx"');
+ });
+ });
+
+ // The React file will overwrite Ember file as that's how Broccoli-React works.
+ // Skipping this to keep this in mind.
+ describe.skip('when both `same-name-same-case-mixed.jsx` and `same-name-same-case-mixed.js` (Ember) exist', function() {
+ it('prioritises the React component (same-name-same-case-mixed.js)', function() {
+ this.render(hbs`{{react-component "same-name-same-case-mixed"}}`);
+
+ expect(
+ this.$()
+ .text()
+ .trim()
+ ).to.equal('My file name is "same-name-same-case-mixed.jsx"');
+ });
+
+ it('prioritises the React component (same-name-same-case-mixed.js) when rendering directly', function() {
+ this.render(hbs`{{same-name-ember}}`);
+
+ expect(
+ this.$()
+ .text()
+ .trim()
+ ).to.equal('My file name is "same-name-same-case-mixed.jsx"');
+ });
+ });
});
}
);