Code Monkey home page Code Monkey logo

react-habitat's Introduction

Deloitte Digital

React Habitat Build Status PRs Welcome

v1.0 Released πŸŽ‰ - Please see migration guides.

Looking for the v0.4 docs?

React Habitat <3 Your CMS

React Habitat is designed for integrating React with your CMS using the DOM as the interface. It's based of some basic container programming principles and brings peace and order to multi-page apps.

This framework exists so you can get on with the fun stuff!

When to use React Habitat

You should use React Habitat any time there is a framework or CMS rendering your HTML and you want one or multiple React components on the page(s). For example, sometimes there are only sections of your page that you want to be a React Component, this framework is perfect for that.

The idea behind this is that, rather than trying to initiate one or many React components; by either hard coding or using a Router. You switch it around so components "new up" themselves when required.

React Habitat works great with:

  • Sitecore
  • Adobe Experience Manager
  • Hybris
  • Umbraco
  • Drupal
  • Joomla
  • WordPress
  • Magento
  • ... etc

When not to use it

Typically if you're building a single page application (SPA) with only a <div id="app"> in the body tag ... then this framework isn't really going to bring many benefits to you. However, you are definitely invited to use it, if you want to.

Features

  • Tiny code footprint (only 4.4kb)
  • Redux supported
  • Pass data (props) to your components directly from HTML attributes and back again
  • Automatic data/JSON parsing
  • All page child apps can still share the same components, stores, events etc. (Everything is connected)
  • Simple to swap out components for others (The beauty of IOC containers)
  • For advanced users, you can use different components for different build environments
  • 100% W3C HTML5 Valid
  • TypeScript definitions included

Table of Contents

Compatibility

  • Supports Browsers IE9+ and all the evergreens.
  • ES5, ES6/7 & TypeScript
  • React v15 and up

Polyfills

We highly recommend you use something like WebPack or Browserify when using this framework.

Installing

Install with NPM

npm install --save react-habitat

This assumes that you’re using a package manager with a module bundler like Webpack or Rollup.

If you don’t use a module bundler, and would prefer a single-file UMD build that makes ReactHabitat available as a global object, you can grab a pre-built version from the dist folder.

πŸŽ“ Getting Started

Using ES5? Read the ES5 spec here.

The basic pattern for integrating React Habitat into your application is:

  • Create a Container Builder.
  • Register React components.
  • Set the container for later use in the DOM.
  • At application execution ...
  • Use the DOM scope to resolve instances of the components.

This getting started guide walks you through these steps for a simple React application. This document assumes you already know:

  • How to compile JSX; and
  • How to bundle using something like webpack or browserify

1. Create a bootstrapper class

The class must extend ReactHabitat.Bootstrapper and is intended to be an entry point of your bundled app. So if you're using something like webpack or browserify then this is the file to point it too.

In the constructor() of the class you need to register your React components with it and then set the container. The container is later bound to the DOM automatically so your React components self-initiate.

In React Habitat, you'd register a component 'as' a unique key with something like this:

containerBuilder.register(SomeReactComponent).as('SomeReactComponent');

So for our sample application, we need to register all of our components to be exposed to the DOM so things get wired up nicely.

We also need to build and store the container so it can be used to resolve components later

import ReactHabitat                 from 'react-habitat';
import SomeReactComponent           from './SomeReactComponent';

class MyApp extends ReactHabitat.Bootstrapper {
    constructor(){
        super();

        // Create a new container builder:
        const builder = new ReactHabitat.ContainerBuilder();

        // Register a component:
        builder.register(SomeReactComponent).as('SomeReactComponent');

        // Or register a component to load on demand asynchronously:
        builder.registerAsync(() => System.import('./AnotherReactComponent')).as('AnotherReactComponent');

        // Finally, set the container:
        this.setContainer(builder.build());
    }
}

// Always export a 'new' instance so it immediately evokes:
export default new MyApp();

By default ReactHabitat ships with a plain ReactDOM factory.

If you are using Redux

You will need to use a different factory. Please install & configure the react-habitat-redux library. Then continue with step 2 below.

Alternatively learn how to write and use your own custom factory.

If you are using TypeScript

You will need to import ReactHabitat using import * as ReactHabitat from 'react-habitat' syntax in order to avoid 'module has no default export' error.

2. Application execution - resolve your components

During the web application execution, you will want to make use of the components you registered. You do this by resolving them in the DOM from a scope.

When you resolve a component, a new instance of the object gets created (Resolving a component is roughly equivalent to calling 'new').

To resolve new instances of your components, you need to attach a data-component attribute to a div or a span element in the HTML. Any child components should be nested inside the React components themselves.

Set the data-component value to equal a component key you have registered the component as.

For instance:

<div data-component="SomeReactComponent"></div>

Will be resolved by the following registration.

container.register(SomeReactComponent).as('SomeReactComponent');

So, for our sample app, we would do something like this

<html>
    <body>
        <div data-component="SomeReactComponent"></div>
        <script src="myBundle.js" />
    </body>
</html>

When you view this page you will see an instance of SomeReactComponent automatically rendered in the div's place. In fact, you can add as many as you like and it will render multiple instances.

For example. This is perfectly valid.

<html>
    <body>
        <div data-component="SomeReactComponent"></div>
        <div data-component="SomeReactComponent"></div>
        <div data-component="SomeReactComponent"></div>
        <script src="myBundle.js" />
    </body>
</html>

Will render 3 instances of your component.

