※本エントリは先日投稿したエントリのコードを修正して投稿したものです。
.NET Core 3.0のBlazorコトハジメとしてリアルタイムプレビューに対応したMarkdownエディタを作ってみました。
Blazorのことよく分かってないので雰囲気で作った感が満載です。
デモ
成果物
コードはGitHubに上げています。
プロジェクトの作成
dotnet new blazorserver -o MarkdownEditor cd MarkdownEditor
Rxの追加
Reactive Extensionsを使用するのでSysntem.Reactive
を追加します。
dotnet add package System.Reactive
Pages/Editor.razor
Pages
以下にエディタの画面を追加します。
@page
でURLを指定しています。
実装は別ファイルへ切り出して@inherits
で取り込みます。
oninput
イベントでTextAreaへの入力をリアルタイムに取得します。
Pages/Editor.razor
@page "/editor" @inherits MarkdownEditor.Components.EditorBase <h1>Markdown Editor</h1> <div style="height: 300px"> <div class="row h-100"> <div class="col-md h-100"> <textarea class="form-control h-100 w-100" @oninput="HandleOnInput"></textarea> </div> <div class="col-md h-100"> <div style="border: solid 1px;" class="h-100"> @((MarkupString)MarkdownText) </div> </div> </div> </div>
Components/EditorBase.cs
Componentsフォルダを作成し、内部にPages/Editor.razor
の実装クラスを追加します。
MarkdownをHtmlへ変換するにはJavaScriptのMarkedとhighlight.jsを使用します。 ※C#のMarkdigなどを使用してもOK
変換の処理はRxを使って1秒毎に行います。
C#のコードからJavaScriptを呼ぶためにIJSRuntime
を[inject]
指定します。
この属性を指定すると、自動的に依存性注入してくれるっぽいですね。
ComponentBase
を継承したクラスから画面要素を更新するにはInvokeAsync
を経由してStateHasChanged
を実行します。WPFのDispatcherみたいなノリですね。
Components/EditorBase.cs
using System; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Logging; using Microsoft.JSInterop; using System.Reactive.Linq; using System.Threading.Tasks; using System.Diagnostics; namespace MarkdownEditor.Components { public class EditorBase : ComponentBase, IDisposable { [Inject] private IJSRuntime _jsRuntime { get; set; } [Inject] private ILogger<EditorBase> _logger { get; set; } protected string MarkdownText { get; set; } private IObservable<long> _timerSource; private IDisposable _timerSubscription; string _inputText; public EditorBase() { MarkdownText = "Hello World."; } public void Dispose() { // 手抜きDispose _timerSubscription?.Dispose(); } // OnAfterRenderAsyncもある protected override void OnAfterRender(bool firstRender) { // 描画毎に呼ばれるので、最初の1回目のみとする. if (!firstRender) return; // Rxで1秒毎にMarkdownをHtmlへ変換 _timerSource = Observable.Timer(TimeSpan.FromSeconds(1), TimeSpan.FromMilliseconds(500)); _timerSubscription = _timerSource.Subscribe(async _ => await ConvertMarkdownToHtml()); } public void HandleOnInput(ChangeEventArgs e) { // oninputイベントハンドラ // TextAreaに入力した文字列を取得 _inputText = e?.Value?.ToString(); } private async Task ConvertMarkdownToHtml() { if (string.IsNullOrEmpty(_inputText)) return; try { // JavaScriptのConvertToHtml関数でMarkdownをHtml化 MarkdownText = await _jsRuntime.InvokeAsync<string>("ConvertMarkdownToHtml", _inputText); // ComponentBaseを継承したクラスから画面の更新する際はInvokeAsyncを使用する await InvokeAsync(() => StateHasChanged()); } catch (System.Exception exp) { _logger.LogError(exp.Message); } } } }
Shared/NavMenu.razor
作成したEditorページへのリンクを追加します。
// 途中省略 <li class="nav-item px-3"> <NavLink class="nav-link" href="fetchdata"> <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data </NavLink> </li> + <li class="nav-item px-3"> + <NavLink class="nav-link" href="editor" Match="NavLinkMatch.All"> + <span class="oi oi-pencil" aria-hidden="true"></span> Editor + </NavLink> + </li> </li> </ul> </div> // 以下省略
wwwroot/app.js
MarkdownテキストをHtml化する関数を作成します。
<code>
のclass
にうまくhljs
が入らなかったり、存在しないプログラム言語を```区で指定した際に例外を投げ続ける挙動があったので、highlight.jsのissueを参考に修正しています。
window.ConvertMarkdownToHtml = (markdownText) => { const renderer = new marked.Renderer() const doRenderCode = (code, lang) => lang && hljs.getLanguage(lang) ? hljs.highlight(lang, code, true).value : hljs.highlightAuto(code).value renderer.code = (code, lang) => `<pre><code class="hljs ${lang || ''}">${doRenderCode(code, lang)}</code></pre>` const markedOptions = { renderer } return marked(markdownText, markedOptions); }
Pages/_Host.cshtml
app.js
, Marked
, highlight.js
, シンタックスハイライト時のカラーテーマを読み込みます。
@page "/" @namespace MarkdownEditor.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>MarkdownEditor</title> <base href="~/" /> <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" /> <link href="css/site.css" rel="stylesheet" /> + <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.6.2/marked.min.js"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.8/highlight.min.js"></script> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.8/styles/monokai.min.css"> </head> <body> <app> @(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered)) </app> <script src="_framework/blazor.server.js"></script> + <script src="app.js"></script> </body> </html>
以上で完成です。
dotnet run
で起動してみましょう。
おわり
以前掲載していた記事から以下の点を変更しました。
TextAreaの値をリアルタイムに取得するのにTimer + IJSRuntimeのDom操作を使用していた点をOnInputイベントに変更しました。
定期的にPreviewを更新する処理を
System.Timer
からRxに変更しました。
前者はひどいやり方だったので気が付いた時にすっごく恥ずかしかったです…。
以上です。