20201223のvue.jsに関する記事は8件です。

LaravelとVue.jsを使った見積作成アプリ その1

はじめに

この記事は私自身がLaravelとVue.jsを勉強する目的で学んだことをまとめたものです。

この記事で作成するアプリケーションについて

この記事は以下の環境で作成しました。

  • Laravel 5.7.29
    • PHP 7.3.11
  • Vue.js 2.6.11

アプリケーションの全体像

今回作成する見積作成アプリは全部で6画面あり、認証機能まで備えたアプリケーションを作成していきます。

こちらが完成した見積作成アプリです。

  • 見積一覧ページ
    • このページでは作成した見積の一覧を表示します。
  • 見積編集ページ
    • このページで見積の内容を編集、保存します。
  • PDF表示ページ
    • このページでは作成した見積をPDFで表示し、保存と印刷を可能にします。
  • ログインページ
    • ログインページも作成します。最終的にはログイン中のユーザーの見積のみ表示するように実装します。
  • 会員登録ページ
    • 会員登録ではメールアドレス、ユーザー名、パスワードを入力します。
  • プロフィール編集ページ
    • このページで見積に表示される自分の情報を編集できるようにします。

テーブル定義

見積テーブルと商品テーブルを作成します。二つのテーブルの関係性は見積一つに対し商品が多数紐づく「一対多」にします。

見積テーブル
ID id
タイトル title
納入場所 location
取引方法 transaction
有効期限 effectiveness
宛先 customer
納入期限 deadline_at
見積日 estimated_at
商品テーブル
ID id
見積ID estimate_id
商品名 name
単位 unit
数量 quantity
単価 unit_price
備考 other

見積一覧ページの作成

環境構築ができていてLaravelの初期画面が表示されている前提で進めます。

データベースの接続設定

まずは接続設定を.envで行います。estimateというデータベースを作成しています。環境構築にはHomesteadを使用しました。

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=estimate
DB_USERNAME=homestead
DB_PASSWORD=secret

マイグレーションファイルとモデルクラスの作成

$ php artisan make:migration create_estimates_table --create=estimates

作成されたファイルに記入していきます。

create_estimates_table.php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateEstimatesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('estimates', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title', 100)->nullable();
            $table->string('location', 100)->nullable();
            $table->string('transaction', 100)->nullable();
            $table->string('effectiveness', 100)->nullable();
            $table->string('customer', 100)->nullable();
            $table->string('deadline_at', 100)->nullable();
            $table->date('estimated_at')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('estimates');
    }
}

テーブル名はestimatesとしました。格納したい物の名前の複数形にするのが一般的です。また、見積作成の途中で保存したい時や記入せずに作成したい場合に対応するため、nullable()でカラムにNULL値を許容しました。

マイグレーションを実行します。

$ php artisan migrate

次にモデルクラスを作成します。

$ php artisan make:model Estimate

appディレクトリにEstimateモデルが作成されます。

Estimate.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Estimate extends Model
{
    //
}

Estimateモデルに記述はしていませんが、継承元であるModelクラスで様々な設定を読み取ってくれるらしいです。

これでデータを扱う準備ができたのですが、テストデータが入っていた方がコントローラーを書きやすいので、Seederを用いてデータを挿入します。

$ php artisan make:seeder EstimatesTableSeeder

runメソッドの中にデータを挿入するコードを記述します。ここでは3つの見積を作りました。

EstimatesTableSeeder.php
<?php

use Carbon\Carbon;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class EstimatesTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $user = DB::table('users')->first();

        $titles = ['2021年おめでとうセール', '商品見積の件', 'サンプル見積の件'];
        $customers = ['株式会社XXX', '株式会社YYY', '株式会社ZZZ'];

        foreach (array_map(NULL, $titles, $customers) as [ $title, $customer ]) {
            DB::table('estimates')->insert([
                'title' => $title,
                'user_id' => $user->id,
                'customer' => $customer,
                'created_at' => Carbon::now(),
                'updated_at' => Carbon::now(),
            ]);
        }
    }
}

コマンドラインで実行します。

$ php artisan db:seed --class=EstimatesTableSeeder

「Database seeding completed successfully.」と返ってきたら成功です。

ルーティングの設定

web.php
Route::get('/estimates', 'EstimateController@index')->name('estimates.index');

コントローラークラスの作成

コントローラークラスはコマンドラインから作成。

$ php artisan make:controller EstimateController

作成されたEstimateController.phpにindexメソッドを追加します。

EstimateController.php
use App\Estimate; // ★ 追加

    public function index()
    {
        $estimates = Estimate::all();

        return view('estimates/index', [
            'estimates' => $estimates,
        ]);
    }

view関数でテンプレートにデータを渡し、その結果を返却しています。view関数の第一引数がテンプレートファイル名で第二引数がテンプレートに渡すデータです。

テンプレートの作成

テンプレートファイルを作成します。

$ mkdir resources/views/estimates
$ touch resources/views/estimates/index.blade.php
$ touch resources/views/layout.blade.php

layout.blade.phpとindex.blade.phpの中身は以下のように記述しました。

layout.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="csrf-token" content="{{ csrf_token() }}">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>見積作成アプリ</title>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
  <link rel="stylesheet" href="{{ asset('css/app.css') }}">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
</head>
<body>
  <header>
    <nav class="navbar navbar-expand-xs navbar-dark bg-dark p-1">
      <a class="navbar-brand" href="{{ route('estimates.index') }}">見積作成アプリ</a>
    </nav>
  </header>
  @yield('content')
  <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
  @yield('scripts')
</body>
</html>
index.blade.php
@extends('layout')

@section('content')
<main>
  <div class="container">
    <div class="row">
      <div class="col col-md-12">
        <h2 class="text-center" style="padding-top:25px">見積一覧</h2>
        <table class="table table-bordered table-hover" style="table-layout:fixed;">
          <thead class="thead-dark">
            <tr>
              <th class="col">タイトル</th>
              <th class="col">見積もり期日</th>
              <th class="col">場所</th>
              <th class="col">宛先</th>
            </tr>
          </thead>
          <tbody>
            @foreach($estimates as $estimate)
              <tr>
                <td class="position-relative">
                  <a href="{{ route('estimates.edit', ['estimate' => $estimate->id]) }}" class="stretched-link">
                    {{ $estimate->title }}
                  </a>
                </td>
                <td>{{ $estimate->estimated_at }}</td>
                <td>{{ $estimate->location }}</td>
                <td>{{ $estimate->customer }}</td>
              </tr>
            @endforeach
          </tbody>
        </table>
      </div>
    </div>
  </div>