⚠️ It's important that the output built javascript file is included at the end of the DOM just before the closing body tag.

Resolving and registering components alone is not all that special, but passing data to it via HTML attributes is pretty useful. This allows the backend to easily pass data to your components in a modular fashion. To do this you use predefined prefix's such as data-prop.

For example, the following would create a new MyReactComponent instance with title and colour props.

<div
    data-component="MyReactComponent"
    data-prop-title="My Title"
    data-prop-colour="#BADA55"
>
</div>

Going Further

The getting start guide gives you an idea of how to use React Habitat, but there's a lot more you can do.

Learn more about:

Still Need Help?

Please ask questions on StackOverflow tagged with react-habitat (We have notifications turned on).

⬆ back to top

πŸ“– API

Registering components

You register components with React Habitat by creating a ReactHabitat.ContainerBuilder and informing the builder which components to expose to the DOM.

Each component is exposed to the DOM using the as() method on the ContainerBuilder.

// Create a new builder:
const builder = new ReactHabitat.ContainerBuilder();

// Register SomeComponent and expose it to the DOM as 'MySomeComponent':
builder.register(SomeComponent).as('MySomeComponent');

// Build the container to finalise registrations:
const container = builder.build();

Passing options to register

You can pass render options with each registrations using the withOptions() method on the ContainerBuilder.

Property Type Description
tag string (optional) The tag to use for the rendered Habitat that houses the component eg 'span'
className string (optional) The Habitat's CSS class name
replaceDisabled boolean (optional) If true, the original node will be left in the dom. False by default

Example using withOptions():

// Register SomeComponent and expose it to the DOM as 'MySomeComponent'
builder
    .register(SomeComponent)
    .as('MySomeComponent')
    .withOptions({
        tag: 'div',
        className: 'myHabitat',
    });

You can also define default options for all registrations by passing the options object in as the first argument when creating a new ContainerBuilder instance.

Example setting defaults for all registrations:

// Register SomeComponent and expose it to the DOM as 'MySomeComponent':
const builder = new ContainerBuilder({
    tag: 'div',
    className: 'myHabitat',
});

⚠️ options can also be configured with HTML attributes. Any options defined with HTML attributes will always take precedence.

Passing default props to register

Typically, you would define the default props in the React component itself. However, there may be instances where you would like different defaults for multiple registrations.

You can pass default props with each registration using the withDefaultProps() method on the ContainerBuilder.

// Register SomeComponent and expose it to the DOM as 'MySomeComponent'
builder
    .register(SomeComponent)
    .as('MySomeComponent')
    .withDefaultProps({
        title: 'My new default title'
    });

⚠️ proxy is a React Habitat reserved prop name. Read more about using the proxy in passing data back again.

⬆ back to top

Dynamic imports and code splitting

React Habitat supports resolving components asynchronously by returning Promises. Use registerAsync to define asynchronous registrations, pass in a function that returns a Promise, that resolves to a React component.

For example:

container
    .registerAsync(() => new Promise((resolve, reject) => {
        // Do async work to get 'component', then:
        resolve(component);
    }))
    .as('AsyncReactComponent');

React Habitat has no restrictions on how you want to resolve your components however this does enable you to define code split points.

Code splitting is one great feature that means our visitors don't need to download the entire app before they can use it. Think of code splitting as incrementally download your application only as its needed.

While there are other methods for code splitting we will use Webpack for these examples.

Webpack 2 & 3 treats System.import() as a split-point and puts the requested module into a separate chunk.

So for example, we could create a split point using System.import() like this:

container.registerAsync(() => System.import('./components/MyComponent')).as('MyComponent');

registerAsync expects a function that returns a Promise, that resolves with a React Component. Since System.import IS a Promise, that allows us to use it directly.

Here is an example using require.ensure() to define a split-point in webpack 1

container
    .registerAsync(() => new Promise((resolve) => {
        require.ensure(['./components/MyComponent'], () => {
            resolve(require('./components/MyComponent'));
        });
    }))
    .as('AsyncReactComponent');

⬆ back to top

Writing and using custom factories

A factory is used to define how components are injected into the DOM. The default factory is simple wrapper of ReactDOM.

Where as the ReactHabitatRedux one wraps Components in a React Redux Provider. You can write custom factories do what ever you want with components and control how they are added to the dom.

A factory is simply a plain javascript class that must have two methods implemented inject and dispose.

Example:

class MyCustomFactory {

    inject(module, props, target) {
        // ...
    }

    dispose(target) {
        // ...
    }
}

Inject

inject(module, props, target)

  • module is the component that was registered.
  • props is the props for the component.
  • target is the html node intended for the module/component.

Dispose

dispose(target)

  • target is the html node containing the module/component that needs to be teared down.

Using factories

To define a factory, just set factory on the container builder before calling build.

const containerBuilder = new ReactHabitat.ContainerBuilder();

containerBuilder.factory = new MyCustomFactory();

If you create a compatible factory, please let us know so we can include a link to it from this page.

⬆ back to top

Resolving components

After you have registered your components, you can resolve components from the built container inside your DOM. You do this by setting the data-component attribute on a HTML element such as a div, span or input etc.

<div data-component="MyComponent"></div>

This will resolve and render a component that was registered as('MyComponent'). It's important to note that this "target" type element by default will be replaced with what we refer to as a Habitat that houses your component. However, <input />'s will always remain in the DOM so it's data is available on a form post (see passing data back again).

In addition to the prop attributes, some Habitat options can also be configured with attributes.

