vue-custom-elementsを利用したVueコンポーネントのカスタム要素化


# 背景

*vue-cli*を使ったVueコンポーネントの*web-component*化は、以下のような*package.json*の書き換えで簡単に実現できたのですが、

```package.json
  "scripts": {
    "build": "vue-cli-service build --target wc --name hoge-component",
  },
```

UIフレームワークとして*Vuetify*を使ったコードなどで、スタイルが適用されない問題の壁を越えられず。。

仕方ないので、Vueコンポーネントのラッパーライブラリ*vue-custom-elements*を試してみました。

# *vue-custom-element*

https://github.com/karol-f/vue-custom-element

Vueコンポーネントをカスタム要素化してくれるライブラリらしいです。

最初、vue-cliで作ったプロジェクトに対して、ライブラリを適用しようとしてみましたが、カスタム要素化された状態の成果物を得ることができず、断念。

初心にかえって(?)、シンプルなVue環境で試した見たところ、それっぽい成果物を得ることが出来ました。

(参考) 最小構成のvue.jsの環境をvue cli使わずに構築する
https://qiita.com/ctoshiki/items/8e5b6e000adec5b332cb

# *vue-custom-element*を使ったサンプル

以下の3つのサンプルを作ってみました。

- シンプルなVueのComponentをカスタム要素化したサンプル
- Element-UIを使っているComponentをカスタム要素化したサンプル
- Vuetifyを使っているComponentをカスタム要素化したサンプル

### シンプルなVueのComponentをカスタム要素化したサンプル

#### 作ったサンプル
https://github.com/yusuke-ka/sample-vce-vue

#### Note
下記のサイト参考にさせていただいて、問題なく作成できました。 (ボタンコンポーネント)
https://qiita.com/doutori/items/09e36b7efa8212ba7044

