日々の学びとか気づきについて書く

キャリアは日々のアウトプットの組み合わせ

ShopifyのPolarisによる Data Table Componentとスタイル編集法

はじめに

この記事はShpoifyの公式サイトにより提供されているNodeやReactでShopify アプリを構築するを完遂した方を対象としています。

まだ手をつけたことがない方は、まずそちらから目を通していただければと思います。

開発はNext×TypeScriptで行います。

TypeScriptのセットアップはこちらを参照してください。

UIコンポーネントとは

ReactやVue、Angularといったフロントエンドライブラリ(フレームワーク)が流行する以前は、開発者がなるべく早くそれなりのUIを実現するためにCSSフレームワーク(Bootstrapなど)が広く使われてきました。

昨今のようにフロントエンドにライブラリを導入する場面が多くなると、ライブラリ向けにサクッとそれなりのUIを実現するコンポーネントセットが出てきました。

コンポーネントセットから呼び出し、コードの中に読み込む事で使用するコンポーネントをUIコンポーネントと呼びます。

コンポーネントは部品といった意味のある英単語です。

WEBアプリケーションにおけるUIを実現する部品としてはボタンやフォームがすぐに思い浮かびます。

UIコンポーネントはいい感じのボタンをサクッと作るために使用できます。

Shopifyが提供しているコンポーネントセットはPolarisです。

これは開発者がアプリを開発するときに、Shopifyの管理画面と似たようなUIを実現するためのコンポーネントセットです。

Polarisを使用することで、アプリのユーザは管理画面との違和感が少ないUIを利用でき、開発者の工数も減らすことができるようになります。

Polarisのインストール

早速ShopifyアプリのプロジェクトにPolarisを導入してみましょう。

Polarisを使うには2つの方法があります。

  1. React のUI Componentとして使う
  2. CSSを読み込んで使う

今回は1に関して説明していきます。

まずは公式にしたがってパッケージをインストールします。

npmを使ってのインストールです。

npm install @shopify/polaris --save

yarnを使っている方はこちらのコマンドでインストールできます。

yarn add @shopify/polaris

これでインストールは完了です。 Polarisをプロジェクトで使用可能になりました!

Table Component を使ってみる

早速Polarisを使ってShopifyライクなUIを実装しましょう。 今回は Data Table Component を使ってみます。

ドキュメントに掲載されているサンプルコードを少しいじってみます。

index.tsx

import jaTranslations from '@shopify/polaris/locales/ja.json';
import '@shopify/polaris/dist/styles.css';
import { AppProvider, Card, DataTable, Page } from '@shopify/polaris';
import React from 'react';

const Index: React.FC = () => {
  const rows = [
    ['モニター', 13000, 140],
    ['キーボード', 15000, 183],
    ['マウス', 5000, 32],
  ];
  return (
    <AppProvider i18n={jaTranslations}>
      <Page>
        <Card>
          <DataTable
            columnContentTypes={[
              'text',
              'numeric',
              'numeric',
            ]}
            headings={[
              '商品名',
              '価格',
              '在庫数',
            ]}
            rows={rows}
          />
        </Card>
      </Page>
    </AppProvider>
  );
}

export default Index;

Polaris Data Table Example シンプルなコードでShopifyライクなUIを実装できましたね。

Polarisコンポーネントを使っても上手くスタイルが反映されない場合があります。 その場合は以下のコードが入っているか確認をしてください。

import '@shopify/polaris/dist/styles.css';

これをアプリケーションのルートに書いておけばどのファイルからでも Polaris コンポーネントが使えるようになります。

Data Table Component を使うためにはcolumnContentTypes headings rowsの3つの値が必須です。 ドキュメントを参照しながら3つの値について説明します。

columnContentTypes ("text" | "numeric")[] List of data types, which determines content alignment for each column. Data types are "text," which aligns left, or "numeric," which aligns right.

このプロパティにはテーブルのカラムのタイプを指定します。指定できる値の型はtext numericの2種類で、textを指定したカラム名は左揃えになり、numericを指定したカラムは右揃えになります。

先程のコードだと商品名にはtextを指定し、それ以外のカラムにはnumericを指定しました。それぞれのカラム名の位置に注目し、違いを確認してみてください。

headings React.ReactNode[] List of column headings.

このプロパティではカラム名をできます。型はReact.ReactNodeとなっておりあまり聞き馴染みがないかもしれません。この型は@types/reactの中で定義されており、その内容はこちらです。

type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;

ReactNodeはunion型であることがわかります。また聞き馴染みのない型がたくさん出てきましたが、ReactChildを押さえておけば問題ありません。