</main>
<footer class="fixed-bottom bg-dark">
  <nav class="my-navbar">
    <div class="container">
      <div class="row">
        <div class="col-md-3">
          <a href="#">
            <button>新規作成</button>
          </a>
        </div>
        <div class="col-md-3 offset-md-6">
          <a href="#">
            <button>プロフィール設定</button>
          </a>
        </div>
      </div>
    </div>
  </nav>
</footer>
@endsection

テンプレートの中でも@を付ければPHPのようにforeachを使えます。この際、コントローラーから渡された$estimatesを参照しています。変数の値の展開は{{ }}のように波括弧二つで実現します。

CSSフレームワークにはBootstrapを使用しました。

見積編集ページの作成

商品テーブルの作成

まずは見積テーブルと同様にマイグレーションファイルを作成します。

$ php artisan make:migration create_items_table --create=items

マイグレーションファイルを記述します。

create_items_table.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateItemsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('items', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('estimate_id')->unsigned();
            $table->string('name', 100)->nullable();
            $table->string('unit', 10)->nullable();
            $table->integer('quantity')->nullable();
            $table->integer('unit_price')->nullable();
            $table->string('other', 100)->nullable();
            $table->timestamps();

            $table->foreign('estimate_id')->references('id')->on('estimates')->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('items');
    }
}

ここでは外部キー制約を設定しています。外部キー制約は他のテーブルとの結びつきを表現するためのカラムに設定します。外部キー制約が設定されたカラムには、好き勝手な値は入れられなくなります。今回の例で言うと、商品テーブルの見積ID列には実際に存在する見積IDの値しか入れることができなくなります。これによりデータの不整合を防ぎます。また、onDelete('cascade')により見積テーブルのデータを削除した場合、商品テーブル内の一致するデータを自動的に削除してくれます。

マイグレーションを実行します。

$ php artisan migrate

続けて商品テーブルに対応するモデルクラスを作成します。

$ php artisan make:model Item
Item.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Item extends Model
{
    //
}

テストデータを挿入するためにシーダーを作成します。

$ php artisan make:seeder ItemsTableSeeder
ItemsTableSeder.php
<?php

use Carbon\Carbon;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class ItemsTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $names = ['ガードレール', 'エムコール', '塩化カルシウム'];
        $units = ['式', '袋', '袋'];
        $quantities = [1, 10, 25];
        $unit_prices = [150000, 5000, 1000];

        foreach (array_map(NULL, $names, $units, $quantities, $unit_prices) as [ $name, $unit, $quantity, $unit_price ]) {
            DB::table('items')->insert([
                'estimate_id' => 1,
                'name' => $name,
                'unit' => $unit,
                'quantity' => $quantity,
                'unit_price' => $unit_price,
            ]);
        }
    }
}

今回はID=1の見積に対して3つの商品を登録しました。

$ php artisan db:seed --class=ItemsTableSeeder

ルーティングの設定

web.php
Route::get('/estimates/edit', 'EstimateController@showEditForm')->name('estimates.edit');
Route::post('/estimates/edit', 'EstimateController@edit');
Route::get('/estimates/create', 'EstimateController@create')->name('estimates.create');
Route::post('/estimates/create', 'EstimateController@create');

コントローラーの作成

コントローラーを書いていきます。既存の見積もりを編集する場合はそのままshowEditFormへ、新規作成の場合はcreate->showEditFormと推移します。

EstimateController.php
use App\Item; // ★ 追加

    public function showEditForm(Request $request)
    {
        $estimate_id = $request->input('estimate');
        $estimate = Estimate::find($estimate_id);

        return view('estimates/edit', [
            'estimate' => $estimate,
        ]);
    }

    public function create()
    {
        $estimate = new Estimate();
        $estimate->save();

        return redirect()->route('estimates.edit', [
            'estimate' => $estimate->id,
        ]);
    }

    public function edit(Request $request)
    {
        $estimate_id = $request->input('estimate');
        $current_estimate = Estimate::find($estimate_id);

        $current_estimate->title = $request->title;
        $current_estimate->location = $request->location;
        $current_estimate->transaction = $request->transaction;
        $current_estimate->effectiveness = $request->effectiveness;
        $current_estimate->customer = $request->customer;
        $current_estimate->deadline_at = $request->deadline_at;
        $current_estimate->estimated_at = $request->estimated_at;

        $current_estimate->save();

        return redirect()->route('estimates.edit', [
            'estimate' => $estimate_id
        ]);
    }

コントローラーメソッドの引数にRequestクラスのインスタンスを受け入れる記述をすることでユーザーの入力値をRequestクラスのインスタンス$requestに詰めて引数として渡してくれます。Requestクラスのインスタンスにはリクエストヘッダや送信元IPなどいろいろな情報が含まれていますが、その中にフォームの入力値も入っています。

$request->title;

リクエスト中の入力値は上記のようにプロパティとして取得することができます。

また、クエリパラメータの取得にはRequestクラスのinputメソッドを使用します。inputメソッドの第一引数へ、クエリパラメータのキーを指定します。今回指定するクエリパラメータのキーは'estimate'です。この為、inputメソッドの第一引数には'estimate'を指定します。

次のポイントはデータベースに書き込む処理です。データベースへの書き込みは以下の手順で実装します。

  1. モデルクラスのインスタンスを作成する。
  2. インスタンスのプロパティに値を代入する。
  3. saveメソッドを呼び出す。

これにより、モデルクラスが表すテーブルに対してINSERTが実行されます。

次回

ここまで、見積一覧ページと見積編集ページのコントローラーを作成しました。次回はVue.jsを利用した見積編集ページのテンプレートを作成します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】Vue.jsをつかむ①

私のアウトプットです。

Vue.jsも人気のフロントエンドフレームワークのはずですが
なぜかReact推しであるブログ記事やYouTubeが多いので
私自身、Vue.jsを学習する上で、理解しておきたいことをまとめました。

