CodeBox CodeBox

Nuxt.jsにWYSIWYGエディタを導入しよう【Ckeditor5】

Vue / Nuxt
けい

概要

Ckeditor5というWordpressのようなエディター機能を追加できるライブラリをNuxt.jsに導入する方法を紹介します。Nuxt.jsを使った日本語の記事はまだまだ少ないので、ぜひ参考にしてみてください。

導入編

まずは2つのライブラリをインストールするので、下記コマンドを実行してください。

npm i @blowstack/ckeditor5-full-free-build @ckeditor/ckeditor5-vue2  --save


多くの記事では、@blowstack/ckeditor5-full-free-buildではなく、@ckeditor/ckeditor5-build-classicをインストールしていますが確実にハマります。理由は@ckeditor/ckeditor5-build-classicは、デフォルトで必要最低限の機能しか揃っておらず、例えば画像のリサイズや文字色などを変えるのが非常に難しいからです。

npm install --save @ckeditor/ckeditor5-build-classic // NG例


*@ckeditor/ckeditor5-build-classicのデフォルトでは、下画像のツールバーとなります。
ckeditor5-build-classicのデフォルト設定

コンポーネントを作成しよう

ライブラリをインストールできたら、エディターのコンポーネントを作成しましょう。componentsディレクトリ配下に「Editor.vue」を作成し、下記コードを貼り付けてください。

[components/Editor.vue]

template>
  <client-only>
    <ckeditor
      :editor="editor"
      v-model="editorData"
      :config="editorConfig"
      @input="updateContents"
    ></ckeditor>
  </client-only>
</template>

<script>
let FullFreeBuildEditor;
let CKEditor;

if (process.client) {
  FullFreeBuildEditor = require("@blowstack/ckeditor5-full-free-build");
  CKEditor = require("@ckeditor/ckeditor5-vue2");
  require("@blowstack/ckeditor5-full-free-build/build/translations/ja.js"); // 日本語化ファイル
} else {
  CKEditor = { component: { template: "<div></div>" } };
}

export default {
  components: {
    ckeditor: CKEditor.component,
  },
  data() {
    return {
      editor: FullFreeBuildEditor,
      editorData: "",
      editorConfig: {
        language: "ja",
      },
    };
  },
  methods: {
    updateContents() {
      this.$emit("update", this.editorData);
    },
  },
};
</script>


scriptタグ内のライブラリの読み込みは非常に重要です。
この書き方をしないとNuxtでお馴染みの「Window is not defined」というエラーにハマることになります。

これはSSR(サーバーサイドレンダリング)の場合、Windowオブジェクトにアクセスできないために起きるエラーです。ですので、CSR(クライアントサイドレンダリング)の場合とSSRで条件分けする必要があります。

if (process.client) {
  FullFreeBuildEditor = require("@blowstack/ckeditor5-full-free-build");
  CKEditor = require("@ckeditor/ckeditor5-vue2");
  require("@blowstack/ckeditor5-full-free-build/build/translations/ja.js"); // 日本語化ファイル
} else {
  CKEditor = { component: { template: "<div></div>" } };
}


次にこのコンポーネントをpageで読み込みましょう。今回はpages配下にarticle/index.vueを作成し、下記のようなコードにしました。

[pages/article/index.vue]

<template>
  <div class="editor-container">
    <Editor />
  </div>
</template>

<script>
import Editor from "@/components/Editor.vue";


export default {
  components: {
    Editor,
  },
};
</script>

<style scoped>
.editor-container {
  padding: 20px;
}
</style>


ここまで出来たら下画像のようになります。

エディターをカスタマイズしよう

ツールバーをカスタマイズする

デフォルトの状態でも機能的に問題ないのですが、不要なツールバーを非表示にしたい場合もあると思います。その場合は、「editorConfig」というオプションにtoolbarを追加します。

editorConfig: {
        toolbar: [
          "undo",
          "redo",
          "heading",
          "|",
          "FontColor",
          "FontSize",
          "FontFamily",
          "bold",
          "italic",
          "|",
          "link",
          "uploadImage",
          "ImageResize",
          "imageTextAlternative",
          "|",
          "numberedList",
          "bulletedList",
        ],
      },


これでツールバーがスッキリしましたね。
項目名に関しては公式ドキュメントを参考にして、必要なものがあれば「toolbar」の配列から削除や追加してください。

placeholderを非表示・変更する

デフォルトの状態では、「タイトルを入力」と「コンテンツを入力または貼り付けてください。」の2つのplaceholderが表示されてしまいます。
タイトルの方は不要なので、「editorConfig」に「removePlugins」を追加します。

editorConfig: {
省略
},
placeholder: "ここに本文を入力してください",
removePlugins: ["Title"]


そうするとタイトルの部分が消え、placeholderの内容も変わりました。

