Router plugin

The Merkur router plugin is integration of universal-router to Merkur ecosystem and extend @merkur/plugin-component for activation only part of widget functionality (controller) for defined path. The plugin adds router property to your widget with a link, redirect, getCurrentRoute and getCurrentContext(context from universal-router) methods.

Installation

At first we must install @merkur/plugin-router as dependency.

npm i @merkur/plugin-router --save

Then we add import of routerPlugin and register it to $plugins property of the widget.

// ./src/widget.js
import { routerPlugin } from '@merkur/plugin-router';


export default defineWidget({
  name: pkg.name,
  version: pkg.version,
  viewFactory: createViewFactory((widget) => ({
    View,
    slotFactories: [
      createSlotFactory((widget) => ({
        name: 'headline',
        View: HeadlineSlot,
      })),
    ],
  })),
  $plugins: [componentPlugin, eventEmitterPlugin, routerPlugin, errorPlugin],
  assets: [
    {
      name: 'widget.js',
      type: 'script',
    },
    {
      name: 'widget.css',
      type: 'stylesheet',
    },
  ],
  onClick(widget) {
    widget.setState({ counter: widget.state.counter + 1 });
  },
  onReset(widget) {
    widget.setState({ counter: 0 });
  },
  load(widget) {
    // We don't want to set environment into app state
    const { environment, ...restProps } = widget.props;

    return {
      counter: 0,
      ...restProps,
    };
  },
});

We have a router.{link|redirect|getCurrentRoute|getCurrentContext} methods available on the widget now.

After that we must initialize universal router with own routes and options in setup phase of creation widget where structure for routes and options are defined from universal-router. The options are extended by Merkur with optional settings protocol and host for generating absolute url address from link method. Returns type from route.action method is defined by Merkur router plugin. It is a object with PageView as main rendered component for defined path and controller life cycle methods (init, load, activate, deactivate, destroy) which extend load, mount, unmount methods from @merkur/plugin-component and other controller custom methods with logic for defined path. The mount method use under the hood widget.viewFactory method to resolving component for render. So we must set View in createViewFactory as route PageView. If you don’t have slots you can set slotFactories as empty array or set as route slots.

// ./src/widget.js
import { createRouter } from '@merkur/plugin-router';

export default defineWidget({
  name: pkg.name,
  version: pkg.version,
  viewFactory: createViewFactory((widget) => ({
    View: widget.router.getCurrentRoute().PageView,
    slotFactories: [],
  })),
  $plugins: [componentPlugin, eventEmitterPlugin, routerPlugin, errorPlugin],
  setup(widget) { 
    const routes = [
      {
        name: 'home',
        path: '/',
        action: ({ params, widget, route }) => {
          return {
            PageView: View,
            load() {
              return {
                counter: 5,
              };
            },
          };
        },
      },
      {
        name: 'detail',
        path: '/detail',
        action: ({ params, widget, route }) => {
          return {
            PageView: View,
            load() {
              return {
                counter: 0,
              };
            },
          };
        },
      },
    ];


    const options = {
      baseUrl: ``,
      // other options from https://github.com/kriasoft/universal-router/blob/main/docs/api.md
      // merkur specific options for generating absolute url from link method
      protocol: 'https',
      host: 'www.example.com',
    };

    createRouter(widget, routes, options);

    return widget;
  },
  assets: [
    {
      name: 'widget.js',
      type: 'script',
    },
    {
      name: 'widget.css',
      type: 'stylesheet',
    },
  ],
  onClick(widget) {
    widget.setState({ counter: widget.state.counter + 1 });
  },
  onReset(widget) {
    widget.setState({ counter: 0 });
  },
  load(widget) {
    const { environment, ...restProps } = widget.props;

    return {
      counter: 0,
      ...restProps,
    };
  },
});

Merkur resolve current route from pathname in widget.props. So we must set it in ./server/routes/widget/widgetAPI.js. Logic for defined pathname is on your use case. For example you can read it from req.query.pathname. The req.query.* or req.body.* are inputs to your API endpoint widget. You must validate it before you use in widget.props.

// ./server/routes/widget/widgetAPI.js
function getStringQueryParams(req) {
	return Object.entries(req.query).reduce((params, [key, value]) => {
		params[key] = Array.isArray(value) ? value.pop() : value;

		return params;
	}, {});
}

router.get(
  '/widget',
  asyncMiddleware(async (req, res) => {
    const { name, pathname } = getStringQueryParams(req);

    const merkurModule = requireUncached(`${buildFolder}/widget.cjs`);
    const widget = await merkurModule.createWidget({
      props: {
        name,
        environment: widgetEnvironment,
        pathname,
      },
    });

    const { html, slot = {} } = await widget.mount();

After that you must update merkur.config.mjs file to send pathname from playground page to widget API through playground.widgetParams method and of course change playground.path for extending playground page to works for more paths than default ‘/’ path.

// ./merkur.config.mjs

/**
 * @type import('@merkur/cli').defineConfig
 */
export default function () {
	return {
    //...
		},
		playground: {
			widgetParams: (req) => {
				return new URLSearchParams({ ...req.query, pathname: req.path });
			},
			path: new RegExp('(\/$|\/some-page\/.*)', 'g'),
		}
	};
}

Now you have install router plugin to your widget.

Methods

getCurrentRoute

Returned object from route.action method for current route.

getCurrentContext

Returned context(first argument) from route.action method for current route.

Returns url for route name with filling route pattern with data. If you define protocol and host settings in createRouter options then the url address generated from link method will be absolute.

redirect

Emit RouterEvents.Redirect event through @merkur/plugin-event-emitter for which must listen skeleton app.

Controller life cycle methods

init

The init method is calling as constructor for you controller. It is for creating some other instances which controller use in other methods.

load

The load method is mandatory and returns current state of the widget. The load method is called before mounting the widget and after changing props. The widget state is merge of object from widget.load method and controller.load method.

activate

The activate method is called after mounting the widget and only in browser environment.

deactivate

The deactivate method is opposite method for activate method and only call in browser environment.

destroy

The destroy method is opposite method for init method.