Building Bubble.io Elements using WebComponent
Table of Contents
I found this blogpost sitting in an export file from Ghost, which was what I used to publish those blogs back in the day. The content is still there, but all the images are lost. I’ll replace those images with some descriptions because I don’t expect anybody to follow what I did then anymore, now that Bubble.io is practically dead.
CitizenDev had released about 20 plugins on to Bubble.io Marketplace. Bubble give us a “working” SDK to develop custom elements, actions, in order to make up for the lack thereof natively in Bubble. However, a “working” SDK is what it is, and working with this SDK is a horrible development experience by 2023 standard.
- The editor is terrible. You can’t even search/scroll normally.
- The
previewsection of the element is a separated, isolatediframe. Which means I have to duplicate all the code inside this section to provide a truly WYSIWYG view - jQuery only. (No reactive goodness)
Don’t want to read this whole write up and just want to jump straight to the good action? Check out the POC Github Repo: https://github.com/citizendev-io/preact-bubble-plugin-poc
Approach
I have been thinking long about integrating some new JS framework into Bubble’s plugin. The main problems are:
- Most new JS frameworks now require you to have a build step. You can still (probably) write React using their olden, non-JSX syntax, but I doubt it would be any better than wrestling with jQuery.
- However Babel allows building JSX code directly on Browser, using their standalone package. This may solves the problem of running React code on Browser directly, but this is going to incur a lot of overhead.
The best way forward with developing (large) component would be ditching the whole Bubble’s plugin editor setup. We need to just load a prebuilt, minified element code into the Bubble’s plugin editor. This is where WebComponent going to shine, because you can use React, Vue to build a totally isolated component that can be loaded with zero dependency on Bubble.
Ideas
Here are several ideas, which we will visit one-by-one when we test out the process
- Use Preact as an alternative for React. Preact support building WebComponent natively with their thin WebComponent wrapper
- Use Vite dev server to serve the JS, CSS bundle on localhost.
- In Bubble’s element editor, load the JS & CSS bundle from localhost and Develop
- When publishing for livemode, upload the JS & CSS to Bubble’s plugin file, then point the loader to load from those source
The setup
The goal is to setup a system that allow efficient development while still able to run actual system code.
This is an interplay between two system: Bubble.io and the dev server on our machine. Let’s setup.
Vite + Preact
First we start with a Vite + Preact project, which is already covered by the Preact documents. Once the setup is done we get a clean project for writing Preact code.
It is probably showing a project layout, scaffolded by Vite + Preact
One important thing that we need to do is setup proper TypeScript support (it’s 2023). Follow this section of the document and we are golden.
Old caption: “Don’t you just love static type checking and their typehint.”
It is probably me showing an autocomplete dropdown from VSCode or something similar, where all the object properties are showing. After all I’m a sucker for typed language
This codebase give us also a functional Enzyme + Jest test suite, but for the sake of brevity in this guide we will skip using it.
Let’s throw in a simple counter:
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
interface BubbleProperties {
name?: string;
}
interface CounterProps {
properties?: BubbleProperties;
}
export default function Counter(props: CounterProps): VNode {
const [count, setCount] = useState(0);
return (
<div>
<h1>Hi {props.properties?.name ?? "anonymous"}.</h1>
<div>
This counter is written fully in Preact. This element can also load
properties from Bubble.io
<br />
The current count is: {count}
</div>
<button onClick={() => setCount(count + 10)}>Plus 10</button>
<button onClick={() => setCount(count - 1)}>Minus</button>
</div>
);
}
Good olde Counter. With a Properties object that we will hopefully get from Bubble.
Next step we make this codebase emit a library. This means we need to:
- Reconfigure Vite to build in library mode. Vite has a section in their document for this.
- We configure Vite to build from a
src/index.tsxentrypoint. This will be the core component code. Then we create thatsrc/index.tsxfile. - We need to remove the
"type": "module"directive inpackage.json. This will allow us to emit anindex.umd.jsfile which can be loaded safely on Browser. Read more about the UMD, ESM, CJS shenanigans here.
Vite devserver won’t emit a compiled version of the src/index.tsx file. Vite’s code revolve around the index.html file, with a hot reloaded connection to the JS/TS files in question. We instead ditch the Vite devserver, and run a modified script.
{
"scripts": {
"dev": "concurrently \"vite build --watch\" \"serve dist --cors\"",
},
"devDependencies": {
"serve": "^14.0.0",
"concurrently": "^8.0.0"
}
}
In this script, we run the vite build command, with the --watch option so it will automatically rebuild the JS bundle. Then we run serve of the whole JS bundle.
At the end of this step, we have this working URL localhost:3000/index.umd.js
Old caption: “A beautiful executable soup.”
I’m not particularly sure what I would have shown here, but I’m thinking it me opening localhost:3000/index.umd.js in the browser or curl-ing it, to show the JS file to be loaded
Loading code
Now that we have sort of executable JS thing going, we can proceed with creating a new Bubble plugin.
Old caption: “We start with an aptly named Counter element.”
This probably showed a Bubble element entry, in their plugin editor.
First thing we need to do is to import the code. I added a header script reference:
Old caption: “Load the script”
This probably showed me adding a <script> tag in the header section
Load the script React component
Spin up a test Bubble app, put the test element in and go:
Old caption: “Uhhhh”
This was probably me showing an error, where the element doesn’t render
As it turns out, Preact still run its element in its own Virtual DOM. For the uninformed (me included), this mean we need to run its element inside a special environment, like its
preact-habitat. But we have a better trick up our sleeve: web-component
Preact give us a nice wrapper called
preact-custom-element that gives us a thinly wrapped Preact element that can be added directly as HTML tag onto the website. Configuration of this element is really simple. We only need to add a line of code to register.
import register from "preact-custom-element";
export default function Counter(props: CounterProps): VNode {
// ...something
}
register(Counter, "custom-counter", ["properties"]);
Now we can load the element like this:
This was probably me showing the HTML markup where I load a custom element. It probably just had the line <custom-counter/> in it
Note here we are attaching the properties sent from Bubble to the element. The Preact Web Component wrapper will ensure the element is re-rendered when we update the properties attribute.
The Bubble element receive a name field:
It probably showed a name field in the Bubble editor for the element
Pass in the name field (I choose Tyler Durden), then open the preview of the test app.
It probably showed me typing “Tyler Durden” into the name field
Old caption: “Adding the Element”
This is probably me adding that element in the Bubble WYSIWYG area
Old caption: “In preview mode”
This definitely showed the preview page with a working Preact component
Too much details?
Yeah I know this is too much word. Consider this as an excited rant. If you just want to test this approach out, check out this Github repo: https://github.com/citizendev-io/preact-bubble-plugin-poc
What’s next?
- WYSIWYG. We need to be able to load the element inside
previewas well. - Setup a project template.
- Really build something out of this.