Markdown editor with Blazor

markdown editor

In this new post, I will show you have to create a simple Markdown editor component for Blazor Assembly and Blazor Server.

The source code of this component with an example is on GitHub. If you are looking for more examples of components, here some more links:

In January 2022 I completely rewrite this component. Now, the Markdown Editor for Blazor is a very powerful and complete component with upload images.

The final result of this component in an application is like the following screenshots. In the Write tab, you type your text in Markdown format. When you click on the Preview tab, you have the text in HTML.

Write your Markdown text - Markdown editor with Blazor
Write your Markdown text
Markdown preview - Markdown editor with Blazor
Markdown preview

The code

So, as usual I have to create a new project for the component and create a new Razor page and its name is MarkdownEditor.razor. The HTML for this page is the following code

@using System.Linq.Expressions

<div id="markdownEditor">
	<ul class="nav nav-tabs mb-3" id="editorTabs" role="tablist">
		<li class="nav-item" role="presentation">
			<a class="nav-link @(isWriteActive ? "active" : "")" id="editor-tab" 
			   data-toggle="tab" href="#editor" role="tab"
			   aria-controls="editor" aria-selected="true" @onclick:preventDefault 
			   @onclick="() => HandleWriteClick()">Write</a>
		</li>
		<li class="nav-item @(isWriteActive ? "" : "active")" role="presentation">
			<a class="nav-link" id="preview-tab" data-toggle="tab" href="#preview" role="tab"
			   aria-controls="preview" aria-selected="false" @onclick:preventDefault 
			   @onclick="() => HandlePreviewClick()">Preview</a>
		</li>
		@if (EnableToolbar)
		{
			<li class="nav-item ml-auto">
				<button class="btn btn-sm btn-secondary" @onclick:preventDefault 
						@onclick="() => HandleBoldClick()"><i class="fas fa-bold"></i></button>
				<button class="btn btn-sm btn-secondary" @onclick:preventDefault 
						@onclick="() => HandleItalicClick()"><i class="fas fa-italic"></i></button>
				<button class="btn btn-sm btn-secondary" @onclick:preventDefault 
						@onclick="() => HandleListClick()"><i class="fas fa-list"></i></button>
			</li>
		}
	</ul>

	<div class="tab-content" id="editorTabContent">
		@if (isWriteActive)
		{
			<div class="tab-pane fade show active" id="editor" role="tabpanel" 
			     aria-labelledby="editor-tab">
				<textarea id="@id" value="@Value" @oninput="HandleInput" 
						  class="@_fieldCssClasses form-control" rows="@_rows"></textarea>
				<span class="text-muted">Learn more about MarkDown 
					<a href="" @onclick:preventDefault @onclick="() => HandleHelpClick()">here.</a>
				</span>
				@if (showHelp)
				{
					<div class="alert alert-info" role="alert">
						<MarkdownHelp />
						<button type="button" class="btn btn-sm btn-secondary" @onclick:preventDefault 
								@onclick="() => HandleCloseHelpClick()">Close Help</button>
					</div>
				}
			</div>
		}
		else
		{
			<div class="tab-pane fade show active" id="editor" role="tabpanel" 
			     aria-labelledby="editor-tab">
				@((MarkupString)_previewText)
			</div>
		}
	</div>
</div>

Then, the code section in the page is:

@code { 
	[Parameter]
	public string Value { get; set; }

	[Parameter]
	public EventCallback<string> ValueChanged { get; set; }

	[Parameter]
	public Expression<Func<string>> ValueExpression { get; set; }

	[Parameter]
	public bool EnableToolbar { get; set; } = true;

	[Parameter]
	public string id { get; set; }

	[CascadingParameter]
	private EditContext CascadedEditContext { get; set; }

	private bool isWriteActive = true;

	private string _previewText = "";
	private int _rows = 6;
	private bool showHelp = false;
	private FieldIdentifier _fieldIdentifier;
	private string _fieldCssClasses => CascadedEditContext?.FieldCssClass(_fieldIdentifier) ?? "";

	protected override void OnInitialized()
	{
		_fieldIdentifier = FieldIdentifier.Create(ValueExpression);
	}

	private void CalculateSize(string value)
	{
		_rows = Math.Max(value.Split('\n').Length, value.Split('\r').Length);
		_rows = Math.Max(_rows, 6);
	}

	private void HandleHelpClick()
	{
		showHelp = true;
	}

	private void HandleCloseHelpClick()
	{
		showHelp = false;
	}

	private async Task HandleInput(ChangeEventArgs args)
	{
		CalculateSize(args.Value.ToString());

		await ValueChanged.InvokeAsync(args.Value.ToString());

		CascadedEditContext?.NotifyFieldChanged(_fieldIdentifier);
		_previewText = MarkdownParser.Parse(args.Value.ToString());
	}

	private void UpdatePreview()
	{
		_previewText = MarkdownParser.Parse(Value.ToString());
	}

	private void HandleBoldClick()
	{
		Value = $"{Value} **(Bolded Text Here)**";
		UpdatePreview();
	}

	private void HandleItalicClick()
	{
		Value = $"{Value} *(Italic Text Here)*";
		UpdatePreview();
	}

	private void HandleListClick()
	{
		Value = $"{Value} \n - List Item";
		UpdatePreview();
	}

	private void HandleWriteClick()
	{
		isWriteActive = true;
	}

	private void HandlePreviewClick()
	{
		isWriteActive = false;
	}
}

MarkdownParser

internal static class MarkdownParser
{
    internal static string Parse(string value)
    {
        if (!string.IsNullOrEmpty(value))
        {
            var pipeline = new MarkdownPipelineBuilder()
                .UseEmojiAndSmiley()
                .UseAdvancedExtensions()
                .Build();

            return Markdown.ToHtml(value, pipeline);
        }
        return "";

    }
}

Usage

Now, to convert Markdown in HTML, I’m adding Markdig from a NuGet package with

Install-Package Markdig

So, add the Editor to your _Imports.razor

@using PSC.Blazor.Components.MarkdownEditor

Then, inside of an EditForm reference the editor component and bind it.

<EditForm OnValidSubmit="DoSave" Model="model">
    <MarkdownEditor @bind-Value="model.Comments"/>
</EditForm>

The editor binds the markdown text, not parsed HTML. The toolbar is added by default. You can disable this by passing EnableToolbar="false" into the component.

Wrap up

In conclusion, I created a component for a Markdown editor with Blazor. Please leave your comment below or in the forum.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.