-->
Moddable adds incredible mod-support to business software. But there’s a catch… the application has to install our component.
The problem is - we want to show them what it looks like before they install it. So we built the “Moddable Emulator”.
As the name suggests, this makes products act like Moddable is installed.
This post is about how we found a way to make this happen: injecting Moddable into third-party platforms that haven’t yet installed it.
I believe in “show not tell”.
Specifically we want to show product-managers, etc. how powerful mod support could be in their product.
The demo I wanted looks like this:
The ideal demo is Moddable running in their platform and us showing the end-to-end process of a user building their own custom features.
In theory you can inject a library like Moddable into a SaaS product in a few ways.
When | How | Technique |
---|---|---|
At runtime | Browser extension | Use chrome.scripting API |
At runtime | Proxy server | Rewrite index file to inject a script into <head> |
At build time | Browser extension | Use chrome.webRequest to modify bundled files |
At build time | Proxy server | Replace the bundle with a proxied + modified bundle |
Basically it’s a combination of when (runtime or build time) and how (proxy server or browser extension).
The when is obvious: runtime is simpler than modifying the built files. Theoretically you would get a cleaner result modifying the built files, but it’s harder as they’re minified and bundled.
So runtime became “Plan A” and we would only look at modifying the built files if we hit a roadblock.
The how is an interesting trade off:
I decided to start with a browser-extension, as that’s what I am most comfortable with. I don’t need to distribute it widely (for now). I just need to show it on demo calls.
Browser extensions can use the chrome.scripting
API to interact with the page after it’s loaded. Or they can use the chrome.webRequest
API to intercept and modify the files themselves before it’s loaded.
As discussed above, to avoid deadling with bundled files we’re going to use the chrome.scripting
API. (Only looking at chrome.webRequest
if we hit a roadblock).
I started a new Plasmo extension. They make setting up a good developer experience with HMR easy & deserve a shout-out.
A minimal prototype to inject Moddable was actually quite simple to build. It’s maybe a couple of thousand lines. Here’s the core logic:
Find a target DOM node to insert into/alongside, etc.
Build the Moddable UI and insert it into the DOM node. The UI has a “Manage Mods” button & some links to open any built mods.
Watch this for changes and rebuild as appropriate.
The links in the inserted content use the Moddable API to get the list of mods, the links, etc. These links are self-autheticating. The end-user never needs to login or create a Moddable account.
Insert a stylesheet to match the platform’s styles. This is purely cosmetic but it’s important to make it look like this belongs. The whole point is to help platforms see what mod support might look like in their platform. And it’s pretty trivial to implement.
Great! This worked for our first platform. Time to scale it up.
The next step is to build a lot of these. This calls for a refactor.
It was important to us that every possible qualified team who requests a demo sees it running live in their platform. So we needed an abstraction that would make it easy to set up for a new platform.
Here’s what we ended up with:
class NewPlatformExtension extends InsertModdableUI {
protected DOM_SELECTOR = '[data-testid="query-sidebar"]'; // where should we insert stuff
protected API_KEY = "ABC123"; // for the Moddable API
protected PLATFORM_ID = "XYZ123"; // for the Moddable API
protected CustomStyles = `/** CSS goes here **/`;
constructor() {
super({
formatInsertedHTML: (html: string) => `<h2>Custom Features</h2>${html}`, // to customize the layout around the moddable UI
});
const watcher = new ContextWatcher({
extractContent: (element: Element) => { } // code goes here to emulate the Context sent by the platform
});
watcher.startWatching((val) => {
this.setPlatformContext(val);
}, "[data-testid='display']") // where to watch for changes
}
}
To unpack this abstraction:
InsertModdableUI
is a base class that handles the core logic of mock-inserting the Moddable UI.CustomStyles
and formatInsertedHTML
allow for visual customization of the Moddable UI to match the platform’s style.ContextWatcher
is used for emulating the Context which the platform supplies to the moddable SDK. This means that for example in a CRM system the custom mods know which contact is open, etc. (Whatever context makes sense for the given platform).