` inside the component html.
118 | After the children are moved, the slot placeholder gets removed and the html is appended to the custom element's `` element.
119 |
120 | ### Lifecycle
121 | - **CustomElement.attached()** -> React component will be rendered if not done yet and mounted to the dom and therefore `componentDidMount` will be called
122 | - **CustomElement.detached()** -> React component is unmounted from dom and `componentWillUnmount` will be called
123 | - **CustomElement.unbind()** -> React component will be completely destroyed and un-rendered
124 | - **CustomElement.anyChanged()** -> Once any bound attribute changes `componentWillReceiveProps` will be called and props on component will be updated
125 |
126 |
127 | A few things to note:
128 | * React component names are converted to kebab case for safe use in HTML. `` in jsx becomes `` in an HTML Aurelia template.
129 | * Pass props to the React component with the `props` binding. The component will be re-rendered when the binding changes.
130 | * If you need to reference the React component directly it is stored in the `component` property of the custom element's view model. You can use a `ref` binding to access it.
131 | * All functions exported from the required module are assumed to be React components, and wrapped with custom elements. Both stateful and stateless React components are supported.
132 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import {noView, customElement, bindable} from 'aurelia-templating';
2 | import {decorators} from 'aurelia-metadata';
3 | import {createElement} from 'react';
4 | import {render} from 'react-dom';
5 |
6 | /**
7 | * Configure the aurelia loader to use handle urls with !component
8 | * @param {FrameworkConfiguration} config
9 | */
10 | export function configure(config) {
11 | const loader = config.aurelia.loader;
12 | loader.addPlugin('react-component', {
13 | fetch(address) {
14 | return loader.loadModule(address)
15 | .then(getComponents);
16 | }
17 | });
18 | }
19 |
20 | /**
21 | * Extract the components from the loaded module
22 | * @param {Object} module Object containing all exported properties
23 | * @returns {Object}
24 | */
25 | export function getComponents(module) {
26 | return Object.keys(module).reduce((elements, name) => {
27 | if (typeof module[name] === 'function') {
28 | const elementName = camelToKebab(name);
29 | elements[elementName] = wrapComponent(module[name], elementName);
30 | }
31 | return elements;
32 | }, {});
33 | }
34 |
35 | /**
36 | * Converts camel case to kebab case
37 | * @param {string} str
38 | * @returns {string}
39 | */
40 | function camelToKebab(str) {
41 | // Matches all places where a two upper case chars followed by a lower case char are and split them with an hyphen
42 | return str.replace(/([a-zA-Z])([A-Z][a-z])/g, (match, before, after) =>
43 | `${before.toLowerCase()}-${after.toLowerCase()}`
44 | ).toLowerCase();
45 | }
46 |
47 | /**
48 | * Wrap the React components into an ViewModel with bound attributes for the defined PropTypes
49 | * @param {Object} component
50 | * @param {string} elementName
51 | * @returns {Object}
52 | */
53 | function wrapComponent(component, elementName) {
54 | let bindableProps = [];
55 | if (component.propTypes) {
56 | bindableProps = Object.keys(component.propTypes).map(prop => bindable({
57 | name: prop,
58 | attribute: camelToKebab(prop),
59 | changeHandler: 'updateProps',
60 | defaultBindingMode: 1
61 | }));
62 | }
63 | return decorators(
64 | noView(),
65 | customElement(elementName),
66 | bindable({name: 'props', attribute: 'props', changeHandler: 'updateProps', defaultBindingMode: 1}),
67 | ...bindableProps
68 | ).on(createWrapperClass(component));
69 | }
70 |
71 | /**
72 | * Create a wrapper class for the component
73 | * @param {Object} component
74 | * @returns {WrapperClass}
75 | */
76 | function createWrapperClass(component) {
77 | return class WrapperClass {
78 | static inject = [Element];
79 |
80 | /**
81 | * @param {Element} element
82 | */
83 | constructor(element) {
84 | this.element = element;
85 | }
86 |
87 | /**
88 | * Re-render the Preact component when values changed
89 | */
90 | attached() {
91 | if (!this.component) {
92 | this.render();
93 | } else if (typeof this.component.componentDidMount === 'function') {
94 | this.component.componentDidMount();
95 | }
96 | }
97 |
98 | /**
99 | * Triggers un-mound function to release events
100 | */
101 | detached() {
102 | if (this.component && typeof this.component.componentWillUnmount === 'function') {
103 | this.component.componentWillUnmount();
104 | }
105 | }
106 |
107 | /**
108 | * Un-render the component
109 | */
110 | unbind() {
111 | this.component = null;
112 | this.element.component = null;
113 | render('', this.element, this.component);
114 | }
115 |
116 | /**
117 | * Determine props passed to create react elements
118 | * @returns {Object}
119 | */
120 | getProps() {
121 | const props = this.props || {};
122 | // Copy bound properties because Object.assign doesn't work deep
123 | for (const prop in this) {
124 | if (this[prop] !== undefined && typeof this[prop] !== 'function') {
125 | props[prop] = this[prop] === '' ? true : this[prop];
126 | }
127 | }
128 | delete props.element;
129 |
130 | return Object.assign({}, component.defaultProps, props);
131 | }
132 |
133 | /**
134 | * Will be called when bindable updated
135 | */
136 | updateProps() {
137 | if (this.component && typeof this.component.componentWillReceiveProps === 'function') {
138 | const props = this.getProps();
139 | this.component.componentWillReceiveProps(props);
140 | this.component.props = props;
141 | }
142 | }
143 |
144 | /**
145 | * Render Preact component
146 | */
147 | render() {
148 | // Create container in active dom to apply styles already
149 | const container = document.createElement('div');
150 | this.element.appendChild(container);
151 |
152 | // Render react component with a slot as children into a container to possibly replace the slot with real children
153 | const reactElement = createElement(component, this.getProps(), createElement('slot'));
154 | this.component = render(reactElement, container);
155 | this.element.component = this.component;
156 |
157 | const slot = container.querySelector('slot');
158 | // If no slot is rendered the component doesn't accept children
159 | if (slot) {
160 | const content = this.element.querySelector('au-content');
161 | if (!content) {
162 | return;
163 | }
164 | // Move original children to slot position
165 | for (let i = 0; i < content.children.length; i++) {
166 | slot.parentNode.insertBefore(content.children[i], slot);
167 | }
168 | slot.parentNode.removeChild(slot);
169 | this.insertContainerContent(container, content);
170 | } else {
171 | this.insertContainerContent(container);
172 | }
173 | }
174 |
175 | /**
176 | * Moves content of the container into the correct place within this element
177 | * @param {HTMLElement} container
178 | * @param {HTMLElement} replacement
179 | */
180 | insertContainerContent(container, replacement) {
181 | // Append child to fragment to get rid of container element which can break element flow
182 | const fragment = document.createDocumentFragment();
183 | for (let i = 0; i < container.children.length; i++) {
184 | fragment.appendChild(container.children[i]);
185 | }
186 | // Either replace au-content or just append if no children are passed
187 | if (replacement) {
188 | this.element.replaceChild(fragment, replacement);
189 | } else {
190 | this.element.appendChild(fragment);
191 | }
192 | // Container is now obsolete as the children are laying directly under the parent
193 | this.element.removeChild(container);
194 | }
195 | };
196 | }
197 |
--------------------------------------------------------------------------------