Attribute Description
data-habitat-class Set the Habitat's css class
data-habitat-no-replace Control the original node replacement behaviour

⚠️ options can also be configured with a registration. Any options defined with HTML attributes will always take precedence.

⬆ back to top

Passing properties (props) to your components

To set props you have a few choices. You can use all of these or only some (they merge) so just use what's suits you best for setting properties.

Attribute Description
data-props Maps encoded JSON to props.
data-prop-* This prefix maps in strings, booleans, null, array or encoded JSON to a prop.
data-n-prop-* This prefix maps in numbers and floats to a prop.
data-r-prop-* This prefix maps a reference to an object that exists on the global scope (window) to a prop.

⚠️ proxy is a reserved prop name. Read more about using the proxy in passing data back again.

Prefix

With an attribute prefix the * may be replaced by any name. This allow's you to define the property name. Property names must be all lower-case and hyphens will be automatically converted to camel case.

For example:

data-prop-title would expose title on the props object inside the component.

data-prop-my-title would expose myTitle on the props object inside the component.

data-props

Set component props via an encoded JSON string on the data-props attribute.

For example:

<div data-component="SomeReactComponent" data-props='{"title": "A nice title"}'></div>

data-prop-*

Set a component prop via prefixing attributes with data-prop-.

For example:

data-prop-title would expose title as a property inside the component.

⚠️ JSON, booleans & null are automatically parsed. Eg data-prop-my-bool="true" would expose the value of true, NOT the string representation "true".

Passing in an array of objects will require you to use HTML encoded characters for quotes etc i.e "foo" will replace "foo".

Simple example:

<div data-component="SomeReactComponent"
    data-prop-title="A nice title"
    data-prop-show-title="true">
</div>

Would expose props as:

class SomeReactComponent extends React.Component {

    constructor(props) {
        super(props);

        props.title === "A nice title";  //> true
        props.showTitle === true;        //> true
    }

    render() {
        return <div>{ this.props.showTitle ? this.props.title : null }</div>;
    }
}

JSON example:

<div
    data-component="SomeReactComponent"
    data-prop-person='{"name": "john", "age": 22}'>
</div>

Would expose as:

class MyReactComponent extends React.Component {
    constructor(props) {
        super(props);

        return (
            <div>
                Name: {this.props.person.name}
                Age: {this.props.person.age}
            </div>
        );
    }
}

data-n-prop-*

Set a component prop with type [number] via prefixing attributes with data-n-prop-.

For example data-n-prop-temperature="33.3" would expose the float value of 33.3 and not the string representation '33.3'.

This is handy if you know that a property is always going to be a number or float.

data-r-prop-*

Referenced a global variable in your component prop via prefixing attributes with data-r-prop-.

For example:

<script>
    var foo = window.foo = 'bar';
</script>

<div data-component="SomeReactComponent" data-r-prop-foo="foo"></div>

This is handy if you need to share properties between Habitats or you need to set JSON onto the page.

⬆ back to top

Passing values back again

It can be handy to pass values back again, particularly for inputs so the backend frameworks can see any changes or read data.

Every React Habitat instance is passed in a prop named proxy, this is a reference the original dom element.

⚠️ only <inputs /> are left in the DOM by default. To keep a generic element in the DOM, set the data-habitat-no-replace="true" attribute.

So for example, we could use proxy to update the value of an input like so

<input id="personId" type="hidden" data-component="personLookup" />

Somewhere inside the component

this.props.proxy.value = '1234'

Sometimes you may additionally need to call this.props.proxy.onchange() if you have other scripts listening for this event.

⬆ back to top

Setting the Habitat's CSS class

You can set a custom CSS class on the Habitat element by setting the data-habitat-class attribute on the target element. Alternatively you can use the withOptions method on the registration.

Example:

<div data-component="MyComponent" data-habitat-class="my-css-class"></div>

Will result in the following being rendered:

<div data-habitat="C1" class="my-css-class">...</div>

⬆ back to top

Replace original node

By default only <inputs /> are left in the DOM when a React Habitat is created.

To keep a generic element in the DOM, set the data-habitat-no-replace="true" attribute.

Alternatively you can use the withOptions method on the registration.

Use encoded JSON in HTML attributes

When passing JSON to an attribute you need to remember its actually JSON inside a string, you will need to encode the value so that content can be preserved and properly rendered.

⚠️ Please note using data-r-prop instead may be better suited for you.

As a general rule, escape the following characters with HTML entity encoding:

  • & --> &amp;
  • < --> &lt;
  • > --> &gt;
  • " --> &quot;
  • ' --> &#x27;
  • / --> &#x2F;

Example:

<div data-props="{&quot;foo&quot;&colon; &quot;bar&quot;}"></div>

Additionally, an encoder may replace extended ASCII characters with the equivalent HTML entity encoding.

Most backend systems are capable of doing this automatically. An alternative is to use the data-r-prop-* option.

Should I use attribute Single of Double Quotes?

Double quotes around attributes values are the most common. There is a known hack of wrapping JSON attributes with single quotes and escaping nested single quotes.

Example:

<div data-props='{"restaurant": "Bob\'s bar and grill"}'></div>

We will use this method in the docs to maintain readability. However, we strongly recommend you encode all JSON inside attributes.

⬆ back to top

Controlling Scope and Lifetime

⬆ back to top

Changing the Habitat query selector

Default: 'data-component'

