Make it to make it

いろいろ作ってアウトプットするブログ

VueのInput周りの用法

チェックボックスラジオボタン、複数セレクト、テキスト入力など。

inputs.vue

<template>
  <div id="app">
    <h4>Check Boxes</h4>
    <input type="checkbox" id="checkbox" v-model="checked" />
    <label for="checkbox">This box is {{ checked ? 'checked' : 'unchecked' }}</label>
    <hr />

    <h4>Radio Buttons</h4>
    <div v-for="(dino, index) in dinos" :key="index">
      <label>
        <input type="radio" :value="dino" v-model="chosenDino" />
        {{ dino }}
      </label>
      <br />
    </div>
    <span>Favorite: {{ chosenDino }}</span>
    <hr />

    <h4>Multi Select:</h4>
    <select v-model="selected" multiple>
      <option
        v-for="(period, index) in periods"
        :value="period.value"
        :key="index"
      >{{ period.name }}</option>
    </select>
    <br />
    <span>Selected IDs: {{ selected }}</span>
    <hr />

    <h4>Text Input:</h4>
    <input type="text" v-model="single" />
    <p>{{ single }}</p>
    <hr />

    <h4>Multiline message:</h4>
    <textarea v-model="message" placeholder="add multiple lines"></textarea>
    <p style="white-space: pre">{{ message }}</p>
  </div>
</template>

<script>
import "./dark.min.css";

export default {
  data() {
    return {
      checked: false,
      selected: [],
      chosenDino: "",
      single: "",
      message: "",
      dinos: ["Triceratops", "Velociraptor", "Tyrannosaurus"],
      periods: [
        { name: "Triassic", value: 1 },
        { name: "Jurassic", value: 2 },
        { name: "Cretaceous", value: 3 }
      ]
    };
  },
  methods: {
    addDinos: function() {
      this.count += this.amount;
    }
  }
};
</script>

Vueのdata, computed, methods, watch, filters全部入りでお買い物リスト

たまに見返したいので。dark.min.cssは下記のものを使用。

github.com

tobuy.vue

<template>
  <div>
    <h1>Vue tobuy list</h1>
    <form @submit.prevent="addItem" autocomplete="off">
      <input type="text" v-model="itemToAdd" @keypress.enter="addItem" />
      <button>{{ buttonText }}</button>
    </form>
    <ul class="todo" style="list-style: none; padding-left: 0;">
      <li v-for="(item, index) in items" :key="index">
        <label>
          <button @click="deleteItem(index)">&times;</button>
          <span style="margin-right: 10px;">{{ item.text | capitalize }}</span>
          <button @click="decrease(index)">&minus;</button>
          <span style="margin-right: 6px;">{{ item.quantity }}</span>
          <button @click="increase(index)">&plus;</button>
        </label>
      </li>
    </ul>
    <div v-if="itemTotalAmount > 0">Total items in cart: {{ itemTotalAmount }}</div>
    <div v-else>There is no items!</div>
  </div>
</template>

<script>
import "./dark.min.css";
import _ from "lodash";

export default {
  data() {
    return {
      itemToAdd: "",
      buttonText: "Add item",
      items: [
        { text: "apple", quantity: 1 },
        { text: "banana", quantity: 1 },
        { text: "clementine", quantity: 1 }
      ]
    };
  },
  computed: {
    itemTotalAmount() {
      return this.items.reduce((prev, curr) => prev + curr.quantity, 0);
    }
  },
  methods: {
    addItem() {
      const itemToAdd = this.itemToAdd.toLowerCase();
      const matchedItemIndex = this.items.findIndex(
        item => item.text.toLowerCase() === itemToAdd
      );
      if (matchedItemIndex !== -1) {
        this.items[matchedItemIndex].quantity += 1;
        return;
      }
      this.items.push({ text: itemToAdd, quantity: 0 });
    },
    deleteItem(index) {
      this.items.splice(index, 1);
    },
    increase(index) {
      this.items[index].quantity += 1;
    },
    decrease(index) {
      if (this.items[index].quantity === 1) {
        this.deleteItem(index);
        return;
      }
      this.items[index].quantity -= 1;
    }
  },
  watch: {
    itemToAdd: _.debounce(function() {
      this.buttonText =
        this.itemToAdd !== "" ? `Add ${this.itemToAdd}` : "Add item";
    }, 100)
  },
  filters: {
    capitalize(value) {
      if (!value) return "";
      const newValue = value.toString();
      return newValue.charAt(0).toUpperCase() + newValue.slice(1);
    }
  }
};
</script>

