My experiences building a plugin of my app for Miro
— JavaScript, Software Development, Technology — 8 min read
After launching the Reciprocal.dev alpha a couple of months ago we started to think about how we’d better scale out our data-driven User Journey Mapping functionality and bring it to the tools that teams use everyday.
We decided Miro would be a good platform to run an experiment to validate this idea due to it being the tool that most companies we’ve interacted with adopted when the lockdowns in the UK happened.
It also worked in Miro’s favour that the inspiration for what would eventually become Reciprocal.dev, which started from me experimenting with building a Miro plugin to link acceptance criteria against UI elements in November 2020.
In order to conduct the experiment we agreed on the following scope:
- We’d build a plugin for Miro that users would install via the Miro Marketplace
- The plugin would allow users to tag widgets on the board with development data points that would then be used to visualise that information
- The plugin would allow users to save versions of a User Journey Map and load versions at a later point to compare them
This would put the functionality on-par with the web app that we built for the alpha, but without us having full control over every aspect of the UI.
The upside of building a Miro plugin would be that we’d no longer need to invest time in building our own editor which would speed up our ability to ship new data driven features.
The downside of building a Miro plugin was that where we didn’t have full control, we were exposing ourselves to the risks of Miro changing their platform and rules, as well as the more limited interaction options presented by the Miro API.
We felt that the upsides outweighed the downsides for the purpose of our experiment so I cracked on building an MVP of a Reciprocal.dev Miro plugin.
A quick intro to building a Miro plugin
A Miro plugin is essentially a set of HTML & JavaScript files rendered in iframes that use the Miro SDK (and maybe the REST API too) to interact with the Miro UI, the Miro board and the board’s widgets.
Before you can start building a Miro plugin you must first set up a ‘developer team’ and create an app in your Miro settings screen and then install that app into a board.
Once you’ve set up and installed your app you’ll need to provide the actual plugin application behind it. Miro provides a really great helper library to bootstrap a plugin called create-miro-app
which I’d highly recommend using to do this.
Within the plugin you’ll be able use the Miro SDK which exposes a set of methods for creating, reading, updating and deleting widgets on the current board as well as creating extension points to register your UI with.
In order for the user to launch your plugin you’ll need to register an extension point in the Miro UI. This is essentially a button that when pressed will trigger a callback that you can use to show your plugin in a ‘custom view’ which can be a modal, left sidebar or bottom panel.
The ‘custom view’ is an iframe that loads a HTML from within your plugin and you can have multiple HTML files that can be loaded if you want to offer different layouts for different extension points.
Inside of the ‘custom view’ HTML you’ll want to load JavaScript modules in so that the user can use your UI to interact with Miro via the SDK.
The Miro SDK
The Miro SDK offers a means of interacting with widgets as well as a way to listen to events happening within Miro such as when selections are made on the board.
Each widget in Miro has it’s own ID and you’ll need to use that ID to interact with the widget so having an event listener for what’s been selected on the board and storing the IDs in state is a good way of ensuring your plugin refreshes when the user selects something else without the need to read the selection everytime an action needs to happen.
If you have the ID of one or more widgets that you want to manipulate you can then use the following commands to do CRUD based operations on them.
miro.board.widgets.get()
— This takes a lodash filter object that will return all results that match the predicate, this does not support the function option that lodash supports so you can’t do anything complexmiro.board.widgets.create()
— This creates a new widget on the board using the object passed in to set the attributes, the minimum the object can be is the type of widget being created (i.e.{"type": "CARD"}
)miro.board.widgets.update()
— This takes a list of widgets (such as returned by thewidgets.get()
method) and updates them based on the new values that have been set via a mutable operation (i.e.widget.text = 'New Value'
)miro.board.widgets.deleteById()
— This takes the ID of a widget to delete
There are some limitations on what type of widget you can interact with. Currently you can use miro.board.widgets.get()
to read any type of widget but widgets.create()
and widgets.update()
only support the following types (at time of writing):
STICKER
— Sticky note widgetsTEXT
— Text widgetsSHAPE
— Shape widgets although this type is used as a base for a lot of other widget types so I can’t say if those more complex types are supportedCARD
— Card widgets are similar to shapes, I can’t say if the cards used by the Jira integration are supportedLINE
— Lines and connections between widgetsFRAME
— Limited to updates only
If like the Reciprocal.dev plugin you’re looking to support any widget the user might want to use you’ll have to find a means of providing interaction to the unsupported widget types via creating and updating supported widget types.
The Miro REST API
The Miro REST API offers a means of programmatically updating boards, users and widgets provided that the access token it’s being called with has the correct scopes.
In order to get an access token for the API you’ll first need to ensure that the user has authorised the app to act on their behalf via the OAuth2 flow and that you extract the access token during the last stage.
The REST API has a similar set of CRUD operations that can be called and it is also limited to the same type of widgets as the SDK, however the API is a lot more strict in the values it accepts when updating widgets (covered in the gotcha section later).
In order to work with widgets via the REST API you need to pass in the board ID as well as the widget ID. You can find the board ID in the URL or calling miro.board.info.get()
with the Miro SDK.
Communicating state across your plugin’s UI
Due to the way that your plugin is loaded into Miro via iframes there’s no global state available to use between your different ‘custom views’ so you need to find ways of sharing data and setting default states.
I found three ways that made this easier for me:
Pass default values in as query strings
In the Reciprocal.dev plugin I added a form to allow users to create a new User Journey entity that could be linked to a widget on the board but I had no means of ‘loading’ that User Journey’s data if the user wanted to edit the entity.
In order to ‘load’ the data I amended the code that launched the HTML for the form in a modal to include the entity’s data as query strings in the URL.
Use broadcastData to send and listen to specific updates
The Miro SDK provides a means for custom views to broadcast update events and other custom views to listen for these updates.
In the Reciprocal.dev plugin I added a call to miro.broadcastData()
when the User Journey create/edit form is submitted in its modal custom view and set up an event listener within the left sidebar custom view to listen for the DATA_BROADCASTED
event.
As I’m using a reducer within the React app that powers my plugin I simply sent my actions through broadcastData()
and used the DATA_BROADCASTED
event callback to match the action type against a set of supported types to pass to the reducer.
Serialise & Deserialise app state to localstorage
I used this approach to manage the UI state as I have a number of extention points that I need to play nicely with each other.
When my extension points are triggered I write a flag to local storage so that subsequent clicks on other extension points and can read that flag and act accordingly. This prevents them closing UI elements that are already opened that the extention point would open anyway.
Inconsistencies between API & SDK and gotchas
While working on the Reciprocal.dev plugin I struggled to work with the FRAME
widget type using the Miro SDK as while it could update the frame I was looking to interact with it could not create widgets within the frame.
I ended up using the REST API for this as it supported passing in a parentFrameId
attribute when creating a widget but when this was supplied the positioning attributes were there ignored.
This unforunately was only the start of the differences that I found so I’m going to list some of the major ones and the solutions I added to bypass them.
Creating a widget within a frame
As mentioned previously the SDK doesn’t offer a means of adding a widget to a frame and the REST API will do this but ignore the positioning. My solution to this was to create the widget under the frame and then update the widget’s position.
For the MVP this is fine enough but this could be improved by setting the widget to not be visible on creation and then setting it to be visible after the position update so it appears to be created in place.
Inconsistencies in widget style properties
Part of the functionality of the Reciprocal.dev plugin is being able to select a frame and save it’s (supported widget) content as a ‘version’ so it can be loaded into a frame later (the Miro version of switching version in the Reciprocal.dev web app).
In order to perfectly reproduce the saved frame’s content when it’s loaded the REST API needs to be provided with the style properties of the widget but the REST API is much stricter on what values are allowed compared to the Miro SDK.
An example of this strictness would be with the opacity value for a SHAPE
widget’s background colour. The SDK allows any number between 0
and 1
but the REST API only accepts 0
, 0.25
, 0.5
, 0.75
or 1
so I had to create a transformer to resolve the SDK value to a value accepted by the REST API.
There are also completely different values used between the SDK and the REST API, for example the SDK would hold the shapeType
style property for the SHAPE
widget type as a number but the REST API requires a string.
I ended up writing a set of functions to transform SDK styles into those compatible with the REST API. These map the SDK values to the API ones or where the API is stricter it places the SDK value into one of the set values.
Widget type strings
This one is a small one but the SDK uses uppercase widget type strings (e.g SHAPE
) but the REST API uses lowercase widget type strings (e.g. shape
).
Summary
Overall the experience of building a Miro plugin has been a good problem to solve and it’s been really great seeing Reciprocal.dev’s functionality ported to Miro as that was what started the journey just over a year ago and it’s come full circle.
If I had one wish in making the developer experience easier I think it would be to increase the support for the other types of widget available within the SDK/API as the lack of support puts developers in a situation of having to explain to their end-user that their tool has limited functionality but that limitation isn’t something in their control.
Based on the roadmap shared by Miro at their most recent conference it didn’t look like there was going to be much development in the area with them instead focusing on chat integration and enterprise functionality but maybe at some point the full set of widges will be supported.