By default React Habitat will resolve components via the data-component attribute. You can configure this by assigning the componentSelector property in your constructor.

It will accept any string containing any valid attribute name.

Example:

class MyApp extends ReactHabitat.Bootstrapper {
    constructor(){
        super();

        this.componentSelector = 'data-myComponents';
    }
}

⬆ back to top

Dynamic Updates

update()

The update method will scan the DOM for any new targets that require wiring up (i.e after ajaxing in some HTML).

Example:

class MyApp extends ReactHabitat.Bootstrapper {
    someMethod() {
        // This will scan the entire document body
        this.update();
    }
}

By default update() will scan the entire body, however, a node can optionally be passed in for better performance if you know where the update has occurred.

Example:

class MyApp extends ReactHabitat.Bootstrapper {
    someMethod() {
        // Will scan just the children of the element with id 'content':
        this.update(document.getElementById('content'))
    }
}

You can call this method from somewhere else in your app by importing it:

import MyApp from './MyApp';

// ...

MyApp.update();

Or you can expose it onto the window object for legacy code.

class MyApp extends ReactHabitat.Bootstrapper {
    constructor() {

        // ...

        window.updateHabitat = this.update.bind(this);
    }
}

// ...

window.updateHabitat();

⬆ back to top

Bootstrapper Lifecycle Events

ReactHabitat.Bootstrapper has "lifecycle methods" that you can override to run code at particular times in the process.

Method Description
shouldUpdate(node) Called when an update has been requested. Return false to cancel the update.
willUpdate(node) Called when an update is about to take place.
didUpdate(node) Called after an update has taken place.
willUnmountHabitats() Called when all active React Habitats are about to be unmounted.
didUnmountHabitats() Called after all active React Habitats have been unmounted.
didDispose() Called after all active React Habitats have been unmounted and the container released.

An "update" is the event when registrations are resolved and a React mount will/did occur.

Example:

class MyApp extends ReactHabitat.Bootstrapper {
    shouldUpdate(node) {
        // Dont allow updates on div's:
        if (node.tagName === 'div') {
            return false;
        }
    }

    willUpdate(node) {
        console.log('I am about to update.', node);
    }

    didUpdate(node) {
        console.log('I just updated.', node);
    }
}

⬆ back to top

Unmount React Habitats

To unmount all React Habitat instances. Call the unmountHabitats() method.

Example:

class MyApp extends ReactHabitat.Bootstrapper {
    constructor(){
        super();

        // ...

        this.unmountHabitats();
    }
}

⬆ back to top

Disposing the container

To unload the container and remove all React Habitat instances. Call the dispose() method.

Example:

class MyApp extends ReactHabitat.Bootstrapper {
    constructor(){
        super();

        // ...

        this.dispose();
    }
}

⬆ back to top

Want to contribute?

  • Got an amazing idea to make this better?
  • Found an annoying bug?

Please don't hesitate to raise an issue through GitHub or open a pull request to show off your fancy pants coding skills - we'll really appreciate it!

Key Contributors

  • @jenna_salau

Who is Deloitte Digital?

Part Business. Part Creative. Part Technology. One hundred per cent digital.

Pioneered in Australia, Deloitte Digital is committed to helping clients unlock the business value of emerging technologies. We provide clients with a full suite of digital services, covering digital strategy, user experience, content, creative, engineering and implementation across mobile, web and social media channels.

http://www.deloittedigital.com/au

LICENSE (BSD-3-Clause)

Copyright (C) 2017, Deloitte Digital. All rights reserved.

React Habitat can be downloaded from: https://github.com/DeloitteDigitalAPAC/react-habitat

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  • Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  • Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

  • Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

⬆ back to top

react-habitat's People

Contributors

6stringbeliever avatar arielkirkwood avatar finnfiddle avatar jennasalau avatar joshuakelly avatar samuelalvin avatar wpprodigy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-habitat's Issues

Concerning React 18 and migration to createRoot

Helloes,

Here are same observations from my brief investigation on how this library could be updated to React 18 and most notably the createRoot mechanic. Hopefully these remarks can be useful if that journey ever comes around.

  1. Changing the render method invocation in inject method of ReactDomFactory.js to createRoot is quite straightforward, but we will probably need to stash the returned root values somewhere and use them later for unmounting in dispose

  2. Unit tests might break badly since in React 18 the rendering is no longer an instant synchronous operation and the unit tests seem to expect this, consider for example:

// tests/Bootstrapper.spec.js, it('should render a component', (done) => { ...
...
const app = new App(containerBuilder.build(), () => {
	const componentLookup = node.innerHTML.match(/\[component MockComponent\]/g);
	expect(componentLookup).not.toEqual(null);
	expect(componentLookup.length).toEqual(1);

	done();
});
...

The second parameter to App is a callback that probably gets invoked once react-habitat thinks things are done. It works fine pre-createRoot. However, swicthing to createRoot breaks this test since nothing will appear in node at the time when this callback gets executed. Wrapping the expects into a timeout makes the test pass, but the root cause probably needs to be fixed elsewhere in the library:

// tests/Bootstrapper.spec.js, it('should render a component', (done) => { ...
...
const app = new App(containerBuilder.build(), () => {
	setTimeout(() => {
		const componentLookup = node.innerHTML.match(/\[component MockComponent\]/g);
		expect(componentLookup).not.toEqual(null);
		expect(componentLookup.length).toEqual(1);

		done();
	}, 0);
});
...

