React integration

There’s no guide on react integration, yet the guides intro lists a lot of frameworks with ready integration to only have polymer section.

I agree it would be nice to have an example showing off how flexible Aurelia can be.

I once gave a day to experimenting with an Aurelia version of the Office UI Fabric library. In it, I simply rendered Office UI Fabric React components within an Aurelia component. It worked great but I didnt go much further than the most basic example. You can see it here:

Here is how I’m doing it

This is my react feature

import {noView, customElement, bindable} from 'aurelia-templating';
import {decorators} from 'aurelia-metadata';
import {React, render} from 'design-components/imports';

/**
 * Configure the aurelia loader to use handle urls with !component
 * @param config {FrameworkConfiguration}
 */
export function configure(config) {
    const loader = config.aurelia.loader;
    loader.addPlugin('react', {
        fetch(address) {
            return loader.loadModule(address)
                .then(getComponents);
        }
    });
}

/**
 * Extract the components from the loaded module
 * @param module {Object} Object containing all exported properties
 * @returns {Object}
 */
function getComponents(module) {
    return Object.keys(module).reduce((elements, name) => {
        if (typeof module[name] === 'function') {
            const elementName = camelToKebab(name);
            elements[elementName] = wrapComponent(module[name], elementName);
        }
        return elements;
    }, {});
}

/**
 * Converts camel case to kebab case
 * @param str {String}
 * @returns {string}
 */
function camelToKebab(str) {
    // Matches all places where a two upper case chars followed by a lower case char are and split them with an hyphen
    return str.replace(/([a-zA-Z])([A-Z][a-z])/g, (match, before, after) => {
        return `${before.toLowerCase()}-${after.toLowerCase()}`;
    }).toLowerCase();
}

/**
 * Wrap the React components into an ViewModel with bound attributes for the defined PropTypes
 * @param component {Object}
 * @param elementName {string}
 */
function wrapComponent(component, elementName) {
    let bindableProps = [];
    if (component.propTypes) {
        bindableProps = Object.keys(component.propTypes).map(prop => bindable({
            name: prop,
            attribute: camelToKebab(prop),
            changeHandler: 'render',
            defaultBindingMode: 1
        }));
    }
    return decorators(
        noView(),
        customElement(elementName),
        bindable({name: 'props', attribute: 'props', changeHandler: 'render', defaultBindingMode: 1}),
        ...bindableProps
    ).on(createWrapperClass(component));
}

/**
 * Create a wrapper class for the component
 * @param component {Object}
 * @returns {WrapperClass}
 */
function createWrapperClass(component) {
    return class WrapperClass {
        static inject = [Element];

        constructor(element) {
            this.element = element;
        }

        bind() {
            this.render();
        }

        attached() {
            this.render();
        }

        render() {
            const props = this.props || {};
            // Copy bound properties because Object.assign doesn't work deep
            for (const prop in this) {
                if (this[prop] !== undefined && typeof this[prop] !== 'function') {
                    props[prop] = this[prop];
                }
            }
            render(
                React.createElement(component, props),
                this.element
            );
        }
    };
}

Registering the feature and make react components global in main.js

export function configure(aurelia) {
  aurelia.use
    .standardConfiguration()
    .feature('feature/react/index.js')
    .globalResources([
      'react!design-components/index'
    ]);

   // ....
}

Use it

<my-react-component some-prop.bind="newUrl"></my-react-component>

Hope that helps :slight_smile:

Best
FragSalat

2 Likes

@fragsalat How is your design-components/imports structured or how do you create it? I can’t find anything in Aurelia’s feature documentation on it. And would this work differently if I’m using Webpack?

This is a reposity with components of our styleguide were developing for our applications.
You can find it here https://github.com/fabric-design/components

1 Like

So Zalando is using Aurelia somewhere? Last time i talked to few of your devs i was under impression you guys are die-hard React only fans :slight_smile:

