Plugin Guide

Adventure: Keep living life like there's no tomorrow and you'll be right sooner than you think.


Reactium comes with the built-in concept of plugins. Plugins are React components that will be rendered automatically through-out your application when you place plugin zones.

Plugins are a helpful pattern for when you do not want to hard-code the composition of your component, often because you can't or shouldn't update the parent component manually, or you want to provide dynamic composition (not just data).

For example, I have an admin UI that specifies a generic layout with a navigation zone, a footer zone, a content zone, and a sidebar zone.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Normal component

import React from 'react';

export default class Template extends React.Component {
    render() {
        return (
            <main>
                <section className='navigation' />

                <section className='content'>{this.props.children}</section>

                <aside className='sidebar' />

                <section className='footer' />
            </main>
        );
    }
}

Great, now I have a template that I can use to create a page, and I can pass the children components for the content zone. But wait, what if I want to load different components into the navigation, sidebar, or footer zones.

Well, maybe I'll conditionally load different navigation, different sidebar content, etc. No problem, except anyone who has done this has seen a template component like this get out of hand.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// Component

import React from 'react';
import HomeNavigation from 'components/navs/HomeNavigation';
import ArticleNavigation from 'components/navs/ArticleNavigation';
import BlogNavigation from 'components/navs/BlogNavigation';
import HomeSidebar from 'components/sidebars/HomeSidebar';
import ArticleSidebar from 'components/sidebars/ArticleSidebar';
import BlogSidebar from 'components/sidebars/BlogSidebar';
import HomeFooter from 'components/footers/HomeFooter';
import ArticleFooter from 'components/footers/ArticleFooter';
import BlogFooter from 'components/footers/BlogFooter';

export default class Template extends React {
    renderNav() {
        switch (this.props.pageType) {
            case 'home': {
                return <HomeNavigation />;
            }

            case 'article': {
                return <ArticleNavigation />;
            }

            case 'blog': {
                return <BlogNavigation />;
            }
        }
    }

    renderSidebar() {
        switch (this.props.pageType) {
            case 'home': {
                return <HomeSidebar />;
            }

            case 'article': {
                return <ArticleSidebar />;
            }

            case 'blog': {
                return <BlogSidebar />;
            }
        }
    }

    renderFooter() {
        switch (this.props.pageType) {
            case 'home': {
                return <HomeFooter />;
            }

            case 'article': {
                return <ArticleFooter />;
            }

            case 'blog': {
                return <BlogFooter />;
            }
        }
    }

    render() {
        return (
            <main>
                <section className='navigation'>{this.renderNav()}</section>

                <section className='content'>{this.props.children}</section>

                <aside className='sidebar'>{this.renderSidebar()}</aside>

                <section className='footer'>{this.renderFooter()}</section>
            </main>
        );
    }
}
Gah! I don't want this import list to get any longer. Also, this is feeling pretty fragile and inflexible too.

There are patterns to "clean" this up. We can extract these zones into new components, create separate templates, but somewhere there is going to be a bunch of imperative code or I'm going to have to repeat myself.


Plugin Zones

Enter Reactium Plugins. This is where Reactium plugin zones can help make your composition more dynamic, and let you accomplish something like this in a more declarative way.

Let's start by defining plugin zones with the built-in <Plugins /> component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React from 'react';
import { Plugins } from 'reactium-core/components/Plugable';

export default class Template extends React {
    render() {
        return (
            <main>
                <section className='navigation'>
                    <Plugins zone='navigation' />
                </section>

                <section className='content'>
                    <Plugins zone='content-pre' />
                    {this.props.children}
                    <Plugins zone='content-post' />
                </section>

                <aside className='sidebar'>
                    <Plugins zone='sidebar' />
                </aside>

                <section className='footer'>
                    <Plugins zone='footer' />
                </section>
            </main>
        );
    }
};

Now that we've defined the zones for our layout, let's add our existing HomeNavigation component as a plugin to thenavigation zone.

Within our components/HomeNavigation directory, we will create a plugin.js file like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import HomeNavigation from './index';

export default {
    /**
     * Required - used as rendering key. Make this unique.
     * @type {String}
     */
    id: 'home-page-main-navigation',

    /**
     * By default plugins in zone are rendering in ascending order.
     * @type {Number}
     */
    order: 0,

    /**
     * One or more zones this component should render.
     * @type {String|Array}
     */
    zone: 'navigation',

    /**
     * Component to render. May also be a string, and
     * the component will be looked up in components directory.
     * @type {Component|String}
     */
    component: HomeNavigation,

    /**
     * (Optional) additional search subpaths to use to find the component,
     * if String provided for component property.
     * @type {[type]}
     *
     * e.g. If component is a string 'TextInput', uncommenting the line below would
     * look in components/common-ui/form/inputs and components/general to find the component 'TextInput'
     */
    // paths: ['common-ui/form/inputs', 'general']

    /**
     * Additional params: (optional)
     *
     * Any additional properties you provide below, will be provided as params to the component when rendered.
     *
     * e.g. Below will be provided to the HomeNavigation, <HomeNavigation pageType="home" />
     * These can also be used to help sort or filter plugins.
     * @type {Mixed}
     */
    pageType: 'home',
};

Advanced Plugin Properties