Additionally, when observing the unit test output in a Karma-browser window I can see the component render. To me these findings suggest that it is the no-longer-synchronous React 18 rendering that is the problem here.

Good luck!

Typescript usage with Bable compiled Files

So Babel does some magic with export default you can see how it conflicts here:

microsoft/TypeScript#5565 (comment)

Supposedly this is being addressed?

babel/babel#2212

Anyway under typescript the example actually becomes:

import * as ReactHabitat from 'react-habitat';
import { Hello } from './components/Hello';

class MyApp extends ReactHabitat.default.Bootstrapper {
    constructor(){
        super();

        // Create a new container builder
        var container = new ReactHabitat.default.Container();

        // Register your top level component(s) (ie mini/child apps)
        container.register('Hello', Hello);

        // Finally, set the container
        this.setContainer(container);
    }
}


// Always export a 'new' instance so it immediately evokes
export default new MyApp();

Note the lookup: ReactHabitat.default.Bootstrapper and ReactHabitat.default.Container()

FR: Automatic react-hot-loader integration

I can use react-hot-loader on any of my components to get hot reloading working…but it would be nice if the builder had a hot boolean (option)[https://github.com/DeloitteDigitalAPAC/react-habitat#passing-options-to-register] to control this for any registered components.

Clearly state features in Readme

Not to be a dick (and I hope I don't come off that way.) but what is the purpose of this library?

How is it any different from just doing something like this:

untested example:

<!doctype html>
<html>
...
<body>
    <div data-component="NavigationComponent"></div>
    <div data-component="SlideshowComponent"></div>
    <div data-component="FooterComponent "></div>
</body>
</html>
// bootstrap.js
const componentMap = {
    NavigationComponent : React.lazy(() => import('./NavigationComponent'));
    SlideshowComponent : React.lazy(() => import('./SlideshowComponent'));
    FooterComponent : React.lazy(() => import('./SlideshowComponent'));
};

const loadComponent = (componentName) => 
    Array.from(document.querySelectorAll(`[data-component=${componentName}]`))
       .forEach((node) => ReactDOM.render(componentMap[componentName], node))


Object.keys(componentMap)
    .forEach(loadComponent );

Perhaps it would be helpful if there was a feature list, or list of things this library does that otherwise wouldn't be possible. Does it allow for compatibility where otherwise there would be conflicts? Does it allow for multiple instances of react? What problems do react-habitat seek to solve?

Promise fired when all lazily components are loaded

Is it possible to fire a callback when all components registered with registerAsync are loaded? So far I resorted to a dirty hack (which is not fully correct, but gives an idea what I want to do) shown below.

// ...
const build = containerBuilder.build();
Promise.all(Object.values(build._registrations).map((registration) => registration._operator()))
   .then(() => console.log("hurray"));

Is there a cleaner way to do that?

The reason I want this is that I want to avoid flickering when N different components are loaded individually. I want all of them to render at once and at the same time if a component is not used I'ld like to not download it's chunk.

Questions

Hello,
Where can I find an example of usage with a CMS ?

As I understand: you feed it with html and it will add the components. But how can I feed it exactly?

What are comparable react projects/components that do about the same thing?

  • Ed

Passing an array of objects as a prop

When i pass down [{}]. It works

When i pass down [{ id: 1 }]. It does not work.

What i've found is that you can pass down [{"1":1}] and that does work.

Is this intended?

Asynced components child imports not included in chunk

Do you want to request a feature or report a bug?
I think this might be a bug either in webpack 4 or react-habitat. Need to look a bit further into this.

What is the current behavior?

Async components are being added into seperated chunks but a child component of a chunk is not inside that chunk instead it is in the main.js file

Example:

I've a megamenu. in the constructor we are do containerBuilder.registerAsync(() => System.import('./containers/MegaMenuContainer')).as('RMegaMenu')

then inside that component we do
import { MegaMenu } from '@components-library';

What is the expected behavior?
All child imports of a dynamically imported file ends up in async chunk, if it is not used anywhere else.

webpack v4.28.4
react-habitat v1.0.1

Habitat Components Must Be Empty

Is there a technical reason that habitat components must be empty?

I'm trying to be a good web citizen and progressively enhance my page. So for example, I want to add a react component that's an image gallery, but I'd like to have to have the featured image of the gallery load in the HTML from the server. So, I do this:

<div data-component="ImageGallery" data-r-prop-images="objectWithImages">
  <img src="featured-image.png" alt="Good web citizens also include alt text, right" />
</div>

This way, they get a useful page before the JS executes and it remains useful if the JavaScript borks for whatever of the myriad reasons JavaScript might bork.

This is actually working for me exactly as I want, but react-habitat is giving me console warning messages that say I can't do this.

I don't see anything in ReactDOM.render() that would make this problematic other than that it is destructive of the child nodes. But in this case, that's precisely the behavior I want.

PS: Thanks for the library. You saved my butt this weekend.

Passing Div content to a React component.

Hi, Is there a possibility of passing a div component to a react component along with the data.
At the moment we have implemented react-habitat within the entire project and we have a requirement where a div needs to be embedded to a react component.

Please let me know if there is a possibility on manipulating the DOM.

Components not loading asynchronously.

I probably have something configured incorrectly, but despite using registerAsync:

containerBuilder
    .registerAsync(System.import("./components/Button/Button"))
    .as("Button");

My bundle files are the same size regardless of whether I have one component on the page or all of them.

Here's my manifest file: https://github.com/ryanpcmcquen/react-habitat-poc/blob/master/source/Manifest.js

Any ideas what I might have configured incorrectly? Or is this just how registerAsync works in this scenario?

Editable prop names

Hi there,

First of all thanks for open-sourcing this nifty project!

I stumbled upon some runtime issues while testing the data-props/data-components on a large variant of websites. To make this library more isolated within existing platforms it would be nice to make the names of the data attributes editable in order to prevent conflicts with existing JS functionalities on other platforms.

E.g. adding a prefix data-myapp-component.

Please let me know if there is any interest for this πŸ‘

ERROR: RHW01 Cannot resolve component "XXX" for element. http://tinyurl.com/jxryd3s#rhw01 Error: Cannot resolve registration.

Hi,

Each of my react components throws the following error on page load BUT they are working fine, just seeing that the following errors are showing in the console (a bit annoying bc not sure what's up):

(index):115 ERROR: RHW01 Cannot resolve component "YourDetails" for element. http://tinyurl.com/jxryd3s#rhw01 Error: Cannot resolve registration.
    at Container.js:95
    at new Promise (<anonymous>)
    at Container.resolve (Container.js:91)
    at _loop (Bootstrapper.js:109)
    at _Mixin._apply (Bootstrapper.js:134)
    at _Mixin.update (Bootstrapper.js:232)
    at _Mixin.setContainer (Bootstrapper.js:180)
    at new _Mixin (createBootstrapper.js:94)
    at Object.createBootstrapper (createBootstrapper.js:157)
    at new OrderHistoryApp (index.js:14)
    at Object.505.../../ajax (index.js:81)
    at o (_prelude.js:1)
    at _prelude.js:1
    at Object.419../ajax (app.js:699)
    at o (_prelude.js:1)
    at r (_prelude.js:1)
    at _prelude.js:1 

Here's how I am registering components:

var ReactHabitat = require('react-habitat'),
	YourDetails = require('./components/YourDetails'),
	SummaryDetails = require('./components/SummaryDetails'),
	GiftMessage = require('./components/GiftMessage'),
	Cart = require('./components/ShoppingCart');

function CheckoutApp() {
	// Create a new react habitat bootstrapper
	this.domContainer = ReactHabitat.createBootstrapper({

		// default options (optional)
		defaultOptions: {
			tag: 'div',                 // (Optional)
			className: 'myHabitat',     // (Optional)
			replaceDisabled: false      // (Optional)
		},

		// Create a new container (Required)
		container: [
			// Register your top level component(s)
			{register: 'YourDetails', for: YourDetails},
			{register: 'SummaryDetails', for: SummaryDetails},	
			{register: 'GiftMessage', for: GiftMessage},
			{register: 'Cart', for: Cart}
		],

		// Should update lifecycle event (Optional)
		// return false to cancel update
		shouldUpdate: function(target, query) {
				return true;
		},

		// Will update lifecycle event (Optional)
		willUpdate: function(target, query) {
		},

		// Did update lifecycle event (Optional)
		didUpdate: function(target) {
		}

	});
}

Here are the package details:

...
"react": "^16.4.1",
"react-dom": "^16.4.1",
"react-habitat": "^1.0.1",
...

Wasn't seeing this issue with prior version of react-habitat was "react-habitat": "^0.5.0"

thanks,
Dennis

ERROR: RHW01 Cannot resolve component "XXX" for element. http://tinyurl.com/jxryd3s#rhw01 TypeError: Cannot read property 'insertBefore' of null

I think it's a bit related to #37

Each of our react components are being rendered correctly and work fine but it's quite annoying to see these errors 😒

CMS: Umbraco
HTML Example:

<div data-component="RSolutionSection"
                    data-prop-background-color="@pageColorGradient"
                    data-prop-align="center left"
                    data-prop-section-title="@title"
                    data-prop-cards="@cardsArray.ToString()"
                    data-prop-translations="@translations.ToString()"
                    data-prop-link-text="@link.Caption"
                    data-prop-link-url="@link.Url"
                    data-prop-src="@desktopImage"
                    data-prop-src-tablet="@tabletImage"
                    data-prop-src-mobile="@mobileImage"
                    data-prop-section-id="@sectionID">
...
</div>

ComponentRegistrations (Small insight in our registration code):

class ComponentContainer extends ReactHabitat.Bootstrapper {
  constructor() {
    super()
    const containerBuilder: ReactHabitat.IContainerBuilder = new ReactHabitat.ContainerBuilder()
    const store = configureStore()
    containerBuilder.factory = new ReduxDomFactory(store)

    containerBuilder
      .registerAsync(System.import('./components/Interfaces/SolutionSectionInterface'))
      .as('RSolutionSection')
      .withOptions({ className: 'section-component-wrapper' })

    this.setContainer(containerBuilder.build())
  }
}

const instance = new ComponentContainer()

declare global {
  // tslint:disable-next-line:interface-name
  interface Window {
    updateHabitat?: (Node?: any) => void
    Barba: any
  }
}

window.updateHabitat = instance.update.bind(instance)

// Barba should be global.
window.Barba = Barba
Barba.Dispatcher.on('newPageReady', (currentStatus, oldStatus, container) => {
  if (window.updateHabitat) {
    if (Object.keys(oldStatus).length > 0) {
      window.updateHabitat()
    }
  }
})

Barba.Dispatcher.on('transitionCompleted', () => {
  const event = document.createEvent('Event')
  event.initEvent('scroll', false, true)
  window.dispatchEvent(event)
})

const loadingElement = document.getElementsByClassName('loading')[0]
const loadingFromPage = document.getElementById('loading-from-page')
const loadingToPage = document.getElementById('loading-to-page')
const loadingLogo = document.getElementById('loading-logo')
const body = document.querySelector('body')
const html = document.querySelector('html')

// Initialize Barba.js
// Please note, the DOM should be ready
const HideShowTransition = Barba.BaseTransition.extend({
  start() {
    this.oldContainer.classList.add('is-transitioning', 'old-container')

    if (
      loadingFromPage !== null &&
      loadingToPage !== null &&
      loadingElement !== null &&
      loadingLogo !== null
    ) {
      loadingElement.classList.add('enter')
      loadingFromPage.classList.add('enter')
      loadingLogo.classList.add('enter')
    }

    this.newContainerLoading.then(this.finishing.bind(this))
  },

  finishing() {
    this.newContainer.classList.add('is-transitioning', 'new-container')
    setTimeout(() => {
      this.finish()
    }, 1000)
  },

  finish() {
    if (
      loadingFromPage !== null &&
      loadingToPage !== null &&
      loadingElement !== null &&
      loadingLogo !== null
    ) {
      this.newContainerLoading
        .then(window.scrollTo(0, 0))
        .then(loadingToPage.classList.add('enter'))
        .then(this.newContainer.classList.remove('new-container'))
        .then(
          setTimeout(() => {
            loadingToPage.classList.add('exit')
            loadingElement.classList.add('exit')
            loadingFromPage.classList.add('exit')
            loadingLogo.classList.add('exit')
          }, 500)
        )
        .then(
          setTimeout(() => {
            loadingToPage.classList.remove('exit', 'enter')
            loadingElement.classList.remove('exit', 'enter')
            loadingFromPage.classList.remove('exit', 'enter')
            loadingLogo.classList.remove('exit', 'enter')
            this.newContainer.classList.remove('is-transitioning')
          }, 1000)
        )
        .then(() => {
          const language = getLanguageFromUrl()

          if (html && language !== '') {
            html.setAttribute('lang', language)
          }
        })
    }

    this.done()
  },
})
Barba.Pjax.getTransition = () => {
  return HideShowTransition
}
Barba.Utils.xhrTimeout = 15000
Barba.Pjax.start()

// Un-hide body.
if (body) {
  body.className = ''
}

export default instance

Large component props are cut off

When passing a large JSON string as a "data-prop-" to a react habitat component, it will be cut off after about 100.000 characters.

Data this happened with: https://gist.github.com/nixolas1/5eec33756cdd1ed70b7a22950be55600
It was cut off at the first occurence of "brand":"Bergby"
Resulting in it not being read as JSON, but as a string, and breaking the application.

A workaround is storing the large JSON to a variable on window then passing that variable with "data-r-prop-" to the component.

Components not loading asynchronously in BrowserSync

Am starting a project, and built up my boilerplate with React-habitat.

I'm trying to use BrowserSync to hot reload while developing & have my components load Async but it does not seem to be working.

I see the chuck .js files. When I do a build and test my project, the Async works.

Not sure what's going on.

My Files

Error I get.

Logger.js:33 ERROR: RHW01 Cannot resolve component "HelloWorld" for element. http://tinyurl.com/jxryd3s#rhw01 Error: Loading chunk 0 failed.
    at HTMLScriptElement.onScriptComplete (bootstrap 0f60d233d44349295ea9:756)
    at Function.requireEnsure [as e] (bootstrap 0f60d233d44349295ea9:761)
    at Function.fn.e (bootstrap 0f60d233d44349295ea9:135)
    at Registration._operator (main.js:48)
    at Container.js:99
    at new Promise (<anonymous>)
    at Container.resolve (Container.js:91)
    at _loop (Bootstrapper.js:109)
    at MyApp._apply (Bootstrapper.js:134)
    at MyApp.update (Bootstrapper.js:232) <div data-component=​"HelloWorld" data-prop-name=​"blad">​</div>​
log @ Logger.js:33
error @ Logger.js:100
(anonymous) @ Bootstrapper.js:129
Promise.catch (async)
_loop @ Bootstrapper.js:128
_apply @ Bootstrapper.js:134
update @ Bootstrapper.js:232
setContainer @ Bootstrapper.js:180
MyApp @ main.js:57
_typeof @ main.js:64
__webpack_require__ @ bootstrap 0f60d233d44349295ea9:707
fn @ bootstrap 0f60d233d44349295ea9:112
Object.defineProperty.value @ ReactPropTypesSecret.js:12
__webpack_require__ @ bootstrap 0f60d233d44349295ea9:707
module.exports @ bootstrap 0f60d233d44349295ea9:805
(anonymous) @ main.js:809

A couple of question

Nice project!

I had a few queries I was hoping you answer, its looks like its a very new project so I was a bit apprehensive about using it for a production site.

I've been tasked with adding some javascript components to an existing Wordpress site. As its a standard multi page site it not a option to develop a React SAP. I could use Angular and create some directives which would work fine but seen as how the whole world is moving to towards React I thought I'd investigate the possibility of using React without having a root container component.

Looks like you've had the same issue and hence React-habitat is designed to fill this gap. So my questions would be:

  • I presume you are doing multiple ReactDOM.render calls? Have you noticed any negative effects of doing this, performance or otherwise?
  • Can you nest data components? e.g.
<div data-component="SomeReactComponent">
       <div data-component="SomeChildReactComponent"></div>
</div>
  • Have you use redux with this, any issues?
  • It would be good to see some live examples, or there any of jsfiddle or links to production sites.

thanks.

Any Ideas for a universal/isomorphic app approach

I'm looking forward of using react-habitat for integrating multiple react component into an cms base site.
It would be really cool, if this components can be rendered serverside and initialized/registred later on.
This would really help to address issues of page rendering speed as well as seo requirements.
Did you already figure out a way to integrate react-habitat with ReactDomServer?

CONT issue #14 Questions (it was closed too early)

Thanks for your answers.

Its important that your javascript is included at the end of the page (just before )
as it will only parse HTML rendered before it.
How does this look if it concerns a html snippet that is downloaded from the backend API?
In case of a SPA (Single Page App), it happens that you replace/add/remove html parts on a screen.
I like to parse a html part that will contain a root div and many nested html elements.
How should I parse this such that habitat will add the required components?

In pseude-code:

htmlSnippet = retrieveFromBackend();
parseWithHabitat(htmlSnippet); (??)

[Question] Is it possible to update props from outside after component is rendered

Is it possible to update props from outside after component is rendered?

Example:
I have non-react form with a single react component bound via habitat. There is a checkbox next to react component which dictates wether react component should be enabled or disabled. What would be the best approach to update props after react component was already bound and rendered.

Separate component bundles?

Have you a way to bundle and dynamically include the data-components as standalone? My steps for doing this with vanilla JS and require.js was first parsing the DOM for any data-components. This "loader script" was part of a core.js that got loaded on every page. The components were separate bundles that got asynced by a require() call. This saved page weight a lot.

I see with react-habitat you have to register all the components up front, but if the components could vary per page, I would only want to load that components code into the browser.

Register children as props

I thought it would be neat if we were able to register children of the component root:

<div data-component="SomeReactComponent">
    <div data-prop="foo"><p>Some markup rendered from the CMS that I'd rather not pass as data</p></div>
</div>

This could also be used as progressive enhancement, or a way inject SEO data.

CSS modules

This isn't an issue, but I was curious as to whether or not react-habitat was able to support something like css modules out of the box? i use react-css-modules extensively and wanted to know if it would mesh well with react-habitat

thanks

ReactHabitat.createBootstrapper is not a function

I'm new to React and feel I'm missing something fundamental here. This is my root 'App.js' code:

var ReactHabitat = require('react-habitat');
var SearchModule = require('./_modules/search/Search.js');

function MyApp() {

  this.domContainer = ReactHabitat.createBootstrapper({

    // Create a new container
    container: [

      // Register your top level component(s)
      {register: 'inline-search-container', for: SearchModule},
      {register: 'booking-search-container', for: SearchModule}
    ]
  });

}


exports.MyApp = new MyApp();

react-habitat is definitely installed and I'm using Browserify to manage the packages I need for the app.

I get 'ReactHabitat.createBootstrapper is not a function' in the console after firing up the server.

Any ideas?

Thanks

Import from React Component.

Hey i got every time I use <div data-component="footer"></div> an error:

TypeError: Cannot read property 'createElement' of undefined

links to:

      return r.default.createElement(
        'footer',
        { className: 'footer' },
        r.default.createElement(
          'div',
          { className: 'footer__logo-container' },
          r.default.createElement('img', { className: 'footer__logo' }),
          r.default.createElement(
            'span',
            { className: 'footer__copyright' },
            'Β© 2019 '
          )
        ),
        r.default.createElement(
          'nav',
          { className: 'footer__nav' },
          r.default.createElement(
            'ul',
            { className: 'footer__nav-list' },
            r.default.createElement(
              'li',
              { className: 'footer__nav-item' },
              'Kontakt'
            ),
            r.default.createElement(
              'li',
              { className: 'footer__nav-item' },
              'Impressum'
            ),
            r.default.createElement(
              'li',
              { className: 'footer__nav-item' },
              'Datenschutz'
            )
          )
        )
      );

The Component

import React from 'react';
import './footer.scss';

const Footer: React.FC = () => (
  <footer className="footer">
    <div className="footer__logo-container">
      <img className="footer__logo" />
      <span className="footer__copyright">Β© 2019</span>
    </div>
    <nav className="footer__nav">
      <ul className="footer__nav-list">
        <li className="footer__nav-item">Kontakt</li>
        <li className="footer__nav-item">Impressum</li>
        <li className="footer__nav-item">Datenschutz</li>
      </ul>
    </nav>
  </footer>
);

export default Footer;

How I create the Container

import * as ReactHabitat from 'react-habitat';

import footer from '../../frontend/src/components/footer/footer';


class Main extends ReactHabitat.Bootstrapper {
  constructor() {
    super();
    const containerBuilder: ReactHabitat.IContainerBuilder = new ReactHabitat.ContainerBuilder();

    containerBuilder.register(footer).as('footer');

    this.setContainer(containerBuilder.build());
  }
}

const instance = new Main();


declare global {
  interface Window {
    updateHabitat?: (Node?) => void;
  }
}
window.updateHabitat = instance.update.bind(instance);

export default instance;

React Version: 16.8.1
Habitat Version: 1.0.1
Webpack Version: 4.29.3

Please add support for React-Redux >7

I have a feeling the response I will get is PRs are welcome it would be neat if this library was actively supported. We are currently stuck with habitat as someone made that choice for us and in a large application moving away from it would grind the development to a halt for a long time.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.