We have more React thats true but in my Team I build the Apps with Aurelia and as well some Angular apps are there as well^^

Ah, so you’re basically just importing this file with that call so your users can pick between React and Preact?

And then you’re importing a list of the exported React component classes with this:

.globalResources([
  'react!design-components/index'
]);

That index file is just a list of export {MyReactElement} from './path/to/MyReactElement'; lines.

Update: So, long story short, I need to set up webpack.config.js correctly (and I’m assuming JSPM/SystemJS/etc. would work somewhat similarly). It’s made even more fun by my decision to use TypeScript – a few updates are needed in tsconfig.json as well. Basically, in both files there are places where the .ts file type is included in a list of file globs and/or file type handlers, and .tsx needs to be added to each of those places.
We may need to publish an AUCS guide. :slight_smile:

Does anyone on this thread want to put together an article on the subject? We’d love to add it into our official integration documentation. Perhaps @fragsalat ???

1 Like

I can put it on my list but since the article about the http-client-mock usage is also still not done I won’t promise anything.

1 Like

Quick follow-up on the loader plugin you wrote. How does that work with nested React components? For example, the MS Office UI Fabric requires me to do this:

<Fabric>
  <PrimaryButton text="Click Here">
</Fabric>

With your loader I can do <fabric></fabric> (with no children) but I can’t do what I really need to do, namely:

<fabric>
  <primary-button text.bind="'Click Here'"></primary-button>
</primary-fabric>

Any ideas? I assume it has to do with passing the child elements as props.children in React, but I need to actually have that child rendering tree be passed back to Aurelia so that the databinding etc. can all work. Is that possible to achieve just with the loader, or am I going to have to roll my own custom element for each component in MS Office UI Fabric?

Actually, the more I work on this, the more I think this is dead in the water unless we can address the nesting issue. Any help or pointers would be tremendously appreciated. :slight_smile:

Update: I’ve looked at every fix I (or Google) could think of, and the more I look at this the more it seems to me that this scenario is beyond what Aurelia & React are capable of doing together, at least without major modifications. At this point I’m having a hard time justifying further effort so I’m looking closely at what it will take to start over in React (& co.) for our current project. If there’s anything that can be done to make this work with Aurelia, please let me know ASAP. Thanks!

Ok. I didn’t debugged it yet but I would guess that the problem here is as following.
The template engine of Aurelia will parse the template from top to bottom to check which elements and attributes to render. It recognizes the tags for the react components which have a registered view model in the template registry. Now the engine probably will build the elements lvl by lvl and the child react view model called React.createElement. This one is attached to the parent but now the parent calls React.createElement and will overwrite the div with the children with it’s own element.

A possibility could be to check if there are children which have a VNode in it and pass it to create element as children.

I’ll check that possibility when I’m at work :slight_smile:

1 Like

So it seems like I found a solution. During bind lifecycle I’m checking if the current ViewModel is a parent or a child. If it’s a child I register it to it’s parent. During the attached lifecycle I call the react createElement function on all children and pass it to the createElement fn of the current ViewModel.
As well I only call the render function if it is a root.

With this construct you can use completely the aurelia binding engine to pass properties to specific react components.
I would expect this construct to work with preact as well.

Here I put together a (for me) working example :slight_smile:

2 Likes

@fragsalat

I’m curious to ask this for vue.js too, Is it possible? (an integration between Aurelia and vue.js)

Thank you!! It’s a step in the right direction, for sure. It works for a single parent-child combination, but it doesn’t work for, e.g., a React parent with an HTML child.

This works:

<fabric>
  <primary-button props.bind="{ text:'Data-bound Greeting' }"></primary-button>
</fabric>

This fails (the React component is rendered with no children):

<fabric>
  <p>Hello World</p>
</fabric>

Using your sample, the equivalent would be:

<parent-component>
  <p>Hello World</p>
</parent-component>

In case it matters, I’m using React with TypeScript. The only thing that I had to change (apparently) to get your example working was to replace render(...) with ReactDOM.render(...) in the attached() function.