type ReactText = string | number;
type ReactChild = ReactElement | ReactText;

この型も同様に@types/react内で定義されています。また合わせてReactTextも確認しておきましょう。ここまでみると、文字列と数値を許可していることがわかりますね。 先程のコードのように文字列でカラム名を指定できることがわかりました。

rows ((string | number | React.ReactNode)) Lists of data points which map to table body rows.

最後のプロパティです。ここにもReactNodeの型が出てきました。このプロパティでは二次元配列を指定しなければいけません。

これらを押さえておけば、Data Table Component を使用できます。

スタイルを編集してみる

最後に Data Table Component のスタイルを変更する方法を紹介します。 その前に現在のアプリの画面を再度確認してみましょう。

Polaris Data Table Example

何とも味気ない画面です。

工夫を加えるために在庫数が50以下の商品は赤文字で在庫数を表示させるようにしてみましょう。

ここで先程学習したrowsに指定できる型が活きてきます。

stringnumberではスタイルを加えることができません。そこでReactNodeを上手く使ってスタイルを埋め込んでいきます。

ReactNodeにはReactChildが指定でき、ReactChildにはReactElementが指定できると学びました。 この記事ではReactElementを明示できてに作成し、スタイルを埋め込んでいきます。

具体的には以下の関数を追加します。

  const createElement = (stock: number): number | ReactElement => {

    if (stock > 50) {
      return stock;
    } else {
      const style: React.CSSProperties = { color: 'red' };
      return React.createElement('span', { style }, stock);
    }
  }

React.createElementReactElement型のオブジェクトを返す関数です。DOMと思ってもらうとわかりやすいと思います。

第一引数にはHTML要素名、第二引数にオプション(省略可能)、第三引数に表示する文字列を入力します。

オプションではスタイルを指定できるので、スタイルを指定する変数を一つ作り、オプションとして渡しています。 CSSでの定義と同じように使えます。

この関数とrowsを組み合わせるとこのようになります。

  const rows = [
    ['モニター', 13000, createElement(140)],
    ['キーボード', 15000, createElement(183)],
    ['マウス', 5000, createElement(32)],
  ];

これで各配列の第三要素にはnumberもしくはReactElementが入るようになりました。 画面を確認してみましょう。

在庫数が32の項目だけ赤文字で表示されていますね。 最終的なコードは以下のようになりました。

import jaTranslations from '@shopify/polaris/locales/ja.json';
import '@shopify/polaris/dist/styles.css';
import { AppProvider, Card, DataTable, Page } from '@shopify/polaris';
import React, { ReactElement } from 'react';

const Index: React.FC = () => {
  const createElement = (stock: number): number | ReactElement => {

    if (stock > 50) {
      return stock;
    } else {
      const style: React.CSSProperties = { color: 'red' };
      return React.createElement('span', { style }, stock);
    }
  }

  const rows = [
    ['モニター', 13000, createElement(140)],
    ['キーボード', 15000, createElement(183)],
    ['マウス', 5000, createElement(32)],
  ];

  return (
    <AppProvider i18n={jaTranslations}>
      <Page>
        <Card>
          <DataTable
            columnContentTypes={[
              'text',
              'numeric',
              'numeric',
            ]}
            headings={[
              '商品名',
              '価格',
              '在庫数',
            ]}
            rows={rows}
          />
        </Card>
      </Page>
    </AppProvider>
  );
}

export default Index;

今回は Data Table Component を使いましたが、他のコンポーネントReactNodeが指定できるプロパティにも同様のことが行えると思います。 ぜひ参考にしてください。

LaravelでPrimary Keyをidから変更したい

Modelのファイルにprotected $primaryKeypublic $increasingを定義する。

例えばphp artisan make:model UserコマンドでUserモデルを作成すると以下のようなファイルが作成されると思います。

<?php
namespace App;

class User extends Model
{
}
?>

LaravelのEloquentはデフォルト状態ではidカラムを主キーとしています。

なので、主キーを変更したい時はModel側で主キーを定義する必要があります。 ここでは主キーをidからuuidに変更する場合を考えてみます。

<?php
namespace App;

class User extends Model
{
    protected $primaryKey = "uuid";
}
?>

このようにすることで主キーを変更することができます。

Laravel5.8 Eloquent:利用の開始

Eloquentは更にテーブルの主キーがidというカラム名であると想定しています。この規約をオーバーライドする場合は、protectedのprimaryKeyプロパティを定義してください。

しかし、このままでは正しく動作しません。 というのもEloquentは主キーを自動で増分する整数値であると想定しています。

