The Cosmos UI plugin system consists of an intricate web of "slots" and "plugs" that weave together with minimal knowledge of each other.
Though it will take some time to get familiar with the subtleties of the Cosmos UI plugin architecture, its core principles are simple and so is creating your first “Hello World”.
Note The underlying
react-pluginlibrary isn't published yet due to time constraints but will be open sourced as a separate project in the future.
The ui field in cosmos.plugin.json points to a module like this:
import React from 'react';
import { createPlugin } from 'react-plugin';
const plugin = createPlugin({ name: 'magicPlugin' });
// We're plugging a React component into an existing slot called "coolSlot"
plugin.plug('coolSlot', () => {
return <div>This is magic.</div>;
});
plugin.register();Slots and plugs make up for the render composition but there's more to UI plugins. To function as standalone UI abstractions that can interact in meaningful ways, UI plugins can also have individual configuration, private states, public methods and event handlers.
These are the arguments supported when creating a plugin:
| Argument | Description |
|---|---|
name |
UI plugin identifier. |
defaultConfig |
Optional plain object config. Set via Cosmos config under ui.plugins.{name} and read privately via PluginContext.getConfig. |
initialState |
Optional plain object state. Accessed privately via PluginContext.getState and PluginContext.setState. |
methods |
Optional method handlers called by other plugins via PluginContext.getMethodsOf. |
Once created, the plugin API allows registering UI plugs, as well as onLoad and event handlers.
Plug a React component into a <Slot>:
plugin.plug('slotName', () => {
return <MyComponent />;
});Plugs get access to the PluginContext:
plugin.plug('slotName', ({ pluginContext }) => {
return <MyComponent state={pluginContext.getState()} />;
});Plugs can receive slot props, which allows slots to parameterize their plugs:
plugin.plug('slotName', ({ slotProps }) => {
return <MyComponent name={slotProps.name} />;
});Somewhere in another plugin...
<Slot name="slotName" slotProps={{ name: 'Sara' }} />Plugs can receive children from their slot. This allows plugs to act as decorators:
plugin.plug('slotName', ({ children }) => {
return <MyDecorator>{children}</MyDecorator>;
});Additionally, when multiple plugs are registered for the same slot they can compose each other. The first plug will be children for the second plug, the first plug wrapped in the second will be children for the third plug, and so on.
You can also choose not to render children in a plug, thereby ignoring the slot's children and replacing all previous plugs.
Plug a React component into an <ArraySlot>:
plugin.namedPlug('slotName', 'plugName', () => {
return <MyComponent />;
});Contrary to Plugin.plug where a plug decorates or replaces previous plugs, a named plug is appended to a list for the slot to render. Multiple named plugs are allowed for the same slot and all will be rendered independently.
Named plugs are rendered in the order they are registered, or based on the optional
plugOrderprop of theArraySlotcomponent.
Register a handler that gets called once when the Cosmos UI loads, or when the plugin is enabled. If the handler returns a callback it will be called when the plugin is disabled or uninstalled.
plugin.onLoad(pluginContext => {
// Add DOM event or fetch external data
return () => {
// Optional: Clean up and unsubscribe from stuff
};
});Register handlers for events of other plugins.
on('otherPlugin', {
eventName(pluginContext, arg1, arg2) {
// React to event from other plugin
},
});Register the plugin in the global store.
The plugin context API allows you to interact with private plugin data (config, state) as well as other plugins (emit events, call methods). All plugin handlers receive the plugin context as the first argument.
Read own (private) plugin config.
pluginContext.getConfig();Read own (private) plugin state.
pluginContext.getState();Change own (private) plugin state.
pluginContext.setState({ enabled: true });
// Or using a state updater callback
pluginContext.setState(prevState => ({
...prevState,
enabled: !prevState.enabled,
}));Get public methods of other plugins.
const otherPlugin = pluginContext.getMethodsOf('otherPlugin');
otherPlugin.doSomething('withThis');
// It's also possible for plugin methods to return something
const value = otherPlugin.getSomething();Emit event to listeners registered by other plugins via Plugin.on.
pluginContext.emit('magicEvent', 'arg1', 'arg2');Slots and plugs are reminiscent of the chicken or egg conundrum. In this case, however, the slot definitely came first. Without a root slot nothing gets rendered in the Cosmos UI.
Each plugin can define new slots by rendering Slot and ArraySlot components.
Render a slot that accepts a React component plug.
| Prop | Description |
|---|---|
name |
Unique slot name. |
children |
Optional children passed to plugs. |
slotProps |
Optional plain object passed to plugs. |
<Slot name="coolSlot" />Slots can pass children to plugs, as well as data through the slotProps prop:
<Slot name="coolSlot" slotProps={{ cool: true, level: 100 }}>
<p>This is cool.</p>
</Slot>Render a slot that accepts a list of React component plugs.
| Prop | Description |
|---|---|
name |
Unique slot name. |
slotProps |
Optional plain object passed to plugs. |
plugOrder |
Optional list of plug names to enforce a sort order. |
<ArraySlot name="coolSlot" />The plug order can be enforced using the plugOrder prop:
<ArraySlot name="coolSlot" plugOrder={['plug1', 'plug2', 'plug3']} />The Cosmos UI is built entirely using this plugin API. It literally starts like this:
const root = createRoot(container);
root.render(<Slot name="root" />);You can browse the complete list of built-in UI plugins at packages/react-cosmos-ui/src/plugins.
Here's a few examples of existing slots:
| Type | Slot name | Description | Plug examples |
|---|---|---|---|
ArraySlot |
rendererAction |
Renderer-related buttons. | Edit fixture source. Go full screen. Toggle responsive preview. |
ArraySlot |
navRow |
Left-hand nav panel widgets. | Fixture search. Fixture bookmarks. Fixture tree view. |
Array |
rendererPreview |
Fixture preview placeholder. | Iframe renderer for web, status message for React Native. |
Like everything else in Cosmos, the UI plugin system is built with TypeScript. All plugin APIs are type-safe and use generics where needed.
The plugin system is built around something called a "spec". The spec is a static interface that allows plugins to interact with each other safely without having to be part of the same codebase.
If you browse the built-in plugins you'll find a spec.ts file inside each plugin. This is an example:
export type RouterSpec = {
name: 'router';
state: {
urlParams: PlaygroundParams;
};
methods: {
getSelectedFixtureId(): null | FixtureId;
selectFixture(fixtureId: FixtureId): void;
unselectFixture(): void;
};
events: {
fixtureSelect(fixtureId: FixtureId): void;
fixtureReselect(fixtureId: FixtureId): void;
fixtureUnselect(): void;
};
};When building your UI plugin make sure it doesn't bundle react or react-plugin inside it. The Cosmos UI plugin system only works when plugins tap into the global React and ReactPlugin instances. The easiest way achive this is by using Webpack externals:
externals: {
'react': 'React',
'react-dom': 'ReactDom',
'react-plugin': 'ReactPlugin'
}See the Boolean input plugin webpack config for a complete example.
For a Vite equivalent for Webpack
externalssee vite-plugin-externals.In the future we might use import maps to remove the need for a bundler to author UI plugins. ESM support is tracked here.
All this might seem intimidating but I encourage you to try it out. Create a blank Cosmos plugin and start hacking. Add something you find useful. Make Cosmos your own.
Join us on Discord for feedback, questions and ideas.