When you define your Plugins zone, you can also optionally provide a mapper callback which will be called for each plugin in the zone, a sort callback to determine rendering order, and a filter callback to disqualify plugins from a zone.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React from 'react';
import { Plugins } from 'reactium-core/components/Plugable';

export default class Template extends React {
    render() {
        return (
            <main>
                <section className='navigation'>
                    <Plugins
                        zone='navigation'
                        mapper={plugin => {
                            return {
                                ...plugin,
                                additionalProp: 'value',
                            };
                        }}
                        sort={(pluginA, pluginB) => {
                            return pluginB.order - pluginA.order;
                        }}
                        filter={plugin => {
                            return plugin.pageType === this.props.pageType;
                        }}
                    />
                </section>
            </main>
        );
    }
}

Template.defaultProps = {
    pageType: 'home',
};

Now that we have a filter on the plugin zone, plugins will only appear when those conditions are met.


Redux Driven Plugins

If you wish to use application state to determine what plugins are loaded in a zone, you can dispatch core actions to dynamically add / remove / update plugins in a zone.

deps().actions.Plugable.addPlugin(plugin);
Add a dynamic Redux plugin:
deps().actions.Plugable.updatePlugin(plugin);
Update a Redux plugin (must have same id):
deps().actions.Plugable.removePlugin(pluginId);
Remove a redux plugin by id:
// action.js

import deps from 'dependencies';

export default {
    // on loading thunk, dynamically add this
    // components/navs/SecondaryNav to the navigation zone, having it appear only on the home pageType
    // (see custom filter above)
    load: () => dispatch => {
        dispatch(deps().actions.Plugable.addPlugin({
            id: 'my-dynamic-plugin',
            order: -1000,
            component: 'SecondaryNav',
            paths: ['navs'],
            zone: ['navigation'],
            pageType: 'home',
        }))
    };
};

Imagine the possibilities here, you could load the payload above from a REST service. Imagine the navs were loaded by calling an api endpoint /api/navs:

// actions.js

import deps from 'dependencies';
import api from 'appdir/api';

export default {
    load: () => dispatch => {
        return api.get('/api/navs')
            .then(({data: plugins}) => {
                plugins.forEach(plugin => dispatch(deps().actions.Plugable.addPlugin(plugin)))
            })
            .catch(error => {
                const errorId = 'error-'+new Date();
                dispatch(deps().actions.Plugable.addPlugin({
                    id: errorId,
                    component: 'ErrorMessage',
                    zone: 'errors',
                    error,
                }))

                // clear error after 2 seconds
                setTimeout(() => {
                    dispatch(deps().actions.Plugable.removePlugin(errorId));
                }, 2000)
            })
    };
};

Live Dispatch Plugin Example

The below example has a DropZone, which will have a plugin zone, and a DropControl that when clicked with add a new Widget to the plugin zone dynamically, using a Redux dispatched plugin.

Clicking one of the widgets will dispatch it's own removal.

The DropZone Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { Component } from 'react';
import { useStore } from 'reactium-core/easy-connect';
import { Plugins } from 'reactium-core/components/Plugable';

export default class DropZone extends Component {
    render() {
        return (
            <div className={'drop-zone'}>
                <h6>Drop Zone</h6>
                <Plugins zone={'drop-zone'} />
            </div>
        );
    }
}

The DropControl Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React, { Component, useState } from 'react';
import { useStore } from 'reactium-core/easy-connect';
import deps from 'dependencies';
import Widget from './Widget';

const DropControl = () => {
    const { dispatch } = useStore();
    const [count, setCount] = useState(0);
    const onAdd = () => {
        dispatch(deps().actions.Plugable.addPlugin({
            id: `widget-${count}`,
            component: Widget,
            zone: 'drop-zone',
            order: count,
            // count goes to props on Widget
            count,
        }))
        setCount(count + 1);
    };

    return (
        <div className={'drop-zone-control'}>
            <button
                className={'btn-primary mt-20'}
                onClick={onAdd}>
                Add Widget {count}
            </button>
        </div>
    );
};

export default DropControl;

The Widget Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react';
import { useStore } from 'reactium-core/easy-connect';
import deps from 'dependencies';

const Widget = ({count = 0, id}) => {
    const { dispatch } = useStore();

    const onRemove = id => () => dispatch(deps().actions.Plugable.removePlugin(id));
    return <div className={'example-widget'} onClick={onRemove(id)}>
        W{count}
    </div>
};

export default Widget;
Click button to add a dynamic Redux plugin
Click one of the widgets to remove it
Drop Zone

Command Line Help

After you've defined the zones for your layout, you can use the CLI to scan those zones using the command:

arcli zones scan
(Optional) This isn't strictly necessary, but it will help you use the CLI to create plugin components later.

Alternatively, you can manually register your zones with the cli (helpful for dynamic zones, i.e. zone property is provided by variable).

arcli zones add -i "navigation" -d "Navigation zone in template"
arcli zones add -i "content-pre" -d "Before Content in template"
arcli zones add -i "content-pre" -d "After Content in template"
arcli zones add -i "sidebar" -d "Sidebar zone in template"
arcli zones add -i "content-pre" -d "Footer zone in template"
(Optional) Again, this just helps speed up your workflow with the CLI when creating new plugins.

For existing components, you can add the plugin.js file to register it as a plugin, by using:

arcli plugins