子要素から親要素のpropsを更新する

VueやNuxtでは親のコンポーネントから子要素に値を渡すことが頻繁にあります。
Vueでは子要素に渡す値を「props」と呼んでいます。

propsは子要素内で変更することができないというルールがあります。実際に下記の例で何が起きるか見ていきます。

[components/Editor2.vue]

<template>
  <client-only>
    <ckeditor
      :editor="editor"
      v-model="contents"
      :config="editorConfig"
    ></ckeditor>
  </client-only>
</template>


<script>
let FullFreeBuildEditor;
let CKEditor;

if (process.client) {
  FullFreeBuildEditor = require("@blowstack/ckeditor5-full-free-build");
  CKEditor = require("@ckeditor/ckeditor5-vue2");
  require("@blowstack/ckeditor5-full-free-build/build/translations/ja.js");
} else {
  CKEditor = { component: { template: "<div></div>" } };
}


export default {
  components: {
    ckeditor: CKEditor.component,
  },
  props: {
    contents: {
      type: String,
      default: "",
    },
  },
  data() {
    return {
      editor: FullFreeBuildEditor,
      editorData: "",
      editorConfig: {
        language: "ja",
        toolbar: [
          "undo",
          "redo",
          "heading",
          "|",
          "FontColor",
          "FontSize",
          "FontFamily",
          "bold",
          "italic",
          "|",
          "link",
          "uploadImage",
          "ImageResize",
          "imageTextAlternative",
          "|",
          "numberedList",
          "bulletedList",
        ],
        placeholder: "ここに本文を入力してください",
        removePlugins: ["Title"],
      },
    };
  },
};
</script>


[pages/article2/index.vue]

<template>
  <div class="editor-container">
    <Editor2 :contents="contents" />
  </div>
</template>

<script>
import Editor2 from "@/components/Editor2.vue";

export default {
  components: {
    Editor2,
  },
  data() {
    return {
      contents: "",
    };
  },
};
</script>

<style scoped>
.editor-container {
  padding: 20px;
}
</style>


この状態で文字を入力して、Chromeの検証ツール(Console)を確認してみましょう。下記のようなエラーが入力するたびに現れるはずです。

これは子要素(Editor.vue)内で、contentsというpropsを直接更新しようとしているためです。propsを更新するには、「emit」を使って親要素にpropsの内容を更新してもらう必要があります。

[components/Editor2.vue(修正版)]

  1. v-model="contents"から:value="contents"(v-bind="contents")に変更
  2. $emitで親コンポーネントであるEditor2に入力された内容を送っています。
  3. Ckeditorには入力されたのを監視する「input」というイベントハンドラが用意されているので、@inputとできます。
<template>
  <client-only>
    <ckeditor
      :editor="editor"
      :value="contents"
      :config="editorConfig"
      @input="(event) => $emit('input', event)"
    ></ckeditor>
  </client-only>
</template>

<script>
let FullFreeBuildEditor;
let CKEditor;

if (process.client) {
  FullFreeBuildEditor = require("@blowstack/ckeditor5-full-free-build");
  CKEditor = require("@ckeditor/ckeditor5-vue2");
  require("@blowstack/ckeditor5-full-free-build/build/translations/ja.js");
} else {
  CKEditor = { component: { template: "<div></div>" } };
}

export default {
  components: {
    ckeditor: CKEditor.component,
  },
  props: {
    contents: {
      type: String,
      default: "",
    },
  },
  data() {
    return {
      editor: FullFreeBuildEditor,
      editorData: "",
      editorConfig: {
        language: "ja",
        toolbar: [
          "undo",
          "redo",
          "heading",
          "|",
          "FontColor",
          "FontSize",
          "FontFamily",
          "bold",
          "italic",
          "|",
          "link",
          "uploadImage",
          "ImageResize",
          "imageTextAlternative",
          "|",
          "numberedList",
          "bulletedList",
        ],
        placeholder: "ここに本文を入力してください",
        removePlugins: ["Title"],
      },
    };
  },
};
</script>


[pages/article2/index.vue(修正版)]

<template>
  <div class="editor-container">
    <Editor2 :contents="contents" @input="updateContents" />
    {{ contents }}
  </div>
</template>

<script>
import Editor2 from "@/components/Editor2.vue";

export default {
  components: {
    Editor2,
  },
  data() {
    return {
      contents: "",
    };
  },
  methods: {
    updateContents(value) {
      this.contents = value;
    },
  },
};
</script>

<style scoped>
.editor-container {
  padding: 20px;
}
</style>


これで親コンポーネントのpropsを更新できるようになりました。

ABOUT ME

けい
ベンチャーのフロントエンジニア。 主にVueとTypescriptを使っています。ライターのための文字数カウントアプリ:https://easy-count.vercel.app/