Build a Markdown Editor Component with AlpineJS & Laravel Blade Component

Build a Markdown Editor Component with AlpineJS & Laravel Blade Component

Written by Mithicher Baro on Mar 23rd, 2022 Views Report Post

In this post we'll be looking at some simple ways to create a reusable markdown editor without having to use a large or complicated javascript library. The blade component uses Tailwind CSS for styling data and Alpine.js for javascript part.

What is markdown?

Markdown is a lightweight markup language for creating formatted text using a plain-text editor - By Wikipedia

Markdown is used everywhere starting from Github, so knowing how to use it can be a valuable tool for us.

The following are some of the various markdown editor:

What we will make?

Here we will try to make a simple homegrown markdown editor with preview, with the help of Alpine.js, Laravel Blade components and marked.js, a javascript library for parsing markdown content.

The UI of the markdown editor is inspired from the Nova markdown editor.

m-editor

marked.js is a javascript library for parsing markdown. It converts Markdown content into HTML with JavaScript.

I'm assuming you're familiar with Alpine.js, Laravel Blade Components and Tailwind CSS and have also set up a basic laravel breeze application.

Here are a few resources to get you started:

Include @tailwindcss/typography for styling the markdown content in your tailwind.config.js.

// tailwind.config.js
const defaultTheme = require('tailwindcss/defaultTheme');

module.exports = {
    content: [
        './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
        './storage/framework/views/*.php',
        './resources/views/**/*.blade.php',
    ],

    theme: {
        extend: {
            fontFamily: {
                sans: ['Nunito', ...defaultTheme.fontFamily.sans],
            },
        },
    },

    plugins: [
        require('@tailwindcss/forms'),
        require('@tailwindcss/typography'),
    ],
};

Update your app layout to this:

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="csrf-token" content="{{ csrf_token() }}">

        <title>{{ config('app.name', 'Laravel') }}</title>

        <!-- Fonts -->
        <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap">

        <!-- Styles -->
        <link rel="stylesheet" href="{{ asset('css/app.css') }}">

        @stack('styles')

        <!-- Scripts -->
        <script src="{{ asset('js/app.js') }}" defer></script>
    </head>
    <body class="font-sans antialiased">
        <div class="min-h-screen bg-gray-100">
            @include('layouts.navigation')

            <!-- Page Heading -->
            <header class="bg-white shadow">
                <div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
                    {{ $header }}
                </div>
            </header>

            <!-- Page Content -->
            <main>
                {{ $slot }}
            </main>
        </div>

        @stack('scripts-footer')
    </body>
</html>

Markdown Editor Blade Component

Let us create a component named m-editor.blade.php inside resources/views/components folder.

//resources/views/components/m-editor.blade.php
@props([
    'id' => 'editor-'. str()->random(8),
    'height' => '400px',
    'label' => null,
    'name' => null,
    'value' => null,
    'noMargin' => false
])

<div class="{{ $noMargin ? 'mb-0' : 'mb-5' }}">
    @if($label)
        <label class="block font-medium text-sm text-gray-800 mb-1">
            {{ $label }}
        </label>
    @endif

    <div 
      x-data="{
        height: '{{ $height }}',
        tab: 'write',
        content: {{ collect($value) }},
        showConvertedMarkdown: false,
        convertedContent: '',
        convertedMarkdown() { 
          this.showConvertedMarkdown = true;
          this.convertedContent = marked.parse(DOMPurify.sanitize(this.content));
        }
      }"
      class="relative" 
      x-cloak
    >
      <div class="flex items-center bg-gray-50 border border-b-0 border-gray-300 block rounded-t-md text-gray-400 pr-4">
          <div class="flex-1">
              <button type="button" class="py-2 px-4 inline-block font-semibold" :class="{ 'text-indigo-600': tab === 'write' }" x-on:click.prevent="tab = 'write'; showConvertedMarkdown = false">Write</button>
              <button type="button" class="py-2 px-4 inline-block font-semibold" :class="{ 'text-indigo-600': tab === 'preview' && showConvertedMarkdown === true }" x-on:click.prevent="tab = 'preview'; convertedMarkdown()">Preview</button>
          </div>
      </div>

      <textarea spellcheck="false" x-show="! showConvertedMarkdown" id="{{ $id }}" x-ref="input" x-model="content" name="{{ $name }}" class="overflow-y-auto form-textarea bg-white relative transition duration-150 ease-in-out block w-full font-mono text-sm text-gray-700 border border-gray-300 bg-white px-5 py-6 resize-none rounded-b-md focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500" :style="`height: ${height}; max-width: 100%`"></textarea>

      <div x-show="showConvertedMarkdown">
          <div x-html="convertedContent" class="w-full prose max-w-none prose-indigo leading-6 rounded-b-md shadow-sm border border-gray-300 p-5 bg-white overflow-y-auto" :style="`height: ${height}; max-width: 100%`"></div>
      </div>
    </div>

    @error($name)
        <p class="text-sm text-red-600 mt-1">{{ $message }}</p>
    @enderror
</div>

@pushOnce('scripts-footer')
<script src="https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js"></script>
@endpushOnce

Let's try to breakdown the component

height: default height of the markdown editor

tab: the editor consists of two tabs write and preview, default is write

content: content of the editor, default to null

showConvertedMarkdown: consists of a boolean value that helps toggling the two tabs

convertedContent: stores the parsed markdown content for preview

convertedMarkdown(): a function that converts the markdown content to HTML

DOMPurify is a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML

<textarea 
  spellcheck="false" 
  x-show="! showConvertedMarkdown" 
  id="{{ $id }}" 
  x-ref="input" 
  x-model="content" 
  name="{{ $name }}" 
  x-bind:style="`height: ${height}; max-width: 100%`"
  class="overflow-y-auto form-textarea bg-white relative transition duration-150 ease-in-out block w-full font-mono text-sm text-gray-700 border border-gray-300 bg-white px-5 py-6 resize-none rounded-b-md focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500"
></textarea>

spellcheck: disables spelling errors

x-show: shows the write tab if showConvertedMarkdown is false

x-model: two-way binding with the initial content values

:style: dynamically binding of height value in style attributes.

<div x-show="showConvertedMarkdown">
    <div x-html="convertedContent" 
class="w-full prose max-w-none prose-indigo leading-6 rounded-b-md shadow-sm border border-gray-300 p-5 bg-white overflow-y-auto" 
:style="`height: ${height}; max-width: 100%`"></div>
</div>

x-show: shows the preview tab if showConvertedMarkdown is true

x-html: display the parsed markdown stored in convertedContent

:style: dynamically binding of height value in style attributes.

How to use the component inside any blade view page

<x-m-editor 
  label="Content"
  name="content"
/>

Feel free to checkout the full component that works with both Laravel Blade and Laravel Livewire.

https://gist.github.com/mithicher/209687865454472dbf0948954e2bc5ce

Checkout my other blog posts

Conclusion

That's all for now, I hope you enjoyed it!

Comments (0)