This article was generated by a human :)
The cover image provided by Gemini AI. UNLEASH BOOKKALLETT POWER!
The full code for this article's bookmarklet is available in the end.
Long time ago, in a galaxy far away, when web developers did not use node_modules to create web pages. Today we will discover how these forgotten techniques of using plain script tags and some js to create rich bookmarklets.
Yet, let us start with the basic, what is a bookmarklet? Essentially it is a bookmark, but with some javascript in place of the URL, in a format, which can be pasted in the browser's address bar and get executed, for example, try pasting the following code into the address bar and press enter:
javascript:alert('hello!');
Rather primitive, eh? However, this ability to execute javascript on the fly opens a huge field of possibilities. Do not feel like typing branch names manually when creating one on the work item? Have a bookmarklet to generate them. Want to check what does google analytics send? Have a bookmarklet to log its data in real time. Imagination is the limit.
Let us imagine a scenario, where we decided to code a bookmarklet with some fancy interface, so it creates a separate element and displays some data. To make it more interesting, let us use a css framework for the looks and a js library for components. For this demo, the bookmarklet will render a simple counter with a button that increments the displayed click count.
Resources we will be using:
- Bulma for css (simply because it does not use js);
- React for components;
- ReactDOM to render our React components.
All of these need to be loaded prior to any serious work, so let's create some functions to load them and await until we are ready to proceed. We can use CDNs to load everything we need. Note, that for this proof of concept we will not encapsulate our bookmarklet into an iframe, we will use a regular div to host our micro bookmarklet application instead.
javascript: (async () => {
await loadResources();
/** Here our main logic will reside */
async function loadResources() {
return Promise.all(
[
'https://unpkg.com/react@18.1.0/umd/react.production.min.js',
'https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js',
]
.map(loadScript)
.concat(['https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css'].map(loadStyleSheet)),
);
}
async function loadScript(src) {
return new Promise((res) => {
const script = document.createElement('script');
script.src = src;
script.onload = res;
document.body.appendChild(script);
});
}
async function loadStyleSheet(src) {
return new Promise((res) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = src;
link.onload = res;
document.head.appendChild(link);
});
}
})();
Take a note, how loading of scripts and stylesheets is combined into a single Promise, which allows us to postpone the execution until everything has been fetched from the web.
Once all the resources have been obtained, it is time to create an anchor component
...
const root = document.createElement('div');
document.body.appendChild(root);
...
What is interesting, is that once the scripts have loaded, React becomes immediately available with all its features, and you can create functional components, use hooks and do whatever you like with it. And though, there is no fancy jsx
available immediately, it still could be used if babel is loaded directly into the bookmarklet, but that would be too much for this demonstration.
For now let us add some basic React components to make up a counter
...
function Counter() {
const state = React.useState(0);
const count = state[0];
const setCount = state[1];
const updateCount = () => {
setCount((count) => count + 1);
};
return React.createElement(
'div',
{ className: 'counter' },
React.createElement(CounterText, { count: count }),
React.createElement(ButtonCounter, { clickHandler: updateCount }),
);
}
function CounterText(props) {
return React.createElement('div', null, `You clicked ${props.count} times!`);
}
function ButtonCounter(props) {
return React.createElement('button', { className: 'button is-primary', onClick: props.clickHandler }, `Click Me!`);
}
...
There we have it, a counter with 2 child elements, the only thing remaining now is to create a container with fixed positioning (just so it sticks) for it and render it using ReactDOM
. Though components have already been created using React
, ReactDOM
is needed to get them rendered on a web page.
...
const reactHost = React.createElement(
'div',
{
style: {
position: 'fixed',
top: '400px',
left: '600px',
backgroundColor: 'antiquewhite',
padding: '100px',
},
},
React.createElement(Counter),
);
ReactDOM.render(reactHost, root);
...
Putting it all together, we get a ridiculously over-engineered bookmarklet, that acts like a mini-application on its own.
javascript: (async () => {
await loadResources();
const root = document.createElement('div');
document.body.appendChild(root);
const reactHost = React.createElement(
'div',
{
style: {
position: 'fixed',
top: '400px',
left: '600px',
backgroundColor: 'antiquewhite',
padding: '100px',
},
},
React.createElement(Counter),
);
ReactDOM.render(reactHost, root);
function Counter() {
const state = React.useState(0);
const count = state[0];
const setCount = state[1];
const updateCount = () => {
setCount((count) => count + 1);
};
return React.createElement(
'div',
{ className: 'counter' },
React.createElement(CounterText, { count: count }),
React.createElement(ButtonCounter, { clickHandler: updateCount }),
);
}
function CounterText(props) {
return React.createElement('div', null, `You clicked ${props.count} times!`);
}
function ButtonCounter(props) {
return React.createElement('button', { className: 'button is-primary', onClick: props.clickHandler }, `Click Me!`);
}
async function loadResources() {
return Promise.all(
[
'https://unpkg.com/react@18.1.0/umd/react.production.min.js',
'https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js',
]
.map(loadScript)
.concat(['https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css'].map(loadStyleSheet)),
);
}
async function loadScript(src) {
return new Promise((res) => {
const script = document.createElement('script');
script.src = src;
script.onload = res;
document.body.appendChild(script);
});
}
async function loadStyleSheet(src) {
return new Promise((res) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = src;
link.onload = res;
document.head.appendChild(link);
});
}
})();
Though this demonstration may look rather primitive and awkward, as it lacks encapsulation for styles and downloaded javascript libraries, the approach can be used to make some amazing bookmarklets, which look like fully fledged widgets, check out this demo of a bookmarklet I made to view Google Analytics data being collected on a web page, and even pause it, which uses React, UI-Kit and iframe to isolate the bookmarklet app from the main page.
The demo for ga bookmarklet I made
Hope you learned something new :)