なので内部では主キーをint型へとキャストしています。 この設定をpublic $increasing = falseとすることで上書きします。

<?php
namespace App;

class User extends Model
{
    protected $primaryKey = "uuid";
 public $increasing = false;
}
?>

Laravel5.8 Eloquent:利用の開始

さらに、Eloquentは主キーを自動増分される整数値であるとも想定しています。つまり、デフォルト状態で主キーは自動的にintへキャストされます。自動増分ではない、もしくは整数値ではない主キーを使う場合、モデルにpublicの$incrementingプロパティを用意し、falseをセットしてください。

Laravelで主キーを変更したくなったら参考にしてください。

HTMLCollectionとNodeListの違いについて

JavaSCriptから複数のDOMを扱う際にはHTMLCollectionやNodeListを使います。

これらの違いについてまとめたのでメモ書きしておきます。

HTMLCollectionとは

HTMLCollectionはdocument.getElementsByClassNameメソッドなどの戻り値です。

配列風オブジェクトなので、Array.fromやスプレッド構文などを使えば配列に変換できます。

NodeListとは

NodeListはdocument.querySelectorAllメソッドなどの戻り値です。

こちらも配列風オブジェクトなので、Array.fromやスプレッド構文などで配列に変換できます。

また、HTMLCollectionと違い、forEachメソッドを使用できます。 なので、セレクトボックスの中の特定条件にマッチするオプションのみ選択不可のような処理をNodeList.forEachで一気に行えます。

const nodeList = document.querySelectorAll('.option')
nodeList.forEach(node => {
    if (node.value % 2 === 0) {
        node.disabled = true
    }
})

上のコードでは、オプションのvalueが偶数の要素は選択できないようにしています。

HTMLCollectionとNodeListの違いとは

それぞれが呼び出せるメソッドが違っていたり、それぞれを戻り値として返す関数が異なっていたりなどの違いはありますが、一番の違いはDOMの変更に対して動的か静的かです。

HTMLCollectionは動的です。 具体的なコードで見てみます。

HTMLファイルは以下のようになっています。

<select>
    <option class="option" value="1">1</option>
    <option class="option" value="2">2</option>
    <option class="option" value="3">3</option>
</select>

そしてJSは以下のようになっています。

const collection = document.getElementsByClassName('option')
console.log(collection.length)
// 3

const option = document.createElement('option')
option.innerText = '4'
option.value = 4
option.classList = 'option'
document.querySelector('select').appendChild(option)
console.log(collection.length)
// 4

document.getElementsByClassNameで複数のオプション要素を取得しています。 この時に返ってくるのはHTMLCollectionで要素数は3です。

次にオプション要素を作成して、セレクトボックスの末尾に追加しています。 するとHTMLCollectionの要素数が4に増えています。

このようにDOMの変化に対して動的なのがHTMLCollectionの特徴です。

対してNodeListは静的です。

const nodeList = document.querySelectorAll('.option')
console.log(nodeList.length)
// 3

const option = document.createElement('option')
option.innerText = '4'
option.value = 4
document.querySelector('select').appendChild(option)

console.log(nodeList.length)
// 3

要素を追加しても変化はありません。 NodeListはDOMの変更にていして静的です。

ただし、querySelectorAllの戻り値のNodeListは静的ですが、childNodesの戻り値であるNodeListは動的です。

ここには注意してください。

developer.mozilla.org

HTMLColecction型のオブジェクトで配列のメソッド(forEach, mapなど)を使いたい

HTML文書から要素を複数取得し、取得した各要素に対してなんらかの処理を行い時があります。

例えば、複数のチェックボックスがあり、全てのチェックボックスのうちチェック済みの要素のみを抽出して配列にしたい、などです。

要素を複数取得するときに使うDocumentインターフェースのメソッドとしてはdocument.getElementsByClassNamedocument.querySelectorAllを主に使うかと思っています。

これらのメソッドを使って取得した複数の要素に対して配列のメソッドを使う方法を紹介します。

document.getElementsByClassNameの場合

document.getElementsByClassNameではクラス名を使って複数の要素を取得します。

サンプルとして以下のようにセレクトボックスの中に複数のオプションが設定されている場合を考えてみます。

<select>
    <option class="option" value="1">1</option>
    <option class="option" value="2">2</option>
    <option class="option" value="3">3</option>
</select>

オプションにはクラス名としてoption、バリューには整数を埋め込んでいる非常にシンプルな構成になっています。

複数のオプションを一括で取得する時、document.getElementByClassNameを使うと以下のようになります。

