DEV Community

Cover image for Event based state control example: Create a TipTap module with Vanilla JS
Eckehard
Eckehard

Posted on

Event based state control example: Create a TipTap module with Vanilla JS

TipTap is a beautifuly and flexible inline WYSIWYG-Editor based on ProseMirror. 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, 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 using Vanilla JS

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

TipTap Interface
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:

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)
Enter fullscreen mode Exit fullscreen mode

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:

<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)
Enter fullscreen mode Exit fullscreen mode

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.

{#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)
Enter fullscreen mode Exit fullscreen mode

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)

The examples on TipTap will not run out of the box with vanilla JS, as the script path "@tiptap" is unknown to JS:

import { Editor } from "@tiptap/core";
Enter fullscreen mode Exit fullscreen mode

Instead you can load all files from a CDN like shown below, there is even no need to install TipTap on your machine

  <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'
Enter fullscreen mode Exit fullscreen mode

Now we can set up the editor buttons using an array "actions[]". This contains all necessary information to build the button menue:

    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)
      ]
Enter fullscreen mode Exit fullscreen mode

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:

    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()
    }
Enter fullscreen mode Exit fullscreen mode

"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:

    let editor = new Editor({
      element: document.querySelector('.element'),
      ...
      onTransaction({ editor }) {
        checkActive() }
    })
Enter fullscreen mode Exit fullscreen mode

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):

    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')
      }
    }
Enter fullscreen mode Exit fullscreen mode

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

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!

Top comments (0)