Drive-by UX enhancing with ViolentMonkey (Part I of N)
Tags: userscripts, bikeshedding, javascript
Published: 2021-02-08 21:38:25

---
series: Drive-by UX enhancing with ViolentMonkey
---
## Intro

I downloaded [ViolentMonkey][1] somewhere in the past week and rediscovered a sense of wonder and joy I didn't think was there any more. It also runs in every major browser, so I can have the same magical mystery scripts follow me around.

Thoroughly customized GMail with it, added a bunch of useful things to Jira, the _usual_ fun.

But then I promptly decided to go and build a thing with it, which I find wonderful! Bask in its glory: 
<figure>
![Bask in its glory](https://dev-to-uploads.s3.amazonaws.com/i/xvspjob8gzyp1ead4qae.png)
<figcaption>Everything after the odd `[-]` thing is my UI extension.
</figcaption>
</figure>
There's a website called Hacker News Network, which you'd be forgiven if you mistook it for an actual hackerspace. It's more of a rough-around-the-edges version of [reddit][2] for startup founder types, with ocasional nerd chat around it. 

It's not bad, it's not great either. So I went and instantaneously improved it 10000% (_estimation based on an n=1 test_) by grafting my own micro-ranking on top of it. Awesome innit?

It works a lot like an extension -in fact, it runs *in the **same** space* as extensions do, due to ViolentMonkey's magical featureset-, except it is entirely private to me and allows me to vote and tag people, storing the results of doing so in a firebase db. 

Let me show you how it works, it's fun. 
## Internals
### The observables

It all hinges on an observable implementation I built specially for this, which while probably **too** minimal, I didn't find restrictive: 

```js
function observable(value) {
  let current = value;
  let listeners = [];
  const instance = function(newValue) {
  if(typeof(newValue) === 'undefined'){
    return current;
  }
  listeners.forEach((listener) => listener(newValue, current));
    current = newValue;
  };
  instance.subscribe = function(fn) {
    listeners.push(fn);
    return function unsubscribe() {
      listeners = listeners.filter((listener) => listener !== fn);
      }
  };
  return instance;
}

/*
* # Usage
* const COUNT = observable(0); 
* const unsubscribe = COUNT.subscribe(function(current){
*   if(current >= 5){
*     unsubscribe();
*   }
*   console.log(current);
* });
* for(let i=0; i < 10; i++){
*   STATE(STATE()+1);
* }
*/
```

### The API

As soon as the script _starts_ its lifetime, I added two subscribers to a `PAGE_STATE` observable I have that is basically empty at this point. See if you can figure out what they do: 

```js
  const PAGE_STATE = observable({});
  PAGE_STATE.subscribe(updateUI);
  PAGE_STATE.subscribe(updateFirebase);
```

Upon page load, I go ask for `firebase.firestore().collection('COLLECTION_NAME').doc('karma').get();`, which is a bit of a mouthful but also used just this once, and promptly put it on a `PAGE_STATE` observable I had declared earlier. 

We haven't really done that much yet but can confidently say this thing is already 

1. Reactive
2. Decoupled
3. Inextrincably tied to a random Google product

Not only that, but it also implements an *unidirectional data flow* 🌈🦄! Awesomest ~20 LOC I've written this far.

### The magic

Turns out most modern sites these days use a `Content-Security-Policy` thing to ensure you don't just sideload a bunch of stuff on them (damn you miscreants for ruining **my** fun) so you can't just plop [firebase][3] in there and expect it to work. 

Or... can you? Well actually, if you tell ViolentMonkey to run your script in extension space via this magic incantation: `// @inject-into content` you totally can. This brings any scripts you injected through `// @require` to a magical mystery location called `unsafeWindow`, which is ugly, but you can totally pretend it's not there because it's the default `global` now.

This allows us to magically sync whatever the UI does to Firebase! 
My `updateFirebase` function ended up looking a lot like this:

```js
function updateFirebase(current){
  const {karma, notes} = current;
  const doc = firebase.firestore().collection('COLLECTION_NAME').doc(karma);
  doc.set({ karma, notes }, { merge: true });
}
```

Had to make a minimal change to my initial page state thingamabob declaration, but with that we can be reasonably sure that we have an autosynced data model. Check this out: 

```js
  const INITIAL_STATE = {
    karma: {},
    notes: {}
  }
  const PAGE_STATE = observable(INITIAL_STATE);
```

And with this, any change the data layer does to either `karma` or `notes` is automagically saved!!1. 

Sure, we don't do any kind of error checking or validation, but I probably will down the line.

No frameworks in sight, easily-abstractable third party libraries... isn't this awesome?

## The gnarly bits

So it turns out that writing an article about extending UI without writing a single bit of UI is generally frowned upon. 

UI however also turns out to be one of the most well-trodden paths in the history of frontend development, so I might be forgiven for having taken a number of creative licenses I could never have applied to my React-based day job. 

What do I mean?

### Drawing the actual UI

By this point we're reasonably sure there is an `updateUI` function that will run whenever the data changes. Right. But... how should it work? 

A good first starting point (which, to be frank, I haven't yet abandoned since the whole thing is *just so pretty*) is as follows:

```js
// HELPERS
function sameish(a, b){
  // there sure is room for improvement here
  return JSON.stringify(a) !== JSON.stringify(b);
}

function $(sel, all = false) {
  return document['querySelector' + (all ? 'All' : '')](sel);
}

function str2HTML(str){
  const wrap = document.createElement('wrap');
  wrap.innerHTML = str;
  return wrap.firstElementChild;
}

// ACTUAL UI CODE
function trashUI(){
  [...$('.karma-trinket', true)].forEach(n => n.remove());
}

function drawUI({karma}){
  const handleUpdoot = (user) => {
    const state = PAGE_STATE();
    PAGE_STATE({
      ...state,
      karma: {
        ...state.karma,
        [user]: (state.karma[user]||0) + 1
      }
    });
  };
  const handleDowndoot = () => {
    const state = PAGE_STATE();
    PAGE_STATE({
      ...state,
      karma: {
        ...state.karma,
        [user]: (state.karma[user]||0) - 1
      }
    });  
  };
  const makeTrinket = (user, points) => {
    const node = str2HTML(`
      <div class="karma-trinket" style="display:inline-flex">
        <div>Own: (${points})</div>
          <div>
            <button aria-label="Upvote">▲</button>
            <button aria-label="Downvote">▼</button>
          </div>
        </div>
    `);
    node.querySelector('[aria-label="Upvote"]')
      .addEventListener('click', () => handleUpdoot(user));
    node.querySelector('[aria-label="Downvote"]')
      .addEventListener('click', () => handleDowndoot(user));
    };

    [...$('.hnuser', true)].forEach(
      function addTrinket(user){
        const points = karma[user.innerText] || 0;
        const parent = user.parentNode;
        parent.insertBefore(
          makeTrinket(user, points), 
          user.nextSibling
        );
      }
    );
}
// Observable subscriber
function updateUI(current, old){
  if(!sameish(current, old)){
    trashUI();
    drawUI(current);
  }
}
```
Whew! We may have sprouted a bunch of helpers, but they're all kind of self explanatory and we now have SOME SORT of UI! My own version of this is more baroque and bikesheddy, but this is a great approximation to my first draft.

Isn't it just the best? 

If you collect all the bits and pieces, you should have something along these lines: 

<figure>
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/8wcdn53fu029u6wm9gwh.jpg)

<figcaption>Beautiful huh?</figcaption>
</figure>

And you should be able to increment or decrement the counter for any given user by clicking on the upvote/downvote arrows. 

## Outro

By this point you should have either lost your sanity, your interest or a combination of both, so I'll leave the rest (and there's a lot left!) for future articles in the series; for starters, I want to be able to define the HTML and CSS in a more, shall we say... expressive way? The sheer modernness of it all! Coming up next 🦄

Please leave suggestions, insults or words of encouragement below. 
 

[1]: https://violentmonkey.github.io/
[2]: https://www.reddit.com
[3]: http://firebase.google.com/