I also get the following error in the console (Chrome stable) with the single React parent+single React child example:

Each child in an array or iterator should have a unique “key” prop.
Check the top-level render call using <CustomizedFabric>. See https://fb.me/react-warning-keys for more information.

I can manually add that key to the props binding which is okay as a workaround, but I’m hoping that can be automated in the loader. I think the bigger problem, though, is that this approach doesn’t (yet) work (at least for me) with HTML (i.e., non-React) components.

I also see a warning, and incorrectly nested DOM output, when I have multiple React-based child elements, as in the following:

<fabric>
  <primary-button props.bind="{ key:'test 1', text:'Data-bound Greeting' }"></primary-button>
  <primary-button props.bind="{ key:'test 2', text:'Data-bound Greeting #2' }"></primary-button>
</fabric>

Warning message:

validateDOMNesting(...): <button> cannot appear as a descendant of <button>.

It looks like the elements are being attached to the wrong parent element which causes siblings to be partially nested inside each other instead of being actual siblings within their parent.

Ok I’ll have a look at it when I’m at work again. The goal is to somehow transform the markup into a VNode which React can handle. Then also html should be possible.

@HamedFathi I’m not 100% sure since I never had a deep look at vue but the general process remains the same. So rendering only one level will work but how the child rendering works I have no clue.

2 Likes

I just recognized I’ve didn’t answered this with my latest solution. I actually managed it to render DOM managed by Aurelia as children into an React/Preact component. The way I did this is kind of simple :slight_smile: I’m passing React.createElement(‘slot’) as children to the render function which renders the React element with a <slot> html element as children. After that I’m searching for the slot element and prepend all html elements inside <au-content> before the <slot>. If there is no <slot> element the React component doesn’t accept children. After projecting the HTML Elements to the <slot> I’m removing the <slot> and <au-content> and insert the whole component into this.element.

If someone want this I could make a PR to the existing aurelia-react-loader from bryan smith or create another plugin if the PR is not merged. Just like the post and I’ll do it^^

Btw now my rendering looks like this:


    /**
     * Render Preact component
     */
    render() {
      // Create container in active dom to apply styles already
      const container = document.createElement('div');
      this.element.appendChild(container);

      // Render react component with a slot as children into a container to possibly replace the slot with real children
      const reactElement = React.createElement(component, this.getProps(), React.createElement('slot'));
      this.component = render(reactElement, container);
      this.element.component = this.component;

      const slot = container.querySelector('slot');
      // If no slot is rendered the component doesn't accept children
      if (slot) {
        const content = this.element.querySelector('au-content');
        if (!content) {
          return;
        }
        // Move original children to slot position
        for (let i = 0; i < content.children.length; i++) {
          slot.parentNode.insertBefore(content.children[i], slot);
        }
        slot.parentNode.removeChild(slot);
        this.insertContainerContent(container, content);
      } else {
        this.insertContainerContent(container);
      }
    }

    /**
     * Moves content of the container into the correct place within this element
     * @param {HTMLElement} container
     * @param {HTMLElement} replacement
     */
    insertContainerContent(container, replacement) {
      // Append child to fragment to get rid of container element which can break element flow
      const fragment = document.createDocumentFragment();
      for (let i = 0; i < container.children.length; i++) {
        fragment.appendChild(container.children[i]);
      }
      // Either replace au-content or just append if no children are passed
      if (replacement) {
        this.element.replaceChild(fragment, replacement);
      } else {
        this.element.appendChild(fragment);
      }
      // Container is now obsolete as the children are laying directly under the parent
      this.element.removeChild(container);
    }

Would you mind sharing your project on github or similar? I am exploring the possibilty to use React components from Office-UI-fabric.

I requested bryan smith if he accepts pr’s on his aurelia-react-loader plugin and if he’s not answering tomorrow I could create a new plugin :frowning:

Let’s hope that he’s still active^^