今回は、敢えてv2.6.11を前提に投稿いたします。

コード全体

index.html
<html>

<head>
  <title>Hello Vue</title>
 <!-- (1) CDNからのVueの読み込み -->
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script>
</head>

<body>
 <!-- (5) Vueインスタンスの有効範囲ここから -->
  <div id="app">
    <!-- (6) マスタッシュ構文 -->
    <p>Hello {{world}}</p>  
    <p>Counter: {{count}}</p>
    <!-- (7) v-ifディレクティブ -->
    <p v-if="count == 5">見えました!</p> 
     <!-- (8) v-modelディレクティブ -->
    <input v-model="world"><br>
    <input type="number" v-model="count" />
  </div>
  <script>
    // (2) Vueインスタンスの作成
    new Vue({
      el: "#app",  // (3) elプロパティ
      data() {     // (4) data()メソッド
        return {
          world: "Vue",
          count: 0
        }
      }
    })
  </script>
</body>

</html>

Vue.jsの読み込み

index.html
<!-- (1) CDNからのVueの読み込み -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script>

(1)は、CDNで公開されているVue.jsを読み込んで実行する為のコードです。
プロトタイピングや学習を目的とする場合は、このCDNを使用するのが良さそうです。

Vueインスタンスの有効範囲

<body>の中の<script></script>を見ます。

index.html
  <script>
    // (2) Vueインスタンスの作成
    new Vue({
      el: "#app",  // (3) elプロパティ
      data() {     // (4) data()メソッド
        return {
          world: "Vue",
          count: 0
        }
      }
    })
  </script>

(2) は、new Vue()でVueインスタンスを作っています。
引数に渡しているオブジェクトがVueのコンポーネントになっています。
このオブジェクトを見ていくと、elプロパティ(3)と、data()メソッド(4)があります。

elプロパティはDOMに対して指定した値に該当するエレメントを見つけて
対象のエレメントが見つかった時にVue.jsのインスタンスとHTMLをマッピングするセレクタです。

data()メソッドはオブジェクトを返すメソッドとして定義しています。
このときのオブジェクトが持つプロパティ((4)では、worldとcount)をVue.jsは監視し続けます。
この監視対象が変化したとき、必要に応じて画面への反映を即座に行ってくれます。

DOM上での変数展開

<body>の中の<div id="app"></div>を見ます。

index.html
 <!-- (5)Vueインスタンスの有効範囲ここから -->
  <div id="app">
    <!-- (6) マスタッシュ構文 -->
    <p>Hello {{world}}</p> 
    <p>Counter: {{count}}</p>
    <!-- (7) v-ifディレクティブ -->
    <p v-if="count == 5">見えました!</p> 
     <!-- (8) v-modelディレクティブ -->
    <input v-model="world"><br>
    <input type="number" v-model="count" />
  </div>

これがVueインスタンスが有効な範囲です。
このid="app"で先ほど作ったVueインスタンスとHTMLをマッピングしていきます。
Vueインスタンスのelプロパティ指定と併せて、
別のidを指定したり、classで指定することも可能です。

<p>Hello {{world}}</p>(6)で使われている{{}}という構文はマスタッシュ構文と言います。
見た目が『口ひげ』に似ているからだそうです。
この中ではdata()の中のプロパティにアクセスができ、
data()の中にあるプロパティが変化すると、それに応じた結果が即座に反映されます。

v-if

index.html
<!-- (7) v-ifディレクティブ -->
<p v-if="count == 5">見えました!</p> 

v-で始まる属性をVue.jsではディレクティブと言います。
このv-if条件付きレンダリングと呼ばれるディレクティブで、
v-ifの後に記述する条件に一致していればDOMにレンダリングされ、
不一致ならDOM上から消えるようになっています。
(言わば、『if文』ですね。)
今回の場合はcountプロパティの値が5の場合に
『見えました!』と表示されることになります。

v-model

index.html
<!-- (8) v-modelディレクティブ -->
<input v-model="world">

これは、双方向バインディングと言って、
<input>タグの様な入力を受け付けるタグに対して
data()プロパティを指定できます。
つまり、<input v-model="world">の場合は、
テキストボックスに値を入力すると
その値がdata()worldに設定されることになります。
これにより、入力フォームのinputイベントを監視して、
data()のプロパティへ随時反応されていくというコードになります。
なお、Vue.jsではv-modelによってフォームと対応付けられているプロパティが認識されている為
識別用のクラスや名前を付与しなくても値の読み取りが可能です。

まとめ

(1)CDNからのVueの読み込み

CDNで公開されているVue.jsを読み込んで実行する為のコードです。

(2)Vueインスタンスの作成

new Vue()でVueインスタンスを作っています。

(3)elプロパティ

DOMに対して指定した値に該当するエレメントを見つけて
対象のエレメントが見つかった時にVue.jsのインスタンスとHTMLをマッピングするセレクタです。

(4)data()メソッド

オブジェクトを返すメソッドです。

(5)Vueインスタンスの有効範囲

id="app"でVueインスタンスとHTMLをマッピングしていきます。

(6)マスタッシュ構文

<p>Hello {{world}}</p>で使われている{{}}です。

(7)v-ifディレクティブ

v-ifの後に記述する条件に一致していればDOMにレンダリングされ、
不一致ならDOM上から消えるようになっています。

(8)v-modelディレクティブ

<input>タグの様な入力を受け付けるタグに対してdata()プロパティを指定できます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

わかりにくいvue.jsのwatch(ウォッチャ)のオプションを使った書き方

概要

vue.jsのwatchにはdeepとimmediateという2つのオプションがあるのですが、書き方がちょっと特殊なのと、マニュアル上で探しにくくいので、癖があります。
毎回ググってしまうので備忘録として残します。

※2019/9にブログに書いていた記事からの転記です

watchとは

vueのドキュメントの説明
vueのドキュメントの説明(API)

普通は算出プロパティ(computed)でいいんだけど、複雑な処理(重い処理?)の時はこっちの方が良いよ〜との事。

オプション

オプションについてはなぜかちょっと遠いところで、APIの中に記載があります。

[vueのドキュメントの説明]https://jp.vuejs.org/v2/api/index.html#vm-watch

deep

