Event based state control example: Create a TipTap module with Vanilla JS
Tags: javascript, vdom, programming, performance
Published: 2023-02-26 12:22:31

TipTap is a beautifuly and flexible inline WYSIWYG-Editor based on [ProseMirror](https://prosemirror.net/). It allows you to make parts of your site editable, but also provides an interactive user interface. There are lot´s of examples how to set up TipTap [here](https://tiptap.dev/examples/default), source code is shown for React, Vue and Svelte. It is mentioned on the page, that you can set up TipTap using Vanilla JS too, but there is no working code example.

>**This example may also be helpful if you want to use the [TipTap WYSIWYG Editor](https://tiptap.dev/examples/default) using Vanilla JS**

This is, how the interface of TipTap may look like (*it is completely customizable depending on your needs*):
 

![TipTap Interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bnnzx9qbhshxrvhke6rq.PNG)
There is a number of "state"-buttons to enable text formatting. ***The nice thing about TipTap is, that the state changes are bidirectional***. You may set a portion of the text to bold, but the bold button will also show up if you select a portion of the text, which was already set to bold. So the state of the buttons always gives you a feedback about the state of the currently selected text.

>**Updating the buttons status is a perfect match to show, how to set up an event driven state logic in vanilla JS (see code below)**

### TipTap in REACT

TipTap delivers quite a lot of useful tools, but the editor definition for React is quite lengthy. Just to set up the buttons, the set up needs about 235 lines of hand crafted JSX. Maybe there are better ways, but this is how it is presented:
```HTML
const MenuBar = ({ editor }) => {
  if (!editor) {
    return null
  }

  return (
    <>
      <button
        onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
        className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
      >
        h1
      </button>
      <button
        onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
        className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''}
      >
        h2
      </button>
... (235 lines)
```
Every button is manually set up including the feedback-functions.

### TipTap in Vue

The setUp in vue is more compact, but still pretty handcrafted and redundant. It needs about 186 lines of code:

```Javascript
<template>
  <div v-if="editor">
    <button @click="editor.chain().focus().toggleBold().run()" :disabled="!editor.can().chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
      bold
    </button>
    <button @click="editor.chain().focus().toggleItalic().run()" :disabled="!editor.can().chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }">
      italic
    </button>
... (186 lines)
```

### TipTap in Svelte

Svelte comes with a more classical approach using template literals instead of JSX. With 171 lines of code this comes out similar to Vue.
```Javascript
{#if editor}
  <div>
    <div>
      <button
        on:click={() => console.log && editor.chain().focus().toggleBold().run()}
        disabled={!editor.can().chain().focus().toggleBold().run()}
        class={editor.isActive("bold") ? "is-active" : ""}
      >
        bold
      </button>
      <button
        on:click={() => editor.chain().focus().toggleItalic().run()}
        disabled={!editor.can().chain().focus().toggleItalic().run()}
        class={editor.isActive("italic") ? "is-active" : ""}
      >
        italic
      </button>
      <button
        on:click={() => editor.chain().focus().toggleStrike().run()}
        disabled={!editor.can().chain().focus().toggleStrike().run()}
        class={editor.isActive("strike") ? "is-active" : ""}
      >
        strike
      </button>
... (171 lines)
```
This is at heart simple HTML contained in a template literal, just creating every button manually. Welcome back to the 80´th...

### TipTap in Vanilla JS ([using DML](https://github.com/efpage/DML))

The examples on [TipTap](https://tiptap.dev/examples/default) will not run out of the box with vanilla JS, as the script path "@tiptap" is unknown to JS: 
```Javascript
import { Editor } from "@tiptap/core";
```
Instead you can load all files from a CDN like shown below, there is even no need to install TipTap on your machine
```HTML
  <script src="https://cdn.jsdelivr.net/gh/efpage/DML/lib/DML.js"></script>
  <script type="module">
    import { Editor } from 'https://esm.sh/@tiptap/core'
    import StarterKit from 'https://esm.sh/@tiptap/starter-kit'
```
Now we can set up the editor buttons using an array "actions[]". This contains all necessary information to build the button menue:
```Javascript
    const actions = [
      { name: "bold", command: 'toggleBold', disable: true },
      { name: "italic", command: 'toggleItalic', disable: true },
      { name: "strike", command: 'toggleStrike', disable: true },
      { name: "code", command: 'toggleCode', disable: true },
      { name: "clear marks", command: 'unsetAllMarks' },
      { name: "clear nodes", command: 'setParagraph' },
      { name: "paragraph", command: 'toggle' },
      ... (just 21 array elements, one for each button)
      ]
```
This delivers all necessary information and will be used in a simple loop. The `button()`-function is a DML-function that creates a button on the fly (in fact: most HTML-elements can be created by DML-functions with the same name). 

Here is the full setup loop:
```Javascript
    for (let i in actions) {
      let a = actions[i]  // get the command definition
      a.button = button(a.name)  // Store the button reference
      a.button.onclick = () => editor.chain().focus()[a.command]().run()
    }
```
"actions[]" is the definition array, "a" a single element of this array containing the properties *'name'* and *'command'* (+ some more to control the set up process). During set up, this array is extended by a new property called actions[].button.This property stores the DOM reference of the related button for later access. 
 
### Now comes the interesting part: the event funtion
The function "**checkActive()**" is used to control the state of a button referring to the state of the current selection in the TipTap-editor. This function was applied to the editor during creation:
```Javascript
    let editor = new Editor({
      element: document.querySelector('.element'),
      ...
      onTransaction({ editor }) {
        checkActive() }
    })
```  
The function "**checkActive()**" is called after every transaction of the editor (usually after a button press). This is very similar to a state change in React, but does not include a complete DOM diffing. It just checks the state of 21 buttons and it knows, which properties might have been changed. So, the operations can be performed very focused. 

Here ist the full Event loop (a bit cleaned for better readability):
```Javascript
    function checkActive() {
      for (let i in actions) {
        let a = actions[i]  // get the button reference
        if (a.button.classList.contains('is-active')) {
          if (!editor.isActive(a.name)) a.button.classList.remove('is-active')
        } else
          if (editor.isActive(a.name)) a.button.classList.add('is-active')
      }
    }
```  
The function is a bit "verbose", as CSS-class manipulation is not very convenient in JS. This loop checks for the state of the current selection ' !editor.isActive(a.name)', where a.name is "bold" or "italic" and sets or deletes the appropriate class name of the button. 

This is no rocket science, the loop just checks each button state once after any transaction. Here, the check is driven by the "onTransaction" event of the editor. In React, the state would be checked by the common state logic. 

You can find a runnig application with the full code [here](https://flems.io/#0=N4IgzgpgNhDGAuEAmIBcIB0ALeBbKIANCLAPYB2YpMaJ1UAhgA6QrEBmAljGGgNqhyDXBFrY8BYmXKIZtADwBCACIB5AMIAVAJoAFAKIACHPgB8AHXLyTUQ43IBzALzmQSCK4vlL1iAyRehobyIvAMhrBYDABOkPAuIACu8OwAtAAcnpZB8vCc8DCmeQUQ8gD0xYXZwaHhQiIJAG6cEADuTKTR8K4RFLLxrq2cSPBYTu7NsBCpQyNYhIac5PmcDFCpYLBrEE4AjBgADFnkOVBLANaG0dAJYPAAnjxYEBDdIMbX7AlnAEbm5q0AMwYWBgMDHU4XK43Vx3R4QMDPV49LCfBK4e5wmAgsEQ4KbaKcJjwQxgaKwBI4eAsVBlMqwJDkDAAKzA7jOjWiGHIrzKDiwZQg7CYDAcEDKygAsgAZMq-CUylngkCmcoEonwQL4h5VE6GMoAKkMACEGGBOLBDMh8p1STqEYYDWVLJZElBCJZqIZgNURUgkEsHKhDAdDLtrrgANyWAC+LvIWF2HoTACZk1hAemACzpgCs6YAbN7qmcealnpx+fBg-tdtHyHHvOQyO5i3qfgxYOcHNFSIlyEhUmQoJ1g9EHB2ABQAYgLuznc4WBwwuwAlPWgsPR4ZZ-O9-XG5YmNc20EO12e32B8HpwdlHe7xveiPojeAGIfp-sPqpdjCbj3MGADkABSrzGtEDBLGAkoUKQQELLgcFgCKUxPn6AaOMGy4AOy5hGYYRk+PydO40SpJBAaJGA2EYPhEBRvGsbxpwuAOKehi4AwAAeMzDKMNYHAcACkT4VlWwYMMkpAHvGPwjl2ACOiSkIgHEYYGqQwOw1aEQxxGkRA5HabpKZMNxpLUMMVwTgwM4PveyhLiu67MU2qIcSR0RkcG5AUBABneUZqTwKQTDBmZFlUGcSA2VOt6OXezlrk+XHjksEUEQcslNj8yShScPp6t+MgbJwABeEDBkszyEvAX4-n+uAAdVCZGfkT5bq+O5Cdl1RpQ4GUhiuRHVF5PlhuZlkxYY8mducgVkRR-icNRtEpvRjF6hpWHDeGDHDVmo3tvNl79kgwatFg+QBdUnZTKVXXBnNXYGbxiL+KQrTYSGk0WQcU0vZc0GvEuv27FNAMWUDOWWHl8AFagAZgAw8nIEVQRhZ2+SAYYGCArDTKcGAqSdnkjQQJ5p29udz2MK91RPTu7As251RqvaXjlM8-heD4JFIPcWrWLsRQIvA3Oi+zAaNIsSAJJKEDkIkECmtExzs2UMvCzLESMGCCTQAxStvKqWucI0wvqsShgPEwOyuEhSBuh4KrVEErEdF03qGPoAahdEhgxoY7C9rghhAVSNJ0giuAYIiZQAAJ5MSzD0p0EBAe7iy4F7JIAMphF0RkANL5CHYcR1HNEx2AccJ8nGpp3cMSIOR5z5FnTZBJuFB3IYZOcH3hhOIYfDZ0EwCGPUVWGK4JFQEgrgLGQuBcdeEehQ4DgwMa1BIAhhjI6jMDBvA0Qq0HyY9z3U8z8Grj5GsFrL70a8MBvQFbzvEAAJJhGcWAh9j5ozPhfSmMZr43x9vfOe4Bz6cHOK7FepB36f2-jAQuhIkHAOJifWe59L6QInjA4Qs9XAtmQW-deF1N6kG3jAdQpB3C4JRqA224Cr4kLvmQh+JAYAxE4jEc4yoUFoNoUBfscQACCUAoCSmEWAICXC9Q3x4SIPhsABGBz8u4UR1CP4SLiLoGIopIJMCwMo4hqjb7T14XAkUkEezMCwK-VeNDgIYMzio6BpCNFwMTG41BHi6EMIgAACT8JhBwh8YgOESCIGQwYp4wAplAGsQcfHQPUeQkAWAUxBPEZ4+hP9IkrUcLE8cCSTbJLsBANJEVMnWN8TkvhGZCkhK-iUmAZTomVPiYk3SKT6nQGDICJpUC1F2P8a4LAWYOmGOKWE3pgZVz9OqUkn2qTRmGCzBM7h0zclYFzAs9B3SIlRMDOswZtTtnpMMLmfZNjJ6HLaQWU5EivErIqQsOJGyhl1IaYYIswdmnZNeXAvKcjXh2GJm8MRnSvHGjdDAeA0o4VWMmbY2BrhDLXFimcO4Hylk-1UEFfF6K7iYoOTiugrZ5KkC7MS0JP8mHuGNApc41Lnl+NyQypSKlEDMq6WEjljLzjKVUt4sFUzaVYE6BVPoawrgu2FXEcJCrypKqgAAJRdtylpELZkxFij8a4DBzhqteOEk1EE-BcqybK+xrhzqkGFa61h+CwFEKxS82l+K3VEAMZ-ANnr2GEIgdnAAuvGG+qKrT+1tKPHkrRfaJuiJODG0CjY3KPoy-5GBlJGXuPnI2CBOiTiAhgHNJsgKrl9VabishzR92DOPHlQRC6t1LvkBtUaG3SH6MBeQTBTCRLkaQQwAB1Toi8UyKHKKOhCJCKCaEgpQQeFBM0JptIHGMq4OK+MiHAc40iEAWwgJOVcJDGw333bGnukAYAIFNJAScrhFbK1VjEVwB66SGDfNwWwoxKaJJVqkNW2dvyB0nPGzgiwTibsoAerNcaYXhFHkhsAfBOBRqCP+sUJIQPBtiu4Lgyw8gUBIQwDA8MCoj1mvlLdNGZ4Hv1GUQw6hzVqXCHRqjPLODsEMJOGjfzBnXo7QPWjTGmQUC0RaS4o8r0j1MDugOIIohLCvRgb8sBqJXr4DR9xhio0iYwGJk2q4MAX3IFekh0BIAkKCDRvjsnmyAMU8Jg9ThVPWnU5EKCtmrO6f06uQzIJgkme0zZuzNjb09ykWW+Ar7L0Sezv+9QzwuyGDPeTBglGTikCE658ENj2D9nPRQCIWXT3nopsp1DPdoPCbgwhgelXkOHugfGjD7WCvYdw-hjjTnFhCbMyAmAKGRsezG35zoIIP7aYC1p4L+awAGaM5FgcpmrMxdXBJ3xviXMyeswxUgFNpEI0JPDS9rgJvIF-dNq0UBHOSZvsdhGFB46vEuwgm7763B4LRkvINrgI2Pck-F3xgnhMfYKiCfWYBKXwAizIQL62gLE1JnVzO+2uvQ7G4oObXJia5YvWZ1jB64dfa0WaJHcLTtIXq5jkmg8KZ1pG8HBzHg3ujeE8TjApOccU7IXj6nTJadgmR+Z-0lasds9xze7O8W1SwEJMSLmZQBZC3jNzCQpgQCQMEGQsQrIiB0BkCbWghvCDG5EGIUEvApB9Ct+gQ3UaYxAA)

### Verdict

Using event functions is a classical approach wich requires a bit more thought than using a state logic. But this pays back, as you can create a very fast and totally encapsulated logic. 

The whole script is very compact using only 65 lines of code and it is completely decoupled from any other page logic. This makes it easy to create modules or web components, that run in any environment, including React, Vue, Sevelte and Vanilla JS. As the code minimizes the transactions to the used properties, it will be veryfast. At least if you want to create framework-independent modules, using this classical approach is definitively worth a second thought!