```index.ts
import Vue from "vue";
import vueCustomElement from "vue-custom-element";
import Button from "./button.vue";

Vue.config.ignoredElements = ["vce-button"];
Vue.use(vueCustomElement);
Vue.customElement("vce-button", Button, {
  // shadowDOM化すると中のCSSが効かず、ここで指定したCSSが効く
  shadow: true,
  shadowCss: `
      button {
      font-size: 8px;
      cursor: pointer;
      color: magenta;
      padding: 1em 1em;
      background: PaleGreen;
    `,
});
```

```button.vue
<template>
  <div>
    <button>{{ text }} {{ message }}</button>
  </div>
</template>

<script>
export default {
  name: "button",
  props: {
    message: String,
  },
  data() {
    return {
      text: "button",
    };
  },
};
</script>
```


### Element-UIを使っているComponentをカスタム要素化したサンプル

#### 作ったサンプル
https://github.com/yusuke-ka/sample-vce-element-ui

#### Note
本家GitHubのコードにあるサンプルを見ながら作成。 (テーブルコンポーネント)
https://github.com/karol-f/vue-custom-element/tree/master/demo/components

注意点としては、カスタム要素化する際に、「shadow: true」を指定すると、CSSが効かなくなりスタイルが崩れることぐらいでしょうか。

```index.ts
import Vue from "vue";
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
import vueCustomElement from "vue-custom-element";
import Table from "./table.vue";

Vue.use(vueCustomElement);
Vue.use(ElementUI);

Vue.config.ignoredElements = ["vce-table"];
Vue.customElement("vce-table", Table); // shadowDOM化すると中のCSSが効かない
```

```table.vue
<template>
  <div class="card card--primary">
    <h4>{{ message }}</h4>

    <el-table :data="tableData" style="width: 100%">
      <el-table-column prop="prop" label="Prop name"> </el-table-column>
      <el-table-column prop="value" label="Value">
        <template slot-scope="scope">
          <div slot="reference">
            <strong>{{ scope.row.value }}</strong>
          </div>
        </template>
      </el-table-column>
      <el-table-column prop="type" label="typeof">
        <template slot-scope="scope">
          <div slot="reference">
            <el-tag type="gray">{{ scope.row.type }}</el-tag>
          </div>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>
export default {
  props: {
    prop1: {
      required: true,
    },
    prop2: {},
    prop3: {},
    stringProp: {
      type: String,
    },
    booleanProp: {
      type: Boolean,
    },
    numberProp: {
      type: Number,
    },
    longPropName: {},
    objectProp: {},
    arrayProp: {},
  },
  data() {
    return {
      message: "Custom Element By Vue + Element UI",
    };
  },
  computed: {
    tableData() {
      const data = [
        {
          prop: "prop1",
          value: JSON.stringify(this.prop1),
          type: typeof this.prop1,
        },
        {
          prop: "prop2",
          value: JSON.stringify(this.prop2),
          type: typeof this.prop2,
        },
        {
          prop: "prop3",
          value: JSON.stringify(this.prop3),
          type: typeof this.prop3,
        },
        {
          prop: "stringProp (type: String)",
          value: this.stringProp,
          type: typeof this.stringProp,
        },
        {
          prop: "booleanProp (type: Boolean)",
          value: this.booleanProp,
          type: typeof this.booleanProp,
        },
        {
          prop: "numberProp (type: Number)",
          value: this.numberProp,
          type: typeof this.numberProp,
        },
        {
          prop: "long-prop-name",
          value: JSON.stringify(this.longPropName),
          type: typeof this.longPropName,
        },
      ];
      this.objectProp &&
        data.push({
          prop: "objectProp",
          value: this.objectProp,
          type: typeof this.objectProp,
        });
      this.arrayProp &&
        data.push({
          prop: "arrayProp",
          value: this.arrayProp,
          type: typeof this.arrayProp,
        });
      return data;
    },
  },
  created() {
    /* eslint-disable no-console */
    console.log("demo-basic created()");
  },
};
</script>
```

※中身は本家サンプルのコピー


### Vuetifyを使っているComponentをカスタム要素化したサンプル

#### 作ったサンプル
https://github.com/yusuke-ka/sample-vce-vuetify

#### Note
VueコンポーネントはVuetifyのUIコンポーネント(treeview)のサンプルコードを参考に作成 (ツリーコンポーネント)
https://vuetifyjs.com/ja/components/treeview/

```index.ts
import Vue from "vue";
import Vuetify from "vuetify";
import vueCustomElement from "vue-custom-element";
import Tree from "./tree.vue";

Vue.use(Vuetify);
Vue.use(vueCustomElement);

Vue.config.ignoredElements = ["vce-tree"];
Vue.customElement("vce-tree", Tree); // shadowDOM化すると中のCSSが効かない
```

```tree.vue
<template>
  <div id="app">
    <v-app id="inspire">
      <v-treeview selectable color="warning" :items="items"></v-treeview>
    </v-app>
  </div>
</template>

<script>
// vuetifyやcssは,、index.tsではなく、ここでimportしないと正しく適用されない模様。
// "vuetify"ではなく"vuetify/lib"にしないと、デザインが微妙に異なる
import Vuetify from "vuetify/lib";
import "material-design-icons-iconfont/dist/material-design-icons.css";

export default {
  el: "#app",
  vuetify: new Vuetify({
    icons: {
      iconfont: "md",
    },
  }),
  data: () => ({
    items: [
      {
        id: 1,
        name: "Applications :",
        children: [
          { id: 2, name: "Calendar : app" },
          { id: 3, name: "Chrome : app" },
          { id: 4, name: "Webstorm : app" },
        ],
      },
      {
        id: 5,
        name: "Documents :",
        children: [
          {
            id: 6,
            name: "vuetify :",
            children: [
              {
                id: 7,
                name: "src :",
                children: [
                  { id: 8, name: "index : ts" },
                  { id: 9, name: "bootstrap : ts" },
                ],
              },
            ],
          },
          {
            id: 10,
            name: "material2 :",
            children: [
              {
                id: 11,
                name: "src :",
                children: [
                  { id: 12, name: "v-btn : ts" },
                  { id: 13, name: "v-card : ts" },
                  { id: 14, name: "v-window : ts" },
                ],
              },
            ],
          },
        ],
      },
      {
        id: 15,
        name: "Downloads :",
        children: [
          { id: 16, name: "October : pdf" },
          { id: 17, name: "November : pdf" },
          { id: 18, name: "Tutorial : html" },
        ],
      },
      {
        id: 19,
        name: "Videos :",
        children: [
          {
            id: 20,
            name: "Tutorials :",
            children: [
              { id: 21, name: "Basic layouts : mp4" },
              { id: 22, name: "Advanced techniques : mp4" },
              { id: 23, name: "All about app : dir" },
            ],
          },
          { id: 24, name: "Intro : mov" },
          { id: 25, name: "Conference introduction : avi" },
        ],
      },
    ],
  }),
};
</script>
```

# カスタム要素化したコンポーネントの呼び出し

カスタム要素化したコンポーネントを外部のファイルから呼び出す際のやり方。
どのサンプルも下記のように簡単に呼び出せます。

ボタンのやつ (simple-vue)

```index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Custom Element By Vue</title>
    <script src="dist/bundle.js"></script>
  </head>
  <body>
    <vce-button message="test"></vce-button>
  </body>
</html>
```

テーブルのやつ (Element UI)

```index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Custom Element By Vue + Element UI</title>
    <script src="dist/bundle.js"></script>
  </head>
  <body>
    <vce-table
      prop1="1"
      prop2="example text"
      prop3="true"
      string-prop="123"
      boolean-prop="false"
      number-prop="123"
      long-prop-name="long name"
    >
    </vce-table>
  </body>
</html>
```

ツリーのやつ (Vuetify)

```index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Custom Element By Vue + Vuetify</title>
    <script src="dist/bundle.js"></script>
  </head>
  <body>
    <vce-tree></vce-tree>
  </body>
</html>
```


vue-cliで生成したやつは、呼び出し元で下記のような記述を足してやる必要がありましたが、これらは必要ありませんでした。
その代わり、カスタム要素化したコンポーネントのサイズが大きくなるので、その点は注意が必要そうです。

```
<script src="https://unpkg.com/vue"></script>
```