About
本サイトについて
趣味で開発したプログラムや開発メモを載せています。
ソースコードはGithubで公開しつつ、なるべく後から分かるように解説に努めてますので、
誰かのお役に立てれば嬉しいです。
プロフィール
kght6123
佐賀県出身で1985年生まれ。
三重県四日市市在住のシステムエンジニア。家庭を大事にしたい2児の父。
趣味で開発したプログラムや開発メモを載せています。
ソースコードはGithubで公開しつつ、なるべく後から分かるように解説に努めてますので、
誰かのお役に立てれば嬉しいです。
佐賀県出身で1985年生まれ。
三重県四日市市在住のシステムエンジニア。家庭を大事にしたい2児の父。
Tailwind CSS を、他の UI フレームワークと連携して導入したいと考えている方へ!
最近、仕事で新規プロダクトのフロントエンドを、タイトルの構成で構築しましたが、
その際に、色々と設定を変えたりしたので、その部分をご紹介しようと思います。
ユーティリティ・ファーストの低レベルな CSS ライブラリです。
Bootstrap や Vuetify、ElementUI の様に決まった UI ではなく オリジナルの UI を爆速で作れます。
設定ファイルを変更することで、 Tailwind CSS のほぼ全てをカスタマイズできます。
今回は、ElementUI の CSS を微調整するために利用しています。
Vue.js 2.0 ベースのデスクトップ(管理画面)向けの UI コンポーネントライブラリです。
Vue.js 3.0 ベースの ElementPlus も存在します。
サンドボックスの中で、コンポーネントの仕様をテストできます。 また、コンポーネントの仕様を一覧できるドキュメントにも使えます。
昔は、 Nuxt.js で利用するための設定が大変でしたが、 nuxt/storybook の登場により、簡単に使える様になりました。
シンプルさを重視した JavaScript のテストフレームワークです。
Nuxt.js のインストール時に、手軽に追加できるため、今回、利用しています。
Tailwind CSS は v2.1 以降の JIT (Just-in-Time) モードで動かします。
StoryBook は postcss@8 のバグにより、下記のような警告をたくさん吐きます。 (今回、試す内容に影響はありませんでした)
Though the "loose" option was set to "false" in your @babel/preset-env config, it will not be used for @babel/plugin-proposal-private-property-in-object since the "loose" mode option was set to "true" for @babel/plugin-proposal-private-methods.
The "loose" option must be the same for @babel/plugin-proposal-class-properties, @babel/plugin-proposal-private-methods and @babel/plugin-proposal-private-property-in-object (when they are enabled): you can silence this warning by explicitly adding
["@babel/plugin-proposal-private-property-in-object", { "loose": true }]
to the "plugins" section of your Babel config.
この問題は、 postcss@7 を使用すれば解決しますが、 Tailwind CSS が postcss@8 を要求するため、そのまま利用しています。
下記のコマンドを実行します。
npx create-nuxt-app new-product-frontend
インストール時の UI フレームワークは Tailwind CSS ではなく ElementUI を選択します。
また、テストフレームワークは、 Jest を選択します。
他の選択項目は、お好みで選択してください。
StroyBook を nuxt.js で使用するために nuxt/storybook をインストールします。
npm install --save-dev @nuxtjs/storybook postcss@latest
.gitignore に追記します。
.nuxt-storybook
storybook-static
nuxt.config.js にも追記します。
// nuxt.config.js
storybook: { // 追加
stories: [
'~/components/**/*.stories.mdx'
],
addons: [
'@storybook/addon-controls',
'@storybook/addon-docs'
]
}
起動を楽にするために、 package.json にも追記します。
// package.json
"scripts": {
"storybook": "npx nuxt storybook",
}
Tailwind CSS をインストールします
npm install --save-dev @nuxtjs/tailwindcss tailwindcss autoprefixer postcss@latest
nuxt.config.js の buildModules に追記します。
buildModules: [
// https://tailwindcss.com/docs/guides/nuxtjs
'@nuxtjs/tailwindcss'
],
tailwind.css ファイルを作成します。
/* assets/css/tailwind.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
tailwind.config.js ファイルを作成します。
その際に、下記の設定を意図的に変えています。
tw-
を追加// tailwind.config.js
module.exports = {
mode: 'jit', // JIT(Just-in-Time)モード有効
prefix: 'tw-', // `tw-`を追加
important: false,
purge: [
'./components/**/*.{vue,js}',
'./layouts/**/*.vue',
'./pages/**/*.vue',
'./plugins/**/*.{js,ts}',
'./nuxt.config.{js,ts}',
],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}
w-[100px]
や m-[10px]
などの微妙な位置調整の指定ができる!
をつける( !w-1
)で !important
の指定ができる@apply
に自分で作成した css を使う時は、 @layer
が必要になるカスタマイズが捗るので、おすすめです。
npx tailwindcss init --full tailwind.config.full.js
この注意事項は、私が所属するチーム全体で共有して進めています。
css クラスの利用の優先順位について
@apply
で可能な限り Tailwind CSS を使う)無闇に !
を付与しない
!
を先頭に付与する動作確認に使用するコンポーネントは、 Atomic Design で実装しています。
<!-- components/atoms/el/Input.vue -->
<template>
<el-input
:name="name"
:value="value"
:type="type"
:placeholder="placeholder"
:disabled="disabled"
:maxlength="maxlength"
:minlength="minlength"
:show-word-limit="showWordLimit"
@input="input"
></el-input>
</template>
<script>
export default {
props: {
name: { type: String, required: false, default: '' },
type: { type: String, required: false, default: 'text' },
value: { type: String, required: false, default: '' },
placeholder: { type: String, required: false, default: '' },
disabled: { type: Boolean, required: false, default: false },
maxlength: { type: Number, required: false, default: null },
minlength: { type: Number, required: false, default: null },
showWordLimit: { type: Boolean, required: false, default: null },
},
methods: {
input(value) {
this.$emit('input', value)
},
},
}
</script>
Tailwind CSS でスタイルを微調整し、追加テキスト( additional
)を追加
<!-- components/molcules/el/InputLabel.vue -->
<template>
<atoms-el-col :span="span">
<div class="tw-text-xs tw-text-gray-600">
<slot></slot
><span class="tw-text-xxxs tw-text-gray-400"
><slot name="additional"></slot
></span>
</div>
<atoms-el-input
:name="name"
:value="value"
:type="type"
:placeholder="placeholder"
:disabled="disabled"
:maxlength="maxlength"
:minlength="minlength"
:show-word-limit="showWordLimit"
@input="input"
></atoms-el-input>
</atoms-el-col>
</template>
<script>
export default {
props: {
name: { type: String, required: false, default: '' },
type: { type: String, required: false, default: 'text' },
value: { type: String, required: false, default: '' },
placeholder: { type: String, required: false, default: '' },
disabled: { type: Boolean, required: false, default: false },
maxlength: { type: Number, required: false, default: null },
minlength: { type: Number, required: false, default: null },
showWordLimit: { type: Boolean, required: false, default: null },
span: { type: Number, required: false, default: null },
},
methods: {
input(value) {
this.$emit('input', value)
},
},
}
</script>
下記の 2 ファイルを作って、markdown で molcule コンポーネントの仕様を記述できることを確認します。
// components/molcules/el/InputLabel.stories.js
export default {
title: 'molcules/el/InputLabel',
}
// eslint-disable-next-line no-empty-pattern
export const Basic = ({}) => ({
template:
'<molcules-el-input-label :type="type" :value="value">Basic</molcules-el-input-label>',
props: {},
})
Basic.argTypes = {}
<!-- components/molcules/el/InputLabel.stories.mdx -->
import { Meta, Story, Props } from '@storybook/addon-docs/blocks'
import { action } from '@storybook/addon-actions'
import InputLabel from './InputLabel'
<Meta title="molcules/el/InputLabel" component={InputLabel} argTypes={{}} />
# 概要
概要を マークダウン で書けます
## サンプル
サンプルを Markdown に埋め込みできます
<Story name="Basic" args={{ type: 'text', value: 'Basic' }}>
{{
template:
'<molcules-el-input-label :type="type" :value="value">Basic</molcules-el-input-label>',
props: {
type: {
default: 'text',
},
value: {
default: 'Basic',
},
},
}}
</Story>
使用例
\```html
<molcules-el-input-label
v-model="プロパティの名前"
:span="8"
:maxlength="12"
:minlength="0"
show-word-limit
>
名前
<template #additional>追加テキスト</template>
</molcules-el-input-label>
\```
## Props
<Props of={InputLabel} />
この StoryBook のイメージは下記のようになります。
テストケースを作成して、作成したコンポーネントが正常に動くことを確認します。
import ElementUI from 'element-ui'
import Vuex from 'vuex'
import { mount, createLocalVue } from '@vue/test-utils'
import InputLabel from '@/components/molcules/el/InputLabel.vue'
import { createStore } from '../../../../.nuxt/store.js'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(ElementUI)
describe('components/modules/el/InputLabel.vue', () => {
let wrapper, store
beforeEach(() => {
store = createStore()
wrapper = mount(InputLabel, {
store,
localVue,
})
})
describe('template', () => {
test('入力フォームが存在すること', () => {
const label = wrapper.find('div')
expect(label.element.tagName).toBe('DIV')
const input = wrapper.find('input[type="text"]')
expect(input.element.tagName).toBe('INPUT')
})
})
describe('props', () => {
test('propsへ値を設定 ➡︎ 入力フォーム操作 ➡︎ emitで値を受け取れること', async () => {
await wrapper.setProps({
value: 'テストデータ',
})
expect(wrapper.vm.value).toBe('テストデータ')
wrapper.find('input[type="text"]').setValue('テストデータ2')
expect(wrapper.emitted('input')[0][0]).toBe('テストデータ2')
// storeの値をテストしたい場合に追記する
// expect(store.getters['organisms/ストアのファイル名/Getterの名前']).toBe(
// 'テストしたい値'
// )
})
})
})
テストを実行したところ、無事、 PASS になりました!
PASS test/components/molcules/el/InputLabel.spec.js
下記の Nuxt.js 特有の 2 点の問題で、苦労してしまいました。
力技ですが、.nuxt/store.js
を、テストケース内で import して createStore
関数を実行して store を作成することで解決します。
// test/components/molcules/el/InputLabel.spec.js
import { createStore } from '../../../../.nuxt/store.js'
// 省略
beforeEach(() => {
store = createStore()
wrapper = mount(InputLabel, {
store,
localVue,
})
})
// 省略
見た目は悪いですが、 store をテストケースに一から書かなくて良くなります。
同じく、力技ですが .nuxt/plugin.js
を、テスト実行前に import することで解決します。
// jest.setup.js
import './.nuxt/components/plugin.js' // 追加
// jest.config.js
module.exports = {
setupFilesAfterEnv: ['./jest.setup.js'] // 追加
}
こちらも、テストケースでコンポーネントをインポートする手間がなくなります。
Tailwind CSS のビルド時間が早く、ブラウザの開発者モードも動作が軽い(JIT モードのおかげ)
ElementUI を、Tailwind CSS のw-[100px]
やm-[10px]
などを使って、微妙な位置調整が簡単にシンプルにできる
先頭に!をつける(!w-1
)だけで!important
の指定ができる
StoryBook のビルド&起動が遅い
StoryBook と Tailwind CSS の postcss が競合しているので、 StoryBook のビルドで警告がたくさん出る
今、この構成の新規システムを開発中ですが、開発がかなり捗っているので、
今後も、既存プロダクトのリニューアルなどで使用してみたいなと考えています。
最後まで記事を見ていただき、ありがとうございました!!
https://github.com/tailwindlabs/tailwindcss/releases/tag/v2.2.0
https://github.com/tailwindlabs/tailwindcss/releases/tag/v2.1.0