const htmlCollection = document.getElementsByClassName('option')
console.log(htmlCollection) 
// HTMLCollection(3) [option.option, option.option, option.option]

クラス名を指定することで、そのクラス名を持つ要素を複数取得できます。 document.getElementsByClassNameメソッドの戻り値はHTMLCollectionです。 このインターフェースを実装していると配列風なオブジェクトになります。 しかし、配列のプロトタイプにあるforEachなどのメソッドを使うことはできません。

mapを使ってオプションタグのバリューからなる新しい配列などを作りたいと思ってもこのままではできないのです。 そこでHTMLCollectionのような配列風オブジェクトを配列に変換します。

const htmlCollection = document.getElementsByClassName('option')
const array = Array.from(htmlCollection)
console.log(array)
// [option.option, option.option, option.option]

Array.fromで配列に変換できるオブジェクトについては下記の記事で説明しています。 javascript-k.hatenablog.com

このメソッドを使うことで配列風オブジェクトであるHTMLCollectionは配列に変換され、mapなどの関数を使う事ができるようになります。

const values = array.map(element => element.value)
console.log(values)
// ["1", "2", "3"]

document.querySelectorAllの場合

document.querySelectorAllメソッドの戻り値はNodeList型のオブジェクトです。

このオブジェクトはHTMLCollectionとの大きな違いは少なく、そのままではmapのような配列のメソッドを使うことはできません。

HTMLCollectionと同様に配列に変換してメソッドを呼び出します。

const nodeList = document.querySelectorAll('.option')
const array = Array.from(nodeList)
const values = array.map(element => element.value)
console.log(values)
// ["1", "2", "3"]

ただ、NodeListHTMLCollectionとは違い、forEachメソッドを呼び出すことは可能です。 例えば、特定条件のオプションを選択不可にしたいときなどは、以下のような実装が考えられます。

const nodeList = document.querySelectorAll('.option')
nodeList.forEach(node => {
    if (node.value % 2 === 0) {
        node.disabled = true
    }
})

これはoptionvalueが偶数の時、選択できなくするプログラムです。 なので、利便性はNodeListの方が高いのかなと思います。

HTMLCollectionNodeListの違いは以下の記事にも書いています。

javascript-k.hatenablog.com

複数のHTML要素を取得し、配列のメソッドが使いたい時には参考にしてください!

Array.fromで配列に変換できるオブジェクトについて

Array.fromで配列に変換できるオブジェクト

Array.fromメソッドで配列に変換できるオブジェクトは

  1. 配列風なオブジェクト
  2. 反復処理可能なオブジェクト

の2種類です。

なので、文字列やHTMLCollectionなどがこのメソッドで配列に変換できます。

const str = 'hogehoge'
console.log(Array.from(str))
// ["h", "o", "g", "e", "h", "o", "g", "e"]


const elements = document.getElementsByClassName('option')
console.log(Array.from(elements))
//  [option.option, option.option, option.option]

またArray.fromメソッドは第二引数にコールバック関数を受け取り、配列風オブジェクトを加工して新しい配列にすることもできます。 例えば、上記の例であればクラス名がオプションを持つ要素の配列を作成していますが、要素のvalueのみからなる新しい配列を作ることができます。

const elements = document.getElementsByClassName('option')
console.log(Array.from(elements, element => element.value))
// ["1", "2", "3"]

Array.fromはこのような使い方ができます。

配列風なオブジェクトとは

配列風なオブジェクトとは、

  1. lengthプロパティを持つ
  2. 添字でアクセスできる

の2つの条件を満たすオブジェクトです。

作成される配列は、lengthプロパティの値と等しい要素を持ちます。なので、値がhogeなどのNumber型に変換できない場合、作成される配列の要素数は0になります。

const likeArr = { length: 'hoge' }
console.log(Array.from(likeArr))
// []

そして配列の要素になる値ですが、オブジェクトの添字が整数になっているプロパティの値です。添字は文字列でも問題ではなく、Number型に変換されます。

const likeArr = { length: '3', '2': 3 }
console.log(Array.from(likeArr))
// [undefined, undefined, 3]

2つ目の添字でアクセスできるですが、添字でアクセスした時の戻り値はundefinedでも問題ありません。 なので以下のようなオブジェクトでも配列風なオブジェクトとしてみなされます。

const likeArr = { length: 3 }
console.log(Array.from(likeArr)) 
// [undefined, undefined, undefined]

このような仕様になっているので、HTMLCollectionやNodeListなども配列に変換できます。

反復処理可能なオブジェクトとは

