A plugin is a widget that can be added to a marcel media. Basically it is just a web site served by marcel and displayed in an iframe.
create-marcel-plugin
is pacakge allowing to create a basic setup for a marcel plugin.
# with yarn
yarn create marcel-plugin marcel-plugin-name
# with npm > 6.0.3
npm init marcel-plugin marcel-plugin-name
# with npm > 5.1.0
npx create-marcel-plugin marcel-plugin-name
- Create a directory for the plugin
mkdir marcel-plugin-<plugin name> && cd marcel-plugin-<plugin name>
- Create a directory name
frontend
mkdir frontend && cd frontend
- Create a node module. Don't forget to give a name to the module, the default will be
frontend
.
# with npm
npm init
# with yarn
yarn init
- Add
marcel-plugin
as a dependency (npm i marcel-plugin
oryarn add marcel-plugin
)
# with npm
npm install marcel-plugin
# with yarn
yarn add marcel-plugin
- Create a file
index.html
The frontend
directory will be served by marcel as a standard web site.
Hence the html, js and other static file must be put in this directory.
The plugin will be displayed in an iframe with index.html
as src
.
A plugin example is available here marcel-plugin-text is an example of a very simple plugin.
The following script must be added in index.html
in order to the interact with marcel.
<script src="./nodes_modules/marcel-plugin/dist/index.js"></script>
Then, in a another script, a class that inherits Marcel.Plugin
must be created:
class MyPlugin extends Marcel.Plugin {
<...>
}
This class is the representaton of the plugin's runtime. Some methods might be implemented in order to declare what the plugin does.
class MyPlugin extends Marcel.Plugin {
render() {
const { props1, props2 } = this.props
}
}
This function is called every time marcel renders the plugin.
As its name implies, this function is responsible for rendering the plugin.
The object this.props
will contain every properties entered by the user in the backoffice.
class MyPlugin extends Marcel.Plugin {
propsDidChange(prevProps) {
const { prop1, prop2 } = this.props
if (prop1 !== prevProps.prop1) {
this.updateMyPluginData()
}
}
}
This function should be used for any sides effect dependending on the props. For example, making API calls in order to fetch some data.
This function will be called after the render()
function.
The current properties are accessible in this.props
(like in the render
method), and the previous properties are accessible in the prevProps
argument.
prevProps
might be used to avoid expensive API calls if the properties didn't change.
Finally, a plugin must be initialized:
Marcel.init(MyPlugin)
A plugin might be tested outside marcel by serving the frontend
directory with any http web server:
npm install --global serve
serve -s frontend
Then new props can be simulated by calling:
Marcel.changeProps({ props1: 'new value' })
Tips: This can either be done in the devtools or by adding it at the end of the plugin's script.
To receive the props and communicate with the marcel media, the standard HTML message API must be uesed.
A basic API built over the message API is provided by marcel-plugin
.
The main feature of marcel-plugin
is the Marcel.Plugin
base class.
By extending this class, the plugin is notified of props updates and might implement a basic state management.
class MyPlugin extends Marcel.Plugin {
constructor() {
// The super constructor must always be called
super({
// Optional default props and default state
defaultProps: { prop1: 'default value' },
defaultState: { state1: 'default state' },
})
// Some initialization can be performed here
// For example, storing dom elements
this.container = document.getElementById('container')
this.p = document.query('#container p')
}
render = () => {
// Any UI updates needed to keep the DOM synchronised with the props and state
// It is called a first time with default props and state and then each time props or state change
const { stylesvar, prop1 } = this.props
const { state1 } = this.state
this.container.style.color = stylesvar['primary-color']
this.p.innerText = `${state1} - ${prop1}`
}
pluginInitialized = () => {
// Every side effects that should only be done once
// but can't be done in the constructor because it depends on props goes here
// It is called just after the first render with loaded props
// WARNING: Most of the time propsChanged should be used
}
propsChanged = async (prevProps, prevState) => {
// Every side effects depending on props goes here
// It is called after render on each props or state change
// Actual changes can be detected by comparing this.props and this.state with prevProps and prevState
if (prevProps.prop1 !== this.props.prop1) {
this.setState({ state1: await fetchSomeData(prop1) })
}
}
}
The render method will be called to let the UI synchronize with updated props and state.
The state can be used to keep track of some values and have automatically rerender the plugin on changes.
To update the state, this.setState(newState)
must be called.
If newState
is an object, it will be merged into the current state. If it is a function, it will be called with the current state and must return the new state.
Since a plugin will be contained in a iframe that will take only a part of the full webpage, it is highly recommended to remove the border, padding and, must importantly, the scrollbar. This can be done with this tiny css snippet:
body {
margin: 0;
padding: 0;
overflow: hidden;
width: 100vw;
height: 100vh;
}
The space reserved to the plugin might change a lot, from a lot of space to a very tiny space, with ratio of 1:1, 4:1, 5:3... For this reason, a plugin must be as responsive as possible.