Vueでform validation

フォームの初期状態

下記のようなファイル構成。

.
├── app.js
└── index.html

index.html

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

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Vue form validation</title>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
  <script src="https://unpkg.com/vue/dist/vue.js"></script>
</head>

<body>
  <div id="app">
    <div class="container">
      <h1>Vue form validation</h1>
      <form @submit.prevent="submitForm" autocomplete="off">
        <div class="form-group">
          <label for="name">Name(Only string):</label>
          <input v-model="form.name" class="form-control" id="name">
          <p v-if="!nameIsValid" class="error-message">The name field is required</p>
        </div>

        <div class="form-group">
          <label for="age">Age(Between 18 and 108):</label>
          <input v-model="form.age" class="form-control" id="age">
          <p v-if="!ageIsValid" class="error-message">The age field is invalid</p>
        </div>

        <div class="form-group form-group-last">
          <button class="btn btn-primary" :disabled="!formIsValid">Submit</button>
          <p v-show="form.sent" class="mt-2 text-info">Form has been successfully sent!</p>
        </div>
      </form>
    </div>
  </div>

  <script src="app.js"></script>
</body>

</html>

app.js

/* global Vue */
// eslint-disable-next-line no-unused-vars
const app = new Vue({
  el: '#app',
  data: {
    form: {
      name: null,
      age: null,
      sent: false
    }
  },
  computed: {
    nameIsValid () {
      return !Number(this.form.name) && !!this.form.name
    },
    ageIsValid () {
      return !!Number(this.form.age) && this.form.age >= 18 && this.form.age <= 108
    },
    formIsValid () {
      return this.nameIsValid && this.ageIsValid
    }
  },
  methods: {
    submitForm () {
      if (!this.formIsValid) return

      this.form.sent = true
    }
  }
})

内容としては、名前と年齢を入力してフォームを送信するというシンプルなもの。 名前には数字は入力できず、年齢には18-108間の数字のみが入力できる。

できるだけhtmlのinput側では制御せず、vueの機能を用いてリファクタリングしていってみる。

まずはinputへの型指定だが、v-modelに修飾子を付けることで行うことができる。

vuejs.org

v-model.numberで数値型に変換してくれるので、数値になってるかどうかのチェックが不要になる。

Before

<input v-model.number="form" class="form-control" id="age">
ageIsValid () {
   return !!Number(this.form.age) && this.form.age >= 18 && this.form.age <= 108
},

After

<input v-model.number="form.age" class="form-control" id="age">
ageIsValid () {
   return this.form.age >= 18 && this.form.age <= 108
},

しかしながら、初期状態でエラー文言が掲出されたままであったりして、まだこのままでは使い勝手がよくない。 一つひとつのエラー文言のハンドリングをしようとすると手間がかかってしまう。

そこで、Vueでvalidateのできるライブラリを用いる。

vuelidateを用いてリファクタリング

appVuelidate.vue

<template>
  <div id="app">
    <div class="container">
      <h1>Vue form validation</h1>
      <form @submit.prevent="submitForm" autocomplete="off">
        <!-- To validate on input synchronously, remove @blur and add v-model.trim="$v.form.name.$model" -->
        <!-- $error === $invalid && $dirty -->
        <div class="form-group">
          <label for="name">Name(Only string):</label>
          <input
            v-model.trim="form.name"
            @blur="$v.form.name.$touch()"
            :class="{ 'input-error': $v.form.name.$error, 'input-valid': !$v.form.name.$invalid }"
            class="form-control"
            id="name"
          />
          <template v-if="$v.form.name.$error">
            <p
              v-if="!$v.form.name.alpha"
              class="text-danger"
            >The name field should be alphabet characters</p>
            <p v-else class="text-danger">The name field is required</p>
          </template>
          <p
            class="text-info"
          >Invalid: {{ $v.form.name.$invalid }} | Dirty: {{ $v.form.name.$dirty }} | Error: {{ $v.form.name.$error }}</p>
        </div>
        <div class="form-group">
          <label for="age">Age(Between 18 and 108):</label>
          <input
            v-model.number.trim="form.age"
            @blur="$v.form.age.$touch()"
            :class="{ 'input-error': $v.form.age.$error, 'input-valid': !$v.form.age.$invalid }"
            class="form-control"
            id="age"
          />
          <template v-if="$v.form.age.$error">
            <p v-if="!$v.form.age.required" class="text-danger">The age field is required</p>
            <p
              v-else-if="!$v.form.age.integer"
              class="text-danger"
            >The age field should be an integer</p>
            <p
              v-else-if="!$v.form.age.between"
              class="text-danger"
            >You should be at least 18 and younger than 108 to coutinue</p>
          </template>
          <p
            class="text-info"
          >Invalid: {{ $v.form.age.$invalid }} | Dirty: {{ $v.form.age.$dirty }} | Error: {{ $v.form.age.$error }}</p>
        </div>
        <button :disabled="$v.form.$invalid" class="btn btn-primary">Submit</button>
        <p v-show="form.sent" class="mt-2 text-success">Form has been successfully sent!</p>
      </form>
    </div>
  </div>
