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:
- https://simplemde.com/
- https://easy-markdown-editor.tk/
- https://ui.toast.com/tui-editor
- https://devdojo.com/markdownx
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.
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:
- https://devdojo.com/bobbyiliev/what-is-laravel-breeze-and-how-to-get-started
- https://devdojo.com/tnylea/tailwindcss-22-in-22-seconds
- https://devdojo.com/tnylea/alpinejs-for-beginners
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)