Edited at
Vue.jsDay 20

Vueで小説投稿サイトのフォームを改良した

More than 3 years have passed since last update.


tl;dr


  • Vue.jsを使って小説投稿サイトのフォームを便利にした

  • 独自記法のプレビュー機能を作った


独自記法のプレビュー機能

denkinovel_preview.gif

こういうの。


作り方

本文のtextareaにv-modelを指定しておき、v-onのclickとkeydown, keyup, keypressにプレビュー表示を更新するメソッド(本件ではupdatePreview)を指定しておく。

なお本件での独自記法は、[bg red]や[bg sky]のような文字列を見つけると、「背景記法」だと判断するというシンプルなもの。

またプレビューは「[bg red]のようにデフォルトで用意してある背景画像名」「ユーザーがアップデートした画像」「URLを直接指定した画像」の3種類の画像を表示できる。が、基本的なVueを使っての作りは同じ。


作り方

HTML(erbだけど)のほうには

    <%= f.text_area :body, rows:20, id:'js-storyForm__body', 'v-model' => 'body', 'v-on' => 'click: updatePreview, keydown: updatePreview, keyup: updatePreview, keypress: updatePreview' %>

と書いておいて、JS(CoffeeScriptだけど)のほうには以下のように記述しておく。


form.js.coffee

class Storyblog.Vue.Stories.Form

createVue: ->
@vm = new Vue(
el: '#js-storyForm'
data:
currentTitle: ''
currentTableType: 'buttonAndCode'
currentTagType: ''
currentNotices: []
currentItems: []
body: ''
tags: []
preparedPictures: []
currentUserID: null
isActivePreview: false
isLoading: false
toggleActivatePreviewButtonText: 'リアルタイムプレビューをONにする'
filenameURL: {}

methods:
initialize: (e) ->
@currentTitle = @$data['basicInstruction'].title
@fillContent('basicInstruction')
@getCurrentUserID()

renderPreview: (e) ->
@changeInstruction(e)
@updatePreview(e)

updatePreview: (e) ->
return unless @isActivePreview
re = /\[.*?\]/g;
tags = @body.match(re)
tagArtRe = /\[(\S+?)\s+(.+?)\]/;
updatedTags = []
_.each(tags, (tag) ->
tagArgArray = tag.match(tagArtRe)
return unless tagArgArray
return if tagArgArray.length < 2
funcName = tagArgArray[1]
tagArg = tagArgArray[2]
isBG = false
if funcName == 'bg'
isBG = true
isImage = false
if funcName == 'image'
isImage = true
isTextColor = false
if funcName == 'text-color'
isTextColor = true
isFilter = false
if funcName == 'filter'
isFilter = true
updatedTags.push({ 'whole':tag, 'funcName': funcName, 'arg':tagArg, 'isBG': isBG, 'isImage': isImage, 'isTextColor': isTextColor, 'isFilter': isFilter ,'url': null})
)
@tags = updatedTags
@fetchFromPreparedPictures()
@updateImagesURLs()
@setImageURLToTags()

setImageURLToTags: ->
self = @
_.each(@tags, (tag) ->
if tag['isBG'] or tag['isImage']
url = self.filenameURL[tag['arg']]
if url
tag['url'] = url
else
tag['url'] = tag['arg']
)

updateImagesURLs: ->
self = @
_.each(@tags, (tag) ->
if tag['isBG'] or tag['isImage']
self.setImageURLWithPreparedPictures(tag['arg'])
self.fetchFromCurrentUserPicturesAndSetImageURLWithFilename(tag['arg'])
)

getCurrentUserID: ->
@currentUserID = parseInt($('#js-currentUserID').text())

fetchFromCurrentUserPicturesAndSetImageURLWithFilename: (filename) ->
self = @
return unless @currentUserID
return if self.filenameURL[filename]
$.ajax({
type: 'GET'
datatype: 'json'
url: "../../users/#{self.currentUserID}/picture.json",
data: {
filename: filename
}
success: (image_info) ->
unless image_info
self.filenameURL[filename] = filename
else
file_path = image_info['file_path']
if file_path
url = file_path['image']['thumb']['url']
self.filenameURL[filename] = url
error: (data) ->
console.log data
}
)
setImageURLWithPreparedPictures: (filename) ->
self = @
return unless self.preparedPictures
url = self.preparedPictures[filename]
self.filenameURL[filename] = url if url

fetchFromPreparedPictures: () ->
self = @
return if self.preparedPictures.length > 0
$.ajax({
type: 'GET'
datatype: 'json'
url: "../../prepared_pictures.json",
success: (data) ->
self.preparedPictures = data.prepared_pictures
error: (data) ->
console.log data
}
)

toggleActivePreview: (e) ->
if @isActivePreview
@toggleActivatePreviewButtonText = 'リアルタイムプレビューをONにする'
@isActivePreview = false
else
@toggleActivatePreviewButtonText = 'リアルタイムプレビューをOFFにする'
@isActivePreview = true
)
@vm.initialize()

Vue.component('previewItem', {
template: '''
<td><code>{{whole}}</code></td>
<td>
<img class="previewItem__image" v-attr="src:url" v-if="isBG"/>
<img class="previewItem__image" v-attr="src:url" v-if="isImage"/>
<span v-if="isTextColor" v-style="color:arg">文字色</span>
<span v-if="isFilter" v-style="background-color:arg">フィルター</span>
</td>
'''
})


ちなみに、本文から独自記法を正規表現で取得しているのだけど、毎アクションごとに検索をかけるとパフォーマンス的に問題があるので、リアルタイムプレビューはOFFにできるようにしておくほうがいい


補足

Vueの便利なところはComponentというViewとVMを凝集度高く記述できる仕組みにあると思っている。上記のプレビュー機能だけだとcomponentはあまり効果的に動いていないが、プレビューで表示したアイテムをクリックするとライトボックスにして詳細表示、という風に、処理とViewが密接に関係する部分があるとComponentが大活躍する。