反復処理可能なオブジェクトとは、Symbol.iteratorプロパティを持っているオブジェクトです。 プロパティの値は何でも良いわけでは無く、仕様が決められています。

こちらにその仕様が詳しく記述されています。 https://ja.javascript.info/iterable

ja.javascript.info

この仕様に反するプロパティだと正しく動作しません。 なので配列に変換することができなくなります。

また、反復処理が可能なオブジェクトはArray.fromメソッドだけでなく、スプレッド構文でも配列に変換できます。

なので、文字列やHTMLCollectionなどのオプジェクトはスプレッド構文でも配列に変換できます。

const str = 'hoge'
console.log([...str])
// ["h", "o", "g", "e"]


const elements = document.getElementsByClassName('option')
console.log([...elements])
// [option.option, option.option, option.option]

スプレッド構文は反復可能なオブジェクトに対しては使用できますが、配列風なオブジェクトに対しては利用できません。

Ruby によるテンプレートメソッド (Template Method) パターン

Template Methods パターンとは

Template MethodパターンはGoFデザインパターンの一種です。

このパターンでは、スーパクラスでテンプレートとなるメソッドの定義を行い、そのメソッドの中に抽象メソッドを実装します。

テンプレートメソッドの中では、抽象メソッドを使うことで処理の骨組みを作ります。

テンプレートが定義されているクラスを継承したサブクラスで、抽象メソッドを実装し、テンプレートの具体的な処理を決めていきます。

このようにすることで、似通った部分を共通化し、サブクラスに対して、スーパクラスで実装されている抽象メソッドを実装する、という責任を与えることができます。

これは僕たちの身の回りだと氷型に似ていると思います。

僕たちは氷型を使うことで星や三角形などの色々な形の氷を作ることができます。

この時、形を作る型がテンプレートになります。

しかし、実際に何を使って氷を作るか、具体的な定義はありません。

例えば、水で作る人もいるし、お茶を凍らせたい人もジュースを凍らせたい人もいるかもしれません。

何で氷を作るかは作り手に委ねられています。

Template Method も同様ににある程度の骨組みをあらかじめ決めておき、その中で実際にどのような処理を行うかを別の責任にします。

この骨組みを提供するのがスーパクラスであり、処理を具体的に実装していくのがそのクラスを継承したサブクラスです。

RubyによるTemplate Method

Ruby を使って Template Method パターンを実装してみます。

今回は書籍を参考にプログラムを書いています。

スーパクラスでは、文字列を表示する display メソッドをテンプレートとして提供します。

display メソッドの中には、open output close の3つの抽象メソッドを実装します。

それぞれ、文字列の先頭文字、文字列、文字列の末尾文字、の出力を行います。

具体的に何を出力するのかはサブクラスに任せることで、ロジックの共通化を行えます。

class AbstractDisplay

    def open()
        raise NotImplementedError.new("#{self.class}##{__method__}")
    end

    def close()
        raise NotImplementedError.new("#{self.class}##{__method__}")
    end

    def output()
        raise NotImplementedError.new("#{self.class}##{__method__}")
    end

    def display
        open()
        5.times do
            output()
        end
        close()
    end
end

テンプレートを提供するスーパクラスを AbstractDisplay としています。

テンプレートとなる display メソッドでは、先頭文字、文字列×5、末尾文字、を出力させます。

Ruby では抽象メソッドを定義できないので、例外処理で代替しています。

もじサブクラスでこれらのメソッドが定義されなかった場合、AbstractDisplay のメソッドが呼び出されて例外を発生させます。

このクラスを継承したクラスを2つ定義します。

class DisplayChar < AbstractDisplay
    def initialize(string)
        @string = string
    end

    def open
        print '<<'
    end

    def close
        print '>>'
    end

    def output
        print @string
    end
end
class DisplayString < AbstractDisplay

    def initialize(string)
        @string = string
    end

    def open
        displayLine()
    end

    def close
        displayLine()
    end

    def displayLine
        puts '+-----+'
    end

    def output
        puts "|#{@string}|"
    end
    
end

具体的な処理内容をサブクラスで行うことで、骨組みを共有することができるようになります。

Nodejs(随時更新)

サーバサイドでJSを動かすための実行環境。

JSの解析にV8を搭載しており、そこそこ高速。

シングルスレッドで動作し、ノンブロッキングなI/Oを実現できるため、スケーラビリティが高い。

コールバック関数は待ち行列に入れられ、実行可能になったタイミングで実行キューに入れられる。

そのため、実行キューの中に処理負荷の高い関数が入っていれば、そこで処理は止まってしまう。