Material Design for Bootstrap 4 (Vue version)を導入してみる


Angular5で使ってたMDBootstrapのVueバージョンがあったので入れてみた。
ナビゲーションがMDBootstrapそのままだとvue-routerのrouter-linkが使えなくて、
コンポーネントを少しローカルで変更した。

完成した画面

  • vue-cliの初期画面に上にナビゲーションくっ付けた

必要なパッケージのインストール

yarn add mdbootstrap bootstrap-vue classnames
yarn add @types/classnames --dev

テンプレートのコンポーネントをgit cloneで落としてくる

  • Angular、Reactはパッケージ追加でいけるけど、Vueは何故かテンプレートをgit cloneしてプロジェクト作成するか、vue-cliでテンプレートを使って新規作成する事になってて、既存プロジェクトへの導入が不便だった
git clone https://github.com/mdbootstrap/Vue-Bootstrap-with-Material-Design.git mdb

テンプレートからvue-cliで作った既存プロジェクトにソースをコピー

  • コンポーネント
mkdir ~/works/todo/src/conponents/mdbootstrap
cp ~/works/mdb/src/components/* ~/works/todo/src/mdbootstrap
  • mixins
mkdir ~/works/todo/src/mixins
cp ~/works/mdb/src/mixins/* ~/works/todo/src/mixins

main.tsにリップル効果のCSSをimport追加

src/main.ts
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import './registerServiceWorker';

import 'bootstrap/dist/css/bootstrap.css';
import 'mdbootstrap/css/mdb.css';
+ import './components/mdbootstrap/Waves.css';

Vue.config.productionTip = false;

new Vue({
  router,
  store,
  render: (h) => h(App),
}).$mount('#app');

App.vueのナビゲーションをMDBootstrapのナビゲーションに変更

src/App.vue
<template>
  <div id="app">
    <!--Navbar-->
    <navbar position="top" class="indigo navbar-dark" name="ToDo" to="/" scrolling>
      <navbar-collapse>
        <navbar-nav>
          <navbar-item to="/" active exact waves-fixed>Home</navbar-item>
          <navbar-item to="/about" waves-fixed>About</navbar-item>
          <navbar-item to="/tasks" waves-fixed>Tasks</navbar-item>
          <!-- Dropdown -->
          <dropdown tag="li" class="nav-item">
            <dropdown-toggle tag="a" navLink color="indigo" waves-fixed>Dropdown</dropdown-toggle>
            <dropdown-menu>
              <dropdown-item>Action</dropdown-item>
              <dropdown-item>Another action</dropdown-item>
              <dropdown-item>Something else here</dropdown-item>
            </dropdown-menu>
          </dropdown>
        </navbar-nav>
          <!-- Search form -->
        <form class="form-inline">
          <mdinput type="text" placeholder="Search" aria-label="Search" label navInput waves waves-fixed/>
        </form>
      </navbar-collapse>
    </navbar>

    <div class="container">
      <router-view/>
    </div>
  </div>
</template>

<script>
import Navbar from "@/components/mdbootstrap/Navbar.vue";
import NavbarItem from "@/components/mdbootstrap/NavbarItem.vue";
import NavbarNav from "@/components/mdbootstrap/NavbarNav.vue";
import NavbarCollapse from "@/components/mdbootstrap/NavbarCollapse.vue";
import Container from "@/components/mdbootstrap/Container.vue";
import Dropdown from "@/components/mdbootstrap/Dropdown.vue";
import DropdownItem from "@/components/mdbootstrap/DropdownItem.vue";
import DropdownMenu from "@/components/mdbootstrap/DropdownMenu.vue";
import DropdownToggle from "@/components/mdbootstrap/DropdownToggle.vue";
import drop from "@/mixins/drop";
import Mdinput from "@/components/mdbootstrap/MdInput.vue";

export default {
  components: {
    Navbar,
    NavbarItem,
    NavbarNav,
    NavbarCollapse,
    Container,
    Dropdown,
    DropdownItem,
    DropdownMenu,
    DropdownToggle,
    Mdinput
  },
  mixins: [drop]
};
</script>

<style lang="scss">
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

.navbar .dropdown-menu a:hover {
  color: inherit !important;
}
.fixed-top {
  position: sticky;
}
</style>

Navbarのコンポーネントをrouter-linkで遷移させるように変更

  • activeのメニューの背景色を変える部分も動かなかったので変更した。App.vue側の/のルーティングにはexact属性を付けてある。
src/components/mdbootstrap/Navbar.vue
<template>
  <nav :class="className" :is="tag">
-    <a :href="href" class="navbar-brand">{{name}}
+    <router-link :to="to" class="navbar-brand">{{name}}
      <img v-if="src" :src="src" :alt="alt"/>
-    </a>
+    </router-link>
    <button class="navbar-toggler" type="button" data-toggle="collapse" :data-target="target" aria-controls="navbarSupportedContent"
        aria-expanded="false" aria-label="Toggle navigation" v-on:click="toggle">
      <span class="navbar-toggler-icon"></span>
    </button>
    <slot></slot>
  </nav>
</template>


<script>
//
//
//
//
//
//
//
//
//
//
//
//
//
//

import classNames from "classnames";

export default {
  props: {
    tag: {
      type: String,
      default: "nav"
    },
    expand: {
      type: String,
      default: "large"
    },
    position: {
      type: String
    },
-    href: {
+    to: {
      type: String
    },
    src: {
      type: String
    },
    alt: {
      type: String
    },
    name: {
      type: String
    },
    target: {
      type: String,
      default: "#navbarSupportedContent"
    },
    scrolling: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      className: classNames(
        "navbar",
        "navbar-dark",
        this.expand === "small"
          ? "navbar-expand-sm"
          : this.expand === "medium"
            ? "navbar-expand-md"
            : this.expand === "large" ? "navbar-expand-lg" : "navbar-expand-lx",
        this.position === "top"
          ? "fixed-top"
          : this.position === "bottom" ? "fixed-bottom" : "",
        this.scrolling ? "scrolling-navbar" : ""
      ),
      scrolled: false,
      toggleClicked: true
    };
  },
  methods: {
    toggle(e) {
      if (this.toggleClicked) {
        this.collapse.classList.toggle("show-navbar");
        this.collapse.classList.remove("hide-navbar");
        this.collapse.classList.toggle("collapse");
        this.collapse.style.overflow = "hidden";
        this.collapseOverflow = setTimeout(() => {
          this.collapse.style.overflow = "initial";
        }, 300);
        this.toggleClicked = false;
      } else {
        this.collapse.classList.add("hide-navbar");
        this.collapse.classList.toggle("show-navbar");
        this.collapse.style.overflow = "hidden";
        this.collapseOverflow = setTimeout(() => {
          this.collapse.classList.toggle("collapse");
          this.collapse.style.overflow = "initial";
        }, 300);
        this.toggleClicked = true;
      }
    },
    handleScroll() {
      if (window.scrollY > 100 && this.scrolled === false) {
        this.$el.style.paddingTop = 5 + "px";
        this.$el.style.paddingBottom = 5 + "px";
        this.scrolled = true;
      } else if (window.scrollY < 100 && this.scrolled === true) {
        this.$el.style.paddingTop = 12 + "px";
        this.$el.style.paddingBottom = 12 + "px";
        this.scrolled = false;
      }
    }
  },
  mounted() {
    this.collapse = this.$el.children.navbarSupportedContent;
    this.collapse.classList.add("collapse");
  },
  created() {
    window.addEventListener("scroll", this.handleScroll);
  },
  destroyed() {
    window.removeEventListener("scroll", this.handleScroll);
  }
};
</script>

<style scoped>
.scrolling-navbar {
  transition: padding 0.5s;
}
.nav-item {
  position: relative;
}
</style>

NavbarItemのコンポーネントをrouter-linkで遷移させるように変更

src/components/mdbootstrap/NavbarItem.vue
<template>
  <li :is="tag" :class="[className, {'ripple-parent': waves}]" @click="wave">
-    <a :href="href" class="nav-link"><slot></slot></a>
+    <router-link :to="to" class="nav-link" :exact="exact"><slot></slot></router-link>
  </li>
</template>

<script>
//
//
//
//
//
//

import classNames from "classnames";
import waves from "../../mixins/waves";

export default {
  props: {
    tag: {
      type: String,
      default: "li"
    },
    active: {
      type: Boolean,
      default: false
    },
-    href: {
+    to: {
      type: String,
      default: "#"
    },
    waves: {
      type: Boolean,
      default: true
    },
    wavesFixed: {
      type: Boolean,
      default: false
    },
+    exact: {
+      type: Boolean,
+      default: false
+    }
  },
  data() {
    return {
      className: classNames("nav-item", this.active ? "active" : "")
    };
  },
  mixins: [waves]
};
</script>

<style scoped>
+ .navbar.navbar-dark .breadcrumb .nav-item > .nav-link.router-link-active,
+ .navbar.navbar-dark .navbar-nav .nav-item > .nav-link.router-link-active {
+   background-color: rgba(255, 255, 255, 0.1);
}
</style>

これで完成。

感想

  • Angular5でMDBootstrap使った時はパッケージ追加してソース修正でやれたけど、Vue.jsでは面倒だった。
  • App.vueをTypeScriptにしたら、エラーで動かなかった。型定義は用意されてないみたいなのでJsでやるしかない。

ERROR in /Users/uwettie/works/todo-client/src/App.vue
43:18 Could not find a declaration file for module '@/mixins/drop'. '/Users/uwettie/works/todo-client/src/mixins/drop.js' implicitly has an 'any' type.
    41 | import DropdownMenu from "@/components/mdbootstrap/DropdownMenu.vue";
    42 | import DropdownToggle from "@/components/mdbootstrap/DropdownToggle.vue";
  > 43 | import drop from "@/mixins/drop";
       |                  ^
    44 | import Mdinput from "@/components/mdbootstrap/MdInput.vue";
    45 |
    46 | export default {
Version: typescript 2.8.3, tslint 5.9.1

Vuetify

Vue.jsのマテリアルデザインのコンポーネントフレームワークは、Vuetifyというのが有名らしい。
そちらでやる方がいいんだろうか。
Vue.js専用っぽいから廃れちゃったら嫌だなと思っちゃうけど大丈夫なんだろうか。

作ったソース