通常、objectのプロパティの変更は、watchが発火しません。

  data: () => ({
    someObject: {},
  )},
  watch: {
    someObject(newVal, oldVal) {
       console.log(newVal);
    },
  }
  methods: {
    onClick: function() {
      // watchが発火しない!
       someObject.hoge = 'hoge';
    },

deepをtrueにする場合はこう

  data: () => ({
    someObject: {},
  )},
  watch: {
    someObject: {
        deep: true,
        handler(newVal, oldVal) {
          console.log(newVal);
       },
    }
  }
  methods: {
    onClick: function() {
      // watchが発火する
       someObject.hoge = 'hoge';
    },

handlerという関数を書かないといけないのが唐突なので、いつも書き方忘れる、、、、

immediate

初期化のタイミングでもwatchが発火するようにしたい場合に使います。

これも、

  data: () => ({
    someObject: {hoge: 'piyo'},
  )},
  watch: {
    someObject: {
        immediate: true,
        handler(newVal, oldVal) {
          console.log(newVal);
       },
    }
  }

という感じで、handler関数にいつもの処理を書いてあげる必要があります。

所感

なんでここだけマニュアルわかりにくいんだろ、、、

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js 3 入門 「Vuex」

はじめに

Vue.js 3 の Vuex について、自分が学んだことを備忘録として記載します。
Vue.js に殆ど触れたことが無い方に少しでも参考になれば幸いです。
誤り等あれば、ご指摘頂けますと大変喜びます

Vuex とは

Vuexはアプリで利用するデータを、一箇所に集中管理するためのライブラリです。
管理だけではなく、データの操作(更新)方法も標準化することができます。コンポーネント間でのデータ共有がシンプルに実現できるようになりますね。

今回のお題

今回は、数字をカウントできる簡単なアプリを作成してみます。

image.png

プロジェクトの作成

まずは Vue CLI を用いてプロジェクトを作成します。
Vue CLI についてはこちらの記事を参照してください。

プロジェクトを作成するには、作成したいフォルダで以下のコマンドを実行します。
hello-vuexはプロジェクト名です。任意のプロジェクト名を設定してください。

cd 任意のフォルダ
vue create hello-vuex

プリセットの選択

すると、以下のように利用するプリセット(プロジェクト設定)の選択を求められます。
まずは最低限の構成とするので「Manually select features」(手動で選択)を選択します。
versionはご自身のバージョンに読み替えてください。

Vue CLI v4.5.9
? Please pick a preset:
  Default ([Vue 2] babel, eslint)
  Default (Vue 3 Preview) ([Vue 3] babel, eslint)
> Manually select features    

プロジェクトに組み込むモジュールを選択

プロジェクトに組み込むモジュールを選択します。
ここでBabelLinterに加えて、Vuexを選択します。
[Space]キーで選択することができ、[Enter]キーで確定となります。

Vuexを選択することによって、Vuexというアプリで利用するデータの、集中管理機能を提供するライブラリが組み込まれます。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project:
 (*) Choose Vue version
 (*) Babel
 ( ) TypeScript
 ( ) Progressive Web App (PWA) Support
 ( ) Router
>(*) Vuex
 ( ) CSS Pre-processors
 (*) Linter / Formatter
 ( ) Unit Testing
 ( ) E2E Testing                                                                                                                                                                                                                                                                   

Vue.js のバージョンを選択

Vue.js のバージョンを選択します。
本記事では 3.x を選択します。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Vuex, Linter
? Choose a version of Vue.js that you want to start the project with
  2.x
> 3.x (Preview)                                                                                                                                                                                              

Linter の設定を選択

Linter の設定を選択します。
今回は最低限のESLint with error prevention only(エラー防止のみ)を選択します。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Vuex, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Pick a linter / formatter config: (Use arrow keys)
> ESLint with error prevention only
  ESLint + Airbnb config
  ESLint + Standard config
  ESLint + Prettier                                                                                                                                                                                                                                                                                                          

続けて、Lintの実行タイミングの選択を求められます。
Lint on save(保存時)を選択します。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Vuex, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Pick a linter / formatter config: Basic
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)
>(*) Lint on save
 ( ) Lint and fix on commit      

設定情報の格納先を選択

BabelESLintの設定情報を個別の設定ファイルとするか、package.jsonにまとめるかを選択します。
個別の設定ファイルとしたほうが綺麗なのでIn dedicated config filesを選択します。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Vuex, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
> In dedicated config files
  In package.json 

今回の設定を保存しておくかを選択

今回の設定を保存しておくかを選択します。
今回はあくまでお試しなのでN(保存しない)とします。

Vue CLI v4.5.9
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Vuex, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? (y/N) N                                                                                                                         

プロジェクトの生成開始

ここまでの設定内容を元に、プロジェクトの生成が開始されるので、完了するまで待機します。
正常に完了すると、以下のような文言が表示されます。

Vue CLI v4.5.9
Creating project in 任意のフォルダ\hello-vuex.
Installing CLI plugins. This might take a while...

途中省略...

Running completion hooks...

Generating README.md...

Successfully created project hello-vuex.
Get started with the following commands:

 $ cd hello-vuex
 $ npm run serve

生成されたフォルダを確認

カレントフォルダに、指定したプロジェクト名のフォルダが生成されています。

image.png

アプリの実行

早速実行してみましょう。
上記のプロジェクト生成完了時の文言(Get started with the following commands:)にある通り、以下のコマンドを実行します。
プロジェクトルートに移動して、開発用のサーバーを実行するコマンドです。

cd hello-vuex
npm run serve

以下のような文言が表示されれば、開発用のサーバーが起動できています。
ブラウザを起動しhttp://localhost:8080にアクセスしてください。

  App running at:

途中省略...

  Note that the development build is not optimized.
  To create a production build, run npm run build.

動作確認

以下のような画面が表示されれば、プロジェクトの作成は成功です。
開発用サーバーは[Ctrl] + [C]で終了することができます。

image.png

ストアの定義

まずはストアを定義します。
ストアは主に、データと、データを更新するためのメソッド で構成されます。プロジェクトを作成した時点で、空のストア定義(/src/store/index.js)が用意されているので、こちらを編集していきます。

/src/store/index.js

import { createStore } from 'vuex'

export default createStore({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

ストアはcreateStoreメソッドで定義することができます。

createStoreメソッド

引数

  • defs
    • ストアを構成する要素(ステート、ミューテーション、etc...)を要素名:定義の形式で記述します
    • 複数の要素を定義する際は区切り

ステートの定義

ステートとは、ストアで管理されるデータの本体です。
ステートで管理すべき情報を名前: 初期値の形式で定義します。(複数の場合は区切り)

今回は、カウンターを示すcountを定義します。

/src/store/index.js

import { createStore } from 'vuex'

export default createStore({
  state: {
    count: 0 //追加
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

ミューテーションの定義

Vuexでは、ステートを専用のメソッド経由で更新します。
ステートの更新フローが限定されるので、コードの見通しが良くなります。

このような、ステートを更新するためのメソッドのことを、ミューテーションと呼びます。
今回はステートcountを1増やすincrementメソッドと
countを1減らすdecrementメソッドを定義します。

ミューテーションは引数にステート(state)を受け取るので、実際の各情報にはstate.名前とするとアクセスできます。

import { createStore } from 'vuex'

export default createStore({
  state: {
    count: 0
  },
  mutations: {
    increment(state){
      state.count += 1
    },
    decrement(state){
      state.count -= 1
    }
  },
  actions: {
  },
  modules: {
  }
})

ストアの有効化

なお、ストアは/src/main.jsで有効化されています。

import { createApp } from 'vue'
import App from './App.vue'
import store from './store'

createApp(App).use(store).mount('#app')

Vueインスタンスにライブラリを組み込むuseメソッドに、定義したストア(store)を渡すことで有効化しています。

ストアにアクセスする

ステートの表示

実際にコンポーネントからストアにアクセスしてみましょう。

まずはストアに定義したステートcountの値を画面に表示してみます。
ステートには、this.$store.state.データの名前でアクセスできます。
/src/App.vueを以下のように修正します。

<template>
  {{count}}
</template>

<script>
export default {
  name: 'App',
  computed:{
    count(){
      return this.$store.state.count
    },
  }
}
</script>

実際の画面を確認すると、初期値に指定した0が表示されています。
image.png

ミューテーションの呼び出し

次に、ステートcountの値を増減できるようにしてみます。
ステートを増減させるには、先程定義したミューテーションを呼び出します。
ミューテーションはthis.$store.commit(ミューテーション名)で呼び出すことができます。
/src/App.vueを以下のように修正します。

<template>
  <input type="button" v-on:click="ondecrement" value="-" />
  {{count}}
  <input type="button" v-on:click="onincrement" value="+" />
</template>

<script>
export default {
  name: 'App',
  computed:{
    count(){
      return this.$store.state.count
    },
  },
  methods:{
    onincrement(){
      this.$store.commit('increment')
    },
    ondecrement(){
      this.$store.commit('decrement')
    }
  }
}
</script>

実際の画面を確認すると、[+]ボタンと[-]ボタンが増えており、
ステートの値を増減させることができるようになりました。

image.png

以上となります。
ありがとうございました。
他の機能(ゲッター/アクション/etc...)については別の記事にします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascriptの知識ゼロからvue.jsで業務がこなせるようになるまでのロードマップ

プログラミング未経験からベンチャー企業に入社して半年の自分がvue.jsを実務で使えるようになるまでの学習手順について紹介します。vue.jsを初めて使った時ってmethods?computed?何それ?って状態だと思うんですよ。そういう初歩的な疑問をいち早くなくせる人が1人でも増えればいいなーと思ってます。

前提知識

・エディタが使える(vscodeとか)
・HTML+CSSでコーティングがある程度できるレベル

目標

vue.jsを使ってこのサイトのような感じで作成できるようにする

Progate

javascriptを初めて触るならまずここから。
基本的にはカリキュラム通り進めればいいんですが、個人的には3章の関数を学ぼう!までを理解できればいいかなーと。
理由は4章以降はクラスの概念とかファイルの分け方などブラウザに表示させる内容とは関係ないので人によっては学んだ感じがしないんですよね。最初はコードを書いたら色が変わった!動いた!っていう体験をした方がモチベーションは上がるのではないでしょうか?そのため3章までは頑張って理解して4章以降は流し見でいいと思います。

②確かな力が身につくJavaScript「超」入門 第2版

javascriptの概念がなんとなくわかった所でもう少し深堀り。
色んな本も見てきましたがカラフル、わかりやすい、サンプルコードつきの3拍子でこの本をおすすめします。
何よりコードを書いたらすぐブラウザに表示されるので勉強してて楽しいです。

③HTML + CSS + javascriptを使ったサイト作成

ここで実際にアウトプットしてみましょう。
何作っていいかわからない!という人は模写でも大丈夫です。
GODIVAのマウスを重ねたらメニューが出るぐらいで大丈夫だと思います。
スクリーンショット 2020-12-19 8.58.21.png

④いちばんやさしい Vue.js 入門教室

javascriptの知識がある程度身についたらvue.jsにシフトしましょう。
vue.jsの本いろいろ読みましたがこの本が一番わかりやすかったです。
なぜならサンプルコード1行1行丁寧に書いてるからです。
他の本だといきなりライフサイクルフックなど難しいワードが出てくるので挫折してしまう可能性があります。

⑤vue.jsを使ったサイト作成

Vue CLIを使って実際にサイトを作成。
ニュース一覧などをv-forで繰り返し表現できるとGOOD!
vue.jsの理解を深めるためcssのhoverやfocusといった擬似要素は封印してみましょう。
実際に自分はこのサイトを参考に作成しました。

Q&A

サイト作成で挫折した

いい証拠です。足りない知識がわかっているならググるかインプットし直しましょう。それでも挫折しそうなら作成するサイトのレベルを下げましょう。勉強は義務ではないので長続きしなければ意味ありません

他におすすめの教材はないの?

javascript、vue.jsも何冊か本読みましたが2冊目以降はあまり意味なかったなーと思います。
インプットする時間をいち早くアウトプットに回しましょう。

さて、続きまして、アルサーガパートナーズ Advent Calendar 2020 24日目の記事は @miumi さんの学習メンタル術です。:tada:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラミング初学者の現役医師が、firebase使って診断名を管理できるLINEBOTを実装してみた。

皆さん、ご自身の病名(医学的には診断名)を覚えていますか?

僕の本職は医師(開業医)です。
日常診療の課題感から、以下のような、『診断名を保管でき、かつ、必要に応じて引き出せる』アプリを実装しました。
動作は以下です。

医師として、月曜から土曜まで毎週700人以上の患者さまを診察させていただいております。
外来診療を行う中で、「多くの方がご自身の診断名を覚えていらっしゃらない」ことに課題感を持っていました。
診断名がわからないと、次の処置を行っても良いものか、非常に悩む瞬間があるからです。
でも例えば、「好酸球性多発血管炎性肉芽腫症」と診断されても、「飛影邪王炎殺黒龍波」(分かる人いるかな?)みたいに難しく、なかなか覚えられるものではありません。
そこで、上述の『診断名を保管でき、かつ、必要に応じて引き出せる』アプリを実装しました。
システム概要は以下です。
protooutstudio.001.jpeg
1-1 webappか診察券番号でIDを設定し病名の登録
1-2 webappで診察券番号で病名の確認
2-1 LINEBOTから診察券番号でIDを設定し病名の登録
2-2 LINEBOTで診察券番号で病名の確認
それぞれについて確認します。

下準備

必要なした準備は以下の2つ
① VScordのインストール
② firebaseの設定
上記はgoogle先生に譲ります。

Webappの実装

今回のwebappはvue.jsで実装します。

webappから病名の登録

UIは以下のようになります。
スクリーンショット 2020-12-23 7.36.27.png

addMydg.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>リアルタイムにデータ取得</title>
</head>

<body>
  <div id="app">
    <p>
      診察券番号:<input v-model="cardId" placeholder="00000000"><br>
      名前:<input v-model="name" placeholder="名前"><br>
      誕生日:<input type="date" v-model="birthday" placeholder="1900/1/1"><br>
      性別:<input v-model="gender" placeholder="男or女"><br>
      診断名:<input v-model="dgname" placeholder="診断名"><br>
      診断日:<input type="date" v-model="dgday" placeholder="1900/1/1"><br>
      <button v-on:click='post'>送信</button><br>
      <a href="https://unruffled-curran-e76f78.netlify.app/searchmydg">登録確認のために診断名検索ページに移動する</a>
    </p>
  </div>

  <script src="https://unpkg.com/vue"></script>
  <script src="https://www.gstatic.com/firebasejs/7.2.3/firebase-app.js"></script>
  <script src="https://www.gstatic.com/firebasejs/7.2.3/firebase-firestore.js"></script>
  <script>
    // firebaseの設定から下記を記入
    const firebaseConfig = {
    apiKey: "",
    authDomain: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: "",
    appId: "",
    measurementId: ""
    };
    // Initialize Firebase
    firebase.initializeApp(firebaseConfig);
    const db = firebase.firestore();
    let patient = '';
    let patientId = '';

    const app = new Vue({
      el: '#app',
      data: {
        allData: [],
        cardId:'',
        name: '',
        birthday: '',
        gender: '',
        dgname: '',
        dgday: '',
      },
      methods: {
        //データ追加
        post: async function () {
          patient = this.cardId
          const res = await db.collection("medicalRecord1").doc(patient).set({
            name: this.name,
            birthday: this.birthday,
            gender: this.gender,
            diagnosis: {dgday:this.dgday, dgname:this.dgname},
            cardId:this.cardId
          });
          console.log(patient);
      },
      }
    })
  </script>

</body>

</html>

webappで病名の確認

UIは以下のようになります。
スクリーンショット 2020-12-23 7.41.57.png

search.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>診断名検索</title>
</head>

<body>
  <div id="app">
    <h1>診断名検索</h1>
    <input v-model:value="cardID" placeholder="診察券番号"><br>
    診察券番号:{{ cardID }}
    <button v-on:click="searchDg">検索</button><br>
    <p>
    氏名:{{ name }}<br>
    診断名:{{ diagnosis }}<br>
    診断日:{{ diagnosisday }}
  </p>
    <button v-on:click="clearAll">全てをクリア</button><br>
    <a href="https://mystifying-tesla-bb64e7.netlify.app/addmydg">診断名登録ページに移動する</a>
  </div>

  <script src="https://unpkg.com/vue"></script>
  <script src="https://www.gstatic.com/firebasejs/7.2.3/firebase-app.js"></script>
  <script src="https://www.gstatic.com/firebasejs/7.2.3/firebase-firestore.js"></script>

  <script>
    // firebaseの設定から下記を記入
    const firebaseConfig = {
    apiKey: "",
    authDomain: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: "",
    appId: "",
    measurementId: ""
    };
    // Initialize Firebase
    firebase.initializeApp(firebaseConfig);
    const db = firebase.firestore();

    const app = new Vue({
    el: '#app', // Vueが管理する一番外側のDOM要素
    data: {
      // Vue内部で使いたい変数は全てこの中に定義する
      cardID:'',
      name:'',
      diagnosis: '',
      diagnosisday:'',
    },
    methods: {
      // 関数はここに記入
      searchDg: async function() {
        console.log('次の診察券番号の患者さまが検索されました:', this.cardID);
        const patientData = await db.collection("medicalRecord1").doc(this.cardID).get();
        console.log(patientData.date);
          //データの格納
        const pData = patientData.data();//繰り返し各箇所なので一回定数に
      this.name = pData.name;
      this.diagnosis = pData.diagnosis.dgname;
         this.diagnosisday = pData.diagnosis.dgday;
      },
      clearAll: function() {
       this.cardID = '';
       this.name = '';
       this.diagnosis = '';
       this.diagnosisday = '';
      console.log('全てのToDoが消去されました');
    },
    },
});
  </script>
</body>
</html>

LINEBotから病名の登録
リッチメニューを設定し、病名登録のwebappがLINEの中で立ち上がるように設定しました。
同様に、病名確認のwebappも立ち上がるように設定しました。

LINEBotで病名の確認
診断名を確認する場合、“診断名確認”とLINEBotに送ると、“診察券番号を教えてください”と返信させ、診察券番号を送ると名前・診断名・診断日を返すように設定しました。

まずは、必要なパッケージの導入
以下のコードを順番にターミナルに入力

ターミナルコマンド
$ npm init -y
ターミナルコマンド
$ npm i express
ターミナルコマンド
$ npm i @line/bot-sdk
ターミナルコマンド
$ npm i firebase

今回作成したコードは以下の通り。

searchMydg.js
'use strict'; 

// ########################################
//               初期設定など
// ########################################

// パッケージを使用します
const express = require('express');
const line = require('@line/bot-sdk');
const firebase = require("firebase/app");
require("firebase/firestore");

// firebaseの設定から下記を記入
const firebaseConfig = {
    apiKey: "",
    authDomain: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: "",
    appId: "",
    measurementId: ""
    };
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
const db = firebase.firestore();

// ローカル(自分のPC)でサーバーを公開するときのポート番号です
const PORT = process.env.PORT || 3000;

// Messaging APIの設定から記入
const config = {
    channelSecret: '',
    channelAccessToken: ''
};

// ########################################
//  LINEサーバーからのWebhookデータを処理する部分
// ########################################

// LINE SDKを初期化します
const client = new line.Client(config);

// LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます
async function handleEvent(event) {
    // 受信したWebhookが「テキストメッセージ以外」であればnullを返すことで無視します
    if (event.type !== 'message' || event.message.type !== 'text') {
        return Promise.resolve(null);
    }
    //診断名検索というテキストを受け取ったら診察券番号を確認するリプライを出す
    if (event.message.text == '診断名確認') {
        return client.replyMessage(event.replyToken, {
        type: 'text',
        text: '診察番号を入力してください'
    });
    }
    //cardIDの中に審査券番号が格納
    const cardID = event.message.text;
    console.log(cardID);

    //診察券番号からfirebase上の診断名を検索
    const patientData = await db.collection('medicalRecord1').doc(cardID).get();
    console.log(patientData.data());
    const pData = patientData.data();
    if (pData != undefined) {
        const name = pData.name;
        const diagnosis = pData.diagnosis.dgname;
        const diagnosisDay = pData.diagnosis.dgday;
        let msg = '';
        msg = 'お名前:'+ name +  '\n' +'診断名:' + diagnosis +  '\n' +'診断日:' + diagnosisDay
        console.log(msg);
        const message = {
            type: 'text',
            text: msg
        };
        client.replyMessage(event.replyToken, message);
    }else{
        const message = {
            type: 'text',
            text: '再度診察券番号を記入してください。'
        };
        client.replyMessage(event.replyToken, message);
    };
}


// ########################################
//          Expressによるサーバー部分
// ########################################

// expressを初期化します
const app = express();

// HTTP GETによって '/' のパスにアクセスがあったときに 'Hello LINE BOT! (HTTP GET)' と返事します
// これはMessaging APIとは関係のない確認用のものです
app.get('/', (req, res) => res.send('Hello LINE BOT! (HTTP GET)'));

// HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします
app.post('/webhook', line.middleware(config), (req, res) => {
    // Webhookの中身を確認用にターミナルに表示します
    console.log(req.body.events);

    // 空っぽの場合、検証ボタンをクリックしたときに飛んできた"接続確認"用
    // 削除しても問題ありません
    if (req.body.events.length == 0) {
        res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します
        console.log('検証イベントを受信しました!'); // ターミナルに表示します
        return; // これより下は実行されません
    }

    // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、
    // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します
    Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result));
});

// 最初に決めたポート番号でサーバーをPC内だけに公開します
// (環境によってはローカルネットワーク内にも公開されます)
app.listen(PORT);
console.log(`ポート${PORT}番でExpressサーバーを実行中です…`);

今後の課題

診断名を一つではないので、診断名をリスト化し、LINEBotに返せる設定を実装したいと思いました。
もう少し深めて行く予定です。

その他の記事

近すぎると小池都知事が『密です。』と連呼するデバイスを作ったら腹筋が崩壊したので、皆さんにも試して欲しい。

誰が使うかわからないけど、膝のレントゲン写真を送ったら、その膝がどの程度痛んでいるのか教えてくれるラインbotを作ってみた。

猪木の名言で元気をくれるbotを作ったら、想定外の応答で笑いが止まらなくなったから、ぜひ試して欲しい。

twitterが使えないというIntegromatの弱点を克服し、最強化する方法を書くので試して欲しい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

V-forとV-showで特定の要素だけclassをつける

やりたいこと

DBからひっぱってきたデータをvueのコンポーネントの中のdataにセットし、それをv-forで表示させているとします。
DBからひっぱってきたデータのいずれかに真偽値を設定するなどしておけばv-forの中で動きの変化はつけることができますが、
「そのコンポーネントの中だけのdataを追加し、v-forの中の特定のコンテンツだけclassをつける」などの変化をつけたい場合にやる方法。

記述内容

//描写部分

<template>
 <div>
  <div v-for="task in tasks" :key="task.id">
   <div class="task__title">{{task.title}}</div>
  //例えばこのtask.bodyだけ、条件に応じてis-activeclassをつけたい場合
   <div class="task__body" v-bind:class="{'is-active':active === task}" >{{task.body}}
 </div>
 <button v-on:click="addActive(task)">bodyにclassをつける</button>

 </div>
</template>


<script>
export default {
 data(){
         return{
                tasks:[ ],//この中にdbからひっぱってきたデータが入っている
                active:null   
            }
        },
 methods:{
          addActive(task){
           if(this.active === task){
             this.active = null;
           }else{
            this.active = task;
           }
          }
        }
}
</script>

こんな感じで、v-forの中の値(task)を引数として渡すことで、そのactiveをtaskにすることができる。
これにより特定のtaskだけにclassを適用できる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vuetify+TypeScriptでちょっとリッチなmarkdownを

前書き

この記事は JSL (日本システム技研) Advent Calendar 2020 - Qiita 22日目の記事です。
Vueでマークダウン書きたいよ、という要望に応えるために色々やってみた備忘録になります。
なんか、あまりこの組み合わせのやってみた記事を見かけないけどなんで・・・??? :thinking:

環境

  • MacOS: Mojave v10.14.5
  • Node: v10.19.0
  • TypeScript: v3.9.7
  • Vue.js: v2.6.12
  • Vuetify: v2.3.21

楽をするために我々はnpmの奥地へと向かった...

vuetify-markdown-editor

なんかそれっぽいのが見つかりました。 vuetify-markdown-editor ですね。覗いてみましょうか・・・

おおぉ・・・、もう今回の要件ほとんど満たしているじゃありませんか。

Vuetifyコンポーネントをベースにしているし、リッチですし、TypeScriptにも対応しているしで至れりつくせりです。

もうこれでいいじゃん...と思いましたが、今後でてくるであろう要望をどこまで吸収できるのかが不安でした(使用例があんまり見つからない)

とはいえ、サクッと用意したいならこれを使わない理由はないのではないでしょうか?

vuetify-markdown-editor 以外にVuetifyをベースにしたものは見つかりませんでした。
今後の自由度考えて独自に実装しちゃおう、ということで本編です。車輪の再開発?

本編

markdown-itを導入してみる

色々調べてみて、 markdown-it が一番一般的っぽかったのでこれにしました。マークダウン形式で書かれたテキストをパースしてhtmlにしてくれるパッケージです。

導入に当たっては、VuejsでMarkdownを使うときの最強な組み合わせ を参考にさせていただきました。 markdown-it にはプラグインがたくさんあって、結構カスタムできるみたいです。

で、以下のプラグインを導入することにしました。(タイトルは「リッチな」とうたっていますし豪華に行きましょう)

  • highlight.js : コードのハイライトしてくれるやつ(重要)
  • markdown-it-container : ↓こういうのを出せるやつ
    SS 2020-12-22 23.30.31.png

  • markdown-it-emoji : 名前の通り :rolling_eyes: 出すためのやつ

  • markdown-it-imsize : 画像サイズを調整できるようにするやつ

  • markdown-it-ins : 文字に下線ひけるやつ

  • markdown-it-sanitizer : XSS対策でサニタイズできるやつ

  • markdown-it-sub : ↓のような表示をできるようにするやつ
    SS 2020-12-22 23.38.25.png

  • markdown-it-task-checkbox : チェックボックス出せるようにするやつ

基本は参考記事の通りで問題ありませんが、ここでTypeScript特有の問題が発生します。
そう、型定義です。

@types/hoge パッケージが配布されていればいいのですが、以下のパッケージにはありませんでした。
declare module hoge でanyにしてもいいのですが、一応ちゃんとやってみることにしました。(えらいぞ自分)

まぁやることは簡単で、ちゃんと .d.ts ファイルに型定義を書いてあげればよいです。(たぶん)

markdown-it-plugins.d.ts
declare module "markdown-it-task-checkbox" {
  import { PluginSimple, PluginWithOptions } from "markdown-it";
  declare const checkbox: PluginSimple | PluginWithOptions;
  export = checkbox;
}

この要領で他のやつの分も追記していきます。

SS 2020-12-22 23.49.32.png

エディタもちゃんと分かってくれました(型を)

とりあえず試してみる

参考資料を元に実装してみました。動かしてみましょう。

SS 2020-12-22 23.58.17.png

コードブロックすらまともに出ない・・・だと???? :thinking:
悪さをしているのはVuetifyのCSS、 <code> タグになんかあたってました。
ならばこちらもCSS書いて上書きしてやるまでです。

Markdown.vue
// 省略

<style lang="scss" scoped>
// 参考記事のCSSインポートはここでやることに
@import "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.11.1/katex.min.css";
@import "../../node_modules/highlight.js/styles/monokai.css";

.v-application .md ::v-deep {
  code {
    font-weight: initial;
    background-color: #2f2f2f;
    color: rgba(255, 255, 255, 0.9);
    font-size: 95%;
    display: inline-block;
    padding: 1px 0.5em 1px 0.5em;
    margin: 2px;
    margin-bottom: 14px;
  }

  pre code {
    display: block;
    padding: 0.5em 0.8em 0.5em 0.8em;
  }

  code:before,
  code:after {
    content: initial;
  }

  p {
    margin-bottom: 8px;
  }

  table {
    margin: 16px 0;
    color: #303030;
    width: auto;
    display: block;
    overflow-x: auto;
    border-collapse: collapse;

    th,
    td {
      padding: 10px;
      border: 1px solid #dbdbdb;
    }

    th {
      background-color: #f0f0f0;
      border-bottom: solid 2px #bfbfbf;
    }
  }
}

::v-deep は子コンポーネントにstyleを指定するためのセレクタです

ちょっと scss 書くの楽しくて他もいじっちゃいましたが許してください :bow:

これでどうでしょうか。

SS 2020-12-23 0.06.02.png

やりました。調教完了です

markdown-it-containerをVuetify標準にしよう

何もしないとこんなです

SS 2020-12-23 0.17.16.png

あれ、リッチとは???? :thinking:

その時思いました、Vuetifyのクラスをつけてあげれば勝手にVuetifyのデザインになるのでは、と。

早速やってみます。プラグインのオプションで変換処理を上書きして実現しようという魂胆です。Vuetifyの v-alart コンポーネントのクラスを拝借して・・・

markdownRender.ts
// 省略

type AlertType = "success" | "info" | "warning" | "error";

const createContainerOptions = (a: AlertType) => {
  const pattern = new RegExp(`^${a}$`);
  return [
    a,
    {
      render: (tokens: Token[], idx: number) => {
        const m = tokens[idx].info.trim().match(pattern);
        if (m && tokens[idx].nesting === 1) {
          return `<div class="v-alert v-alert--dense v-alert--text ${a}--text">\n`;
        }
        return `</div>\n`;
      }
    }
  ];
};

const md = markdownIt({
  highlight: function(code, lang) {
    return hljs.highlightAuto(code, [lang]).value;
  },
  html: true,
  linkify: true,
  breaks: true,
  typographer: true
})
  .use(container, ...createContainerOptions("success"))
  .use(container, ...createContainerOptions("info"))
  .use(container, ...createContainerOptions("warning"))
  .use(container, ...createContainerOptions("error"))

// 省略

こんなんでどうよ・・・

SS 2020-12-23 0.26.08.png

完☆璧

完成品

SS 2020-12-23 0.33.39.png

こんな感じで、ちょっとリッチな表示ができているのではないでしょうか。
styleとプラグインで自由に拡張できるのは楽しいですね。
Vuetifyくんに邪魔されることもありますが、仲良くできればいくらでも活かすことができそうです!夢が膨らみます

あとがき

エディタのツールバーとかはハリボテです・・・ごめんなさい・・・

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む