</template>

<script>
import "bootstrap/dist/css/bootstrap.min.css";
import { validationMixin } from "vuelidate";
import * as validators from "vuelidate/lib/validators";

export default {
  data() {
    return {
      form: {
        name: null,
        age: null,
        sent: false
      }
    };
  },
  mixins: [validationMixin],
  validations: {
    form: {
      name: {
        required: validators.required,
        alpha: validators.alpha
      },
      age: {
        required: validators.required,
        integer: validators.integer,
        between: validators.between(18, 108)
      }
    }
  },
  methods: {
    submitForm() {
      this.$v.form.$touch();
      if (!this.$v.form.$invalid) {
        this.form.sent = true;
        return;
      }
      this.form.sent = false;
    }
  }
};
</script>

<style>
#app .input-valid {
  border-color: rgba(126, 239, 104, 0.8);
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset,
    0 0 8px rgba(126, 239, 104, 0.6);
}
#app .input-error {
  border-color: tomato;
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px tomato;
}
</style>

VeeValidateを用いてリファクタリング

appVeeValidate.vue

<template>
  <div class="container">
    <h1>Vue form validation</h1>
    <form @submit.prevent="submitForm" autocomplete="off">
      <div class="form-group">
        <label for="name">Name(Only alphabetic characters):</label>
        <input
          v-model.trim="form.name"
          v-validate="'alpha_spaces'"
          name="name"
          class="form-control"
          id="name"
        />
        <p class="text-danger">{{ errors.first('name') }}</p>
      </div>
      <div class="form-group">
        <label for="age">Age(Between 18 and 108):</label>
        <input
          v-model.number.trim="form.age"
          v-validate="'between:18,108'"
          name="age"
          class="form-control"
          id="age"
        />
        <p class="text-danger">{{ errors.first('age') }}</p>
      </div>
      <button class="btn btn-primary">Submit</button>
    </form>
  </div>
</template>

<script>
import { ValidationProvider } from "vee-validate";

export default {
  data() {
    return {
      form: {
        name: null,
        age: null,
        sent: false
      }
    };
  },
  components: {
    ValidationProvider
  },
  methods: {
    submitForm() {}
  }
};
</script>

Vueでrapid prototyping

rapid prototyping setup

VueのSFC (Single File Component) で軽く何かを試したいとき、いちいちvue-cliを使うのは面倒。 そこでvue-cliの一部機能であるcli-service-globalを使うことで、rapid prototypingができて便利。

vue-cliのインストール

まずはVue CLIを先にインストールする。

npm install -g @vue/cli

// or

yarn global add @vue/cli

cli-service-globalのインストール

npm install -g @vue/cli-service-global

// or

yarn global add @vue/cli-service-global

cli-service-globalの実行

vue serve app.vue

TypeScript学習5(Classes)

Classes

Blueprint to create an object with some fields (values) and methods (functions) to represent a 'thing'.

class Vehicle {
  drive(): void {
    console.log('chugga chugga');
  }

  honk(): void {
    console.log('beep');
  }
}

class Car extends Vehicle {
  drive(): void {
    console.log('vroom');
  }
}

const car = new Car();
car.drive();
car.honk();

上記のようにES2015のClassと同様に働くが、TSならではのmodifierが存在する。

Modifiers

public

This method can be called anywhere, anytime.

private

This method can only be called by other methods in this class.

privateは決してセキュリティ目的で使用されるものではなく、メソッドが使用されるスコープを制限するためだからである。

protected

This method can be called by other methods in this class, or by other methods in child classes.

classのメソッドを継承先で使用したいときに使う。

使用例

class Vehicle {
  constructor(public name: string) {
    this.name = name; // 省略可
  }

  protected honk(): void {
    console.log('beep');
  }
}

class Car extends Vehicle {
  private drive(): void {
    console.log('vroom');
  }

  startDriving(): void {
    this.drive();
    this.honk();
  }
}

const car = new Car('Beatle');

console.log(car.name);
car.startDriving();

継承

class Vehicle {
  constructor(public name: string) {
    this.name = name; // 省略可
  }

  protected honk(): void {
    console.log('beep');
  }
}

class Car extends Vehicle {
  constructor(public wheels: number, name: string) {
    super(name);
  }

  private drive(): void {
    console.log('vroom');
  }

  startDriving(): void {
    this.drive();
    this.honk();
  }
}

const car = new Car(4, 'Beatle');

console.log(car.name);
car.startDriving();

まとめ

Interfaces + Classes = How we get really strong code reuse in TS

TypeScript学習4(Typed objects: Interfaces)

Interfaces

Creates a new type, describing the property names and value types of an object

例えば、今まで学んだことを利用して次のように型定義を行うと、コードが長くなってしまって可読性も低くなる。

const oldCivic = {
  name: 'civic',
  year: 200,
  broken: true
};

const printVehicle = (vehicle: {
  name: string;
  year: number;
  broken: boolean;
}): void => {
  console.log(`Name: ${vehicle.name}`);
  console.log(`Year: ${vehicle.year}`);
  console.log(`Broken? ${vehicle.broken}`);
};

そこで、Interfaceを用いて別箇所での型定義を行う。

const oldCivic = {
  name: 'civic',
  year: 200,
  broken: true
};

interface Vehicle {
  name: string;
  year: number;
  broken: boolean;
}

const printVehicle = (vehicle: Vehicle): void => {
  console.log(`Name: ${vehicle.name}`);
  console.log(`Year: ${vehicle.year}`);
  console.log(`Broken? ${vehicle.broken}`);
};

プリミティブだけでなく、どんな型でもInterfacesに入れることができる。

const oldCivic = {
  name: 'civic',
  year: new Date(),
  broken: true,
  summary(): string {
    return `Name: ${this.name}`;
  }
};

interface Vehicle {
  name: string;
  year: Date;
  broken: boolean;
  summary(): string;
}

const printVehicle = (vehicle: Vehicle): void => {
  console.log(vehicle.summary());
};

printVehicle(oldCivic);

Interfaceとは、ファンクションに引数を渡す際のゲートキーパである。

TypeScript学習3(Typed arrays)

TSでarrayを扱う上で重要なポイント

  • TS can do type inference when extracting values from an array
  • TS can prevent us from adding incompatible values to the array
  • We can get help with map, forEach, reduce, functions
  • Flexible - arrays can still contain multiple different types

配列(Array)の記述方法

const carMakers: string[] = ['ford', 'toyota', 'chevy'];
const dates = [new Date(), new Date()];

const carsByMake: string[][] = [];

// Help with inferences when extracting values
const car = carMakers[0];
const myCar = carMakers.pop();

// Prevent incompatible values
carMakers.push(100); // Throws error

// Help with `map`
carMakers.map(
  (car: string): string => {
    return car.toUpperCase();
  }
);

const importantDates: (Date | string)[] = [new Date(), '2030-10-10'];
importantDates.push('2030-10-10');
importantDates.push(new dates());

Tuple

Array-like structure where each element represents some property of record.

配列の中でも、決まった順序である特定の型の値が格納されるというものに関しては、Tupleという型定義を行う。

const drink = {
  color: 'brown',
  carbonated: true,
  sugar: 40
};

const pepsi = ['brown', true, 40];

例えば上記のようなコードの書き方だと、配列の値はstring, number, booleanのどれでも取れるということになってしまい、順番も自由にできてしまう。そこでTupleを用いてannotationを行う。

const drink = {
  color: 'brown',
  carbonated: true,
  sugar: 40
};

type Drink = [string, boolean, number];

const pepsi: Drink = ['brown', true, 40];
const sprite: Drink = ['clear', true, 40];

だがここでよくよく考えてみる。

そもそも配列の順序が固定されていて値の型も固定ならば、配列よりオブジェクトを書いたほうが有効なはずである。 なので、あまりTapleを使う出番はないであろう。