関数型コンポーネントも含めたVue(バージョン2系)のコンポーネントの書き方をサンプルコードと共に洗い出してみた。
基本のコンポーネント
Vueのコンポーネントで一番オーソドックスな単一ファイル形式。
<template>
<div>Basic Vue Component</div>
</template>
<script>
export default {
name: 'Basic'
};
</script>
<style>
</style>
以下のコードは前述の単一ファイル形式と同じ意味になる。
const Basic = {
name: 'Basic',
template: '<div>Basic Vue Component</div>'
}
グローバル登録とローカル登録
Vue.componentを利用するとグローバル登録したコンポーネントになる。
Vue.component('Global', {
template: '<div>Global Vue Component</div>'
})
単一ファイルのコンポーネントでもグローバル登録おk。
import Global from "@/components/Global";
Vue.component("Global", Global);
ローカル登録はコンポーネントオプションのcomponentsを利用する。
import Basic from "@/components/Basic";
export default {
name: "App",
components: {
Basic,
}
};
Scoped CSSを利用する
styleタグでscoped属性をつけるとScoped CSSが発動する
<template>
<div class="normal">Normal Vue Component (Scoped CSS)</div>
</template>
<script>
export default {
name: "Normal"
};
</script>
<style scoped>
.normal {
color: blue;
border: solid 1px;
padding: 1rem;
}
</style>
CSS Modulesを利用する
scopedの代わりにmodule属性をstyleタグにつけるとCSS Modulesモードが発動する。
CSSモジュールモードになると、$styleという算出プロパティがコンポーネントに注入される。
<template>
<div :class="$style.module">Normal Vue Component (CSS Modules)</div>
</template>
<script>
export default {
name: "CSSModule"
};
</script>
<style module>
.module {
color: orange;
border: solid 1px;
padding: 1rem;
}
</style>
ハイフネートとキャメルケースは区別される。
ディレクティブやプロパティみたいに変換はされない。
<template>
<div :class="$style.isHot">.isHot というクラス</div>
<div :class="$style['is-hot']">.is-hot というクラス</div>
</template>
//...
<style module>
.isHot {
color: blue;
}
.is-hot {
color: red;
}
</style>
描画関数を利用するコンポーネント
templateタグの代わりに描画関数を使うこともできる。
これはコンパイル後の状態を書いてる感じ…。
プロパティの値でタグを変えるみたいなテクいことしたい時には便利かもしれない。
<script>
export default {
name: "NormalRender",
render(h) {
return h(
"div",
{
class: [this.$style["is-render"]]
},
"Use Render Vue Component (CSS Modules)"
);
}
};
</script>
<style module>
.is-render {
color: goldenrod;
border: solid 1px;
padding: 1rem;
}
</style>
子コンポーネントの利用
グローバルまたはローカルに登録してあるコンポーネントを利用できる。
子コンポーネントにつけた属性は継承される。
<template>
<div class="normal">Normal Vue Component (Scoped CSS)
<p>Use Child Component(Heart)→
<Heart class="border"/>
</p>
</div>
</template>
<script>
import Heart from "@/components/Heart";
export default {
name: "HasChild",
components: { Heart }
};
</script>
<style scoped>
.normal {
color: blue;
border: solid 1px;
padding: 1rem;
}
.border {
border: solid 1px red;
}
p {
text-indent: 1rem;
}
</style>
親コンポーネントから子コンポーネントへのデータの受け渡しはpropsの利用が必須で、子コンポーネント側で受け取る体制になっている必要がある。
<template>
<div class="has-props">Component data passing.
<p>I like
<child/>! (default)
</p>
<p>I like
<child :text="text"/>!
</p>
</div>
</template>
<script>
const Child = {
name: "HasProps",
props: {
text: {
type: String,
default: "Dog"
}
},
template: "<strong>{{text}}</strong>"
};
export default {
name: "PropsChild",
components: { Child },
data() {
return {
text: "Cat"
};
}
};
</script>
<style>
.has-props {
color: sienna;
border: solid 1px;
padding: 1rem;
}
</style>
関数型コンポーネント
単一ファイルコンポーネントで、templateタグにfunctional属性をつけると、関数型コンポーネントになる。
<template functional>
<div class="functional">Functional Template Vue Component (Scoped CSS)
<p :class="$style.box">Child Element (CSS Modules)</p>
</div>
</template>
<script>
export default {
name: "Functional"
};
</script>
<style scoped>
.functional {
color: green;
border: solid 1px;
padding: 1rem;
}
</style>
<style module>
.box {
color: darkolivegreen;
text-indent: 1rem;
}
</style>
functionalオプションの利用
functionalオプションをtrueに設定しても関数型コンポーネントになる。
これだとjsファイルでもコンポーネントが作れる。
export default {
name: "FucOption",
functional: true,
render(h) {
return (
<div style={{ color: "purple", border: "solid 1px", padding: "1rem" }}>
Functional Option Render Vue Component (Inline CSS)
</div>
);
}
};
描画関数とJSX
render関数ではcreateElement(hで省略してるやつ)を使って要素をいちいち作る書き方をするが、JSXを利用してテンプレートぽく書くこともできる。
この2つは共存もできる。
<script>
export default {
name: "FunctionalRender",
functional: true,
render(h) {
return h(
"div",
{
staticClass: "functional-render"
},
[
"Functional Render Vue Component(Scoped CSS)",
<p style={{ color: "darkorchid", textIndent: "1rem" }}>
JSX Child Element (Inline CSS)
</p>,
]
);
}
};
</script>
<style scoped>
.functional-render {
color: orchid;
border: solid 1px;
padding: 1rem;
}
</style>
描画関数のコンテキスト
前のサンプルコードではrender(h)と省略したが、描画関数には第二引数でコンテキストが渡される
- props: 提供されるプロパティのオブジェクト
- children: 子 VNode の配列
- slots: slots オブジェクトを返す関数
- scopedSlots: (2.6.0 以降) スコープ付きスロットを公開するオブジェクト。通常のスロットも関数として公開します
- data: createElement の第 2 引数としてコンポーネントに渡される全体のデータオブジェクト
- parent: 親コンポーネントへの参照
- listeners: (2.3.0 以降) 親に登録されたイベントリスナーを含むオブジェクト。これは単純に data.on のエイリアスです
- injections: (2.3.0 以降) inject オプションで使用する場合、これは解決されたインジェクション(注入)が含まれます
CSS Modulesを利用した場合、$styleがコンテキストに含まれるので、render内で使うことができる。
<script>
export default {
name: "FunctionalCssModules",
functional: true,
render(h, { $style }) {
return h(
"div",
{ class: $style.red },
"Functional Render Vue Component (CSS Modules)"
);
}
};
</script>
<style module>
.red {
color: red;
border: 1px solid;
padding: 1rem;
}
</style>
vue-styled-componentsの利用
CSS Modulesを利用する場合vueファイルにして単一ファイルコンポーネント形式にする必要があるけど、
vue-styled-componentsを使えばjsファイルで同じことができる。
つまり全部JSで書ける。
import styled from "vue-styled-components";
const Wrapper = styled.div`
color: tomato;
border: solid 1px;
padding: 1rem;
`;
const Child = styled.p`
text-indent: 1rem;
color: darkred;
`;
export default {
render(h) {
return h(Wrapper, {}, [
"Functional Render Vue Component (vue-styled-component)",
<Child>JSX Child Element</Child>
]);
}
};
全部この書き方するならReactでいいのでは感🤔
子コンポーネントの利用
関数型コンポーネントで描画関数を使っている場合は、componentsでの設定をしなくても利用できる。
<script>
import Heart from "@/components/Heart";
import FunctionalHeart from "@/components/Functional/Heart";
export default {
name: "FunctionalRender",
functional: true,
render(h) {
return h(
"div",
{
staticClass: "functional-render"
},
[
<p>
Use Child Component(Heart) → <Heart />
</p>,
h("p", {}, [
"Use Child Component(Functional/Heart)→",
h(FunctionalHeart, {
staticClass: "border"
})
])
]
);
}
};
</script>
<style scoped>
.functional-render {
color: orchid;
border: solid 1px;
padding: 1rem;
}
</style>
関数型コンポーネントを子コンポーネントとして利用する
templateタグでfunctional属性を利用した関数型コンポーネントが子であるとき、
親コンポーネントでクラス名を設定しても引き継がれない。
<template functional>
<span>💙</span>
</template>
<script>
export default {
name: "FunctionalHeart"
};
</script>
contextで渡されるstaticClassとclassを:class属性で使うと親で設定したクラスが有効になる。
<template functional>
<span class="blue-heart" :class="[data.class,data.staticClass]">💙</span>
</template>
<script>
import { mergeData } from "vue-functional-data-merge";
export default {
name: "FunctionalHeartFixed"
};
</script>
親の方で >>> を使うという手も…:
>>> .bold {
font-weight: bold;
}
※Sassなどのプリプロセッサでは/deep/になる。