サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

Vue.js: 簡単なMarkdownエディタをつくる


Vue.js公式サイトの「」に、わずかなコードでつくられた「Markdown エディタ の例」があります(サンプル001)。使っているライブラリやVueの構文についての説明はないので、いざつくってみようとすると、つまずきそうなところがあります。そこで、解説を加えながら、少しずつ組み立ててみましょう。

サンプル001■Vue.js: Markdown Editor Example

01 データを要素にバインディングする

ページを左右ふたつに分けて、左にテキストを入力し、右にフォーマットして表示するレイアウトにします(図001)。<body>要素には、つぎのように<textarea>要素と<div>要素をそれぞれ入力および表示の領域として、親の<div>要素(id属性"editor")に加えます。また、スタイルは以下のとおりです(「Markdown エディタ の例」のCSSをそのまま用いました)。

<body>要素

<div id="editor">
	<textarea>hello</textarea>
	<div></div>
</div>

<style>要素

html, body, #editor {
	margin: 0;
	height: 100%;
	font-family: 'Helvetica Neue', Arial, sans-serif;
	color: #333;
}
textarea, #editor div {
	display: inline-block;
	width: 49%;
	height: 100%;
	vertical-align: top;
	box-sizing: border-box;
	padding: 0 20px;
}
textarea {
	border: none;
	border-right: 1px solid #ccc;
	resize: none;
	outline: none;
	background-color: #f6f6f6;
	font-size: 14px;
	font-family: 'Monaco', courier, monospace;
	padding: 20px;
}
code {
	color: #f66;
}

図001■レイアウトされたページ

図001

JavaScritpコードは、つぎのようにVueクラスのインスタンスを定めます。引数のオブジェクトのプロパティelはVueオブジェクトが扱う要素で、<div>要素のid属性("editor")を渡しました。dataプロパティには、オブジェクトで任意のデータを納めます。プロパティや属性をデータとバインディングするのがv-bindディレクティブです。Vueインスタンスのデータ(input)は、v-bind ディレクティブで<textarea>要素のプロパティにバインディングしました[*1]。これでデータが要素のテキストとして加えられます(図002)。

<script>要素

var app = new Vue({
	el: '#editor',
	data: {
		input: '# hello'
	}
});

<body>要素

<div id="editor">
	<!--<textarea>hello</textarea>-->
	<textarea v-bind:value="input"></textarea>

</div>

図002■データが<textarea>要素のテキストとして加えられた

図002

[*1] <textarea>要素にはvalueという属性はありません。バインディングしているのは、JavaScriptが<textarea>要素を扱うHTMLTextAreaElementオブジェクトのプロパティvalueです(「公式チュートリアルから始めるVue.js vol.1『Markdown エディタ』」の「注意:textarea要素とvalue属性、textareaオブジェクトとvalueプロパティ」参照)。

02 テキストをMarkdownして表示する

Markdown」は、テキストのフォーマットを定める簡易な記法です。HTMLコードよりも簡単な書き方で、文字や段落の表記が整えられます。そして、markedはMarkdownのテキストをHTMLコードに変えるJavaScriptライブラリです。markedをcdnjsからつぎのように読み込んでおきます。

<head>要素

<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.6/marked.min.js"></script>

要素の子となるHTMLのコード(innerHTML)を書き替えるにはv-htmlを用います。値には算出プロパティ(compiledMarkdown)を定めて与えることにします(「Vue.js入門 05: 項目を数えて表示する」02「条件に合ったデータを数えて返す」参照)。

<body>要素

<div id="editor">

	<!--<div>-->
	<div v-html="compiledMarkdown"></div>
</div>

算出プロパティは、Vue()コンストラクタに渡すオブジェクトのcomputedプロパティに加えます。getterとして働きますので関数(function)で定め、値を返さなければなりません。HTMLコードの要素(<h1>)にデータ(input)をテキストとして含めました。これで、データがHTMLコードとして解釈され、バインディングした要素に加えられます(図003)。

<script>要素

var app = new Vue({

	data: {
		input: '# hello'
	},
	computed: {
		compiledMarkdown: function () {
			return '<h1>' + this.input + '</h1>';
		}
	}
});

図003■データがHTMLコードとして右の領域に加えられた

図003

HTMLコードが正しく解釈されて要素の子に加えられることが確かめられましたので、marked()メソッドを使って書き替えます。引数に渡すのはMarkdownされたテキストです。HTMLコードが返されますので、算出プロパティ(compiledMarkdown)の戻り値とします。これでMarkdownしたテキストがHTMLコードのフォーマットで示されます(図004)。

<script>要素

var app = new Vue({

	computed: {
		compiledMarkdown: function () {
			// return '<h1>' + this.input + '</h1>';
			return marked(this.input);
		}
	}
});

図004■MarkdownしたテキストがHTMLフォーマットで表示される

図004

ここまでの<body>要素の記述を、以下のコード001にまとめました。なお、marked()メソッドの呼び出しに第2引数のオブジェクトを加え、sanitizeプロパティを定めています。この意味と役割については、05「サニタイジングする」でご説明します。

<script>要素

computed: {
	compiledMarkdown: function () {
		return marked(this.input, {sanitize: true});
	}
}

コード001■MarkdownしたテキストをHTMLのフォーマットで別の要素に表示する

<body>要素

<div id="editor">
	<textarea v-bind:value="input"></textarea>
	<div v-html="compiledMarkdown"></div>
</div>
<script>
var app = new Vue({
	el: '#editor',
	data: {
		input: '# hello'
	},
	computed: {
		compiledMarkdown: function () {
			return marked(this.input, {sanitize: true});
		}
	}
});
</script>

03 テキストの入力に応じてMarkdownしたテキストをフォーマットする

v-onディレクティブは、コロン(:)のあとに引数として添えたイベントにリスナーを定めます。input<textarea>要素の値が変更された場合に起こるイベントです。つまり、リスナーはテキストがひと文字書き替わるたびに呼び出されます。

<body>要素

<div id="editor">
	<textarea v-bind:value="input" v-on:input="update"></textarea>

</div>

イベントリスナー(update())は、Vueオブジェクトにメソッド(methodsプロパティ)として加えます。引数から得たEvent.targetプロパティはイベントが起こった<textarea>要素を参照しますので、入力したテキストを取り出すのはvalueプロパティです(前述注[*1]参照)。そのテキストでVueオブジェクトのデータ(input)を書き替えます。

<script>要素

var app = new Vue({

	methods: {
		update: function(eventObject) {
			this.input = eventObject.target.value;
		}
	}
});

これで左(<textarea>要素)にMarkdownで入力したテキストが、入力に応じて右(<div>要素)にHTMLのフォーマットで表示されます(図004)。Markdownのエディタとして最低限の動きはできたといえるでしょう。<body>要素の記述は、以下のコード002のとおりです。

図004■Markdownしたテキストが入力に応じてフォーマットされる

図004

コード002■Markdownしたテキストのキー入力に応じてフォーマットする

<body>要素

<div id="editor">
	<textarea v-bind:value="input" v-on:input="update"></textarea>
	<div v-html="compiledMarkdown"></div>
</div>
<script>
var app = new Vue({
	el: '#editor',
	data: {
		input: '# hello'
	},
	computed: {
		compiledMarkdown: function () {
			return marked(this.input, {sanitize: true});
		}
	},
	methods: {
		update: function(eventObject) {
			this.input = eventObject.target.value;
		}
	}
});
</script>

04 処理を一定時間分まとめて行う

テキストを続けざまに入力したり削除しているとき、ひと文字ごとにフォーマットし直すのは、負荷が無駄に増えます。表示は少し遅れても、キー入力をある程度まとめて処理した方が効率的です。ユーティリティライブラリLodash_.debounce()メソッドを使えばそのように組み立てられます。第1引数の関数は、第2引数の時間(ミリ秒)遅れて実行されます。そして、その間メソッドが繰り返し呼び出された場合でも、関数の処理は1回にまとめられるのです。


_.debounce(関数, 時間)

前掲コード002でテキスト入力時(inputイベント)のリスナーに定めたメソッド(update)に、_.debounce()メソッドをつぎのように組み込みます。

<script>要素

var app = new Vue({

	methods: {
		update: _.debounce(function(eventObject) {
			this.input = eventObject.target.value;
		}, 1000)
	}
});

Vue.jsではもっともよく使われるふたつのディレクティブv-bindv-onについては省略した書き方ができます。つぎのように、v-bindはコロン:のみで済み、v-onはイベントの前に@を添えるだけでよいのです。書き上がった<body>要素の記述は、以下のコード003にまとめました。

<body>要素

<div id="editor">
	<!--<textarea v-bind:value="input" v-on:input="update"></textarea>-->
	<textarea :value="input" @input="update"></textarea>

</div>

コード003■テキスト入力を少しずつまとめてMarkdownする

<body>要素

<div id="editor">
	<textarea :value="input" @input="update"></textarea>
	<div v-html="compiledMarkdown"></div>
</div>
<script>
var app = new Vue({
	el: '#editor',
	data: {
		input: '# hello'
	},
	computed: {
		compiledMarkdown: function () {
			return marked(this.input, {sanitize: true});
		}
	},
	methods: {
		update: _.debounce(function(eventObject) {
			this.input = eventObject.target.value;
		}, 1000)
	}
});
</script>

05 サニタイジングする

前掲コード003ではmarked()メソッドの呼び出しで、第2引数のオブジェクトにsanitizeプロパティをtrueにして与えました。これはサニタイジングするためです。第2引数を渡さないとこの処理が加えられません。すると、テキストにタグを含めてしまうことができます。たとえば、属性onclickにJavaScriptコードを書けば実行されてしまうのです(図005)。悪意のある攻撃を受ける危険が生じます。

<script>要素

var app = new Vue({

	computed: {
		compiledMarkdown: function () {
			return marked(this.input);  // , {sanitize: true});
		}
	},

});

図005■要素にonclick属性にスクリプトを加えてクリックすると実行される

図005

sanitizeオプションをtrueに定めれば、タグを一般的な文字列に変える(エスケープする)ため、危険が防げるのです。

図006■sanitizeオプションを有効にするとタグがエスケープされる

図005

作成者: 野中文雄
作成日: 2016年3月24日


Copyright © 2001-2017 Fumio Nonaka.  All rights reserved.