wordpress_blog

This is a dynamic to static website.

Vue3 複習06

進階 API

進階章節說明

操作 DOM 元素技巧 refs

// HTML
<div id="app">
  <h3>使用 ref 定義元素</h3>
  <input type="text" ref="inputDom" />

  <h3>使用 ref 取得元件內的資訊</h3>
  <button type="button" @click="getComponentInfo">取得元件資訊</button>
  <card ref="card"></card>

  <h3>進階,使用 ref 搭配 Bootstrap</h3>
  Bootstrap Modal:
  <a href="https://getbootstrap.com/docs/5.0/components/modal/"
    >https://getbootstrap.com/docs/5.0/components/modal/</a
  >
  <button @click="openModal">開啟 Bootstrap Modal</button>
  <!-- 2. 定義 ref modal 屬性 -->
  <div class="modal" tabindex="-1" ref="modal">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title">Modal title</h5>
          <button
            type="button"
            class="btn-close"
            data-bs-dismiss="modal"
            aria-label="Close"
          ></button>
        </div>
        <div class="modal-body">
          <p>Modal body text goes here.</p>
        </div>
        <div class="modal-footer">
          <button
            type="button"
            class="btn btn-secondary"
            data-bs-dismiss="modal"
          >
            Close
          </button>
          <button type="button" class="btn btn-primary">Save changes</button>
        </div>
      </div>
    </div>
  </div>
</div>
// JS
<!-- 另外載入 Bootstrap CDN -->
<script>
  const app = Vue.createApp({
    data() {
      return {
        // 1. 先定義一個 bsModal 變數
        bsModal: "",
      };
    },
    methods: {
      getComponentInfo() {
        console.log(this.$refs.card);
        this.$refs.card.title = "被更新的元件標題";
      },
      openModal() {
        // 4. 加上 show() 把 bootstrap Modal 打開
        this.bsModal.show();
      },
    },
    mounted() {
      console.log(this.$refs, this.$refs.inputDom);
      this.$refs.inputDom.focus();

      // 3. 使用 this.bsModal 建立一個 new bootstrap Modal 實體
      // 3-1. this.refs 指到我們在畫面上這個的元素
      // 3-2. new bootstrap.Modal 實體會存在 this.bsModal
      // 3-3. this.bsModal 可以在不同作用域取用
      this.bsModal = new bootstrap.Modal(this.$refs.modal);
    },
  });

  app
    .component("card", {
      data() {
        return {
          title: "文件標題",
          content: "文件內文",
        };
      },
      template: `
                  <div class="card" style="width: 18rem;">
                    <div class="card-body">
                      <h5 class="card-title">{{ title }}</h5>
                      <p class="card-text">{{ content }}</p>
                    </div>
                  </div>
                `,
    })
    .mount("#app");
</script>

自訂元件生成位置 teleport

// HTML
<div id="app">
  <div id="target"></div>

  <h3>Teleport 自訂義元件位置</h3>
  結構:<code>&lt;teleport to="{ target }"&gt;</code>
  <card></card>

  <h3>使用限制(錯誤情境)</h3>
  <card2></card2>

  <h3>實用技巧(取代標題、多個)</h3>
  <new-title></new-title>
</div>
// JS
<script>
  const app = Vue.createApp({});

  app.component("card", {
    data() {
      return {
        title: "文件標題",
        content: "文件內文",
        toggle: false,
      };
    },
    template: `
      <div class="card" style="width: 18rem;">
        <div class="card-body">
          <h5 class="card-title">{{ title }}</h5>
          <p class="card-text">{{ content }}</p>
          <button type="button" @click="toggle = !toggle">切換元素顯示</button>
        </div>
      </div>
      <teleport to="#target">
        <div v-if="toggle" class="alert alert-danger">被招喚的元素</div>
      </teleport>
    `,
    props: ["item"],
  });

  app.component("card2", {
    template: `
      <teleport to=".col-md-3">
        <div class="alert alert-danger">被招喚的元素</div>
      </teleport>
    `,
  });

  app.component("new-title", {
    template: `
      <teleport to="title"> - 新增的標題片段</teleport>
      <teleport to="h1"> - 新增的文字片段</teleport>
    `,
  });

  app.mount("#app");
</script>

跨層級資料傳遞 provide

// HTML
<div id="app">
  <h2>多層級資訊傳遞</h2>
  <ul>
    <li>在外層加入 provide</li>
    <li>內層元件補上 inject</li>
  </ul>
  <card></card>
  {{ user.name }}

  <h3>注意事項:響應式</h3>
</div>
// JS
<script>
  // 最內層
  const userComponent = {
    template: `
      <div>
        userComponent 元件:<br />
        {{ user.name }}, <br />
        {{ user.uuid }}
      </div>
    `,
    // 2. 內層元件補上 inject
    // 2-1. inject 是一個陣列
    // 2-2. inject 內容加上 user 這個名稱
    inject: ["user"],
    created() {
      console.log(this.user);

      // 如果根元件沒有使用 function return 則不能使用響應式
      this.user.name = "杰倫";
    },
  };

  // 最外層
  const app = Vue.createApp({
    data() {
      return {
        user: {
          name: "小明",
          uuid: 78163,
        },
      };
    },
    // 1. 在外層加入 provide
    // 1-1 函式、物件運作上有點不一樣
    // provide: {
    //   user: {
    //     name: "小明",
    //     uuid: 78163,
    //   },
    // },

    // 注意事項: 響應式
    // provide 物件改成函式
    // 實戰建議直接使用函式的結構
    provide() {
      return {
        user: this.user,
      };
    },
  });

  // 內層
  app.component("card", {
    data() {
      return {
        title: "文件標題",
        content: "文件內文",
        toggle: false,
      };
    },
    components: {
      userComponent,
    },
    inject: ["user"],
    template: `
      <div class="card" style="width: 18rem;">
        <div className="card-header">card 元件</div>
        <div class="card-body">
          {{ user.name }}
          <userComponent></userComponent>
        </div>
      </div>
    `,
  });

  app.mount("#app");
</script>

元件直接加入 v-model

// HTML
<div id="app">
  <h2>將元件內的值傳回 v-model(update:modelValue)</h2>
  <p>
    參考說明:<a
      href="https://vue3js.cn/docs/zh/guide/component-custom-events.html#v-model-%E5%8F%82%E6%95%B0"
      >https://vue3js.cn/docs/zh/guide/component-custom-events.html#v-model-%E5%8F%82%E6%95%B0</a
    >
  </p>
  {{ name }}
  <!-- 1. v-model:text="name" -->
  <custom-input v-model:text="name"></custom-input>

  <hr />
  <h2>多個 v-model</h2>
  {{ text }} {{ text2 }}
  <!-- 1. v-model:t1="text"、v-model:t2="text2 -->
  <custom-input2 v-model:t1="text" v-model:t2="text2"></custom-input2>
</div>
// JS
<script>
  const app = Vue.createApp({
    data() {
      return {
        name: "小明",

        text: "這是文字片段 1",
        text2: "這是文字片段 2",
      };
    },
  });

  // $emit('update:text', $event.target.value) 搭配 props,可以將更新後的值寫回 v-model 內
  app.component("custom-input", {
    props: ["text"],
    // 2-1. :value="text"
    // 2-2. @input="$emit('update:text', $event.target.value)"
    template: `
    <input type="text" 
    :value="text" 
    @input="$emit('update:text', $event.target.value)"
    class="form-control">
    `,
  });

  app.component("custom-input2", {
    props: ["t1", "t2"],
    // 2-1 :value="t1"、:value="t2"
    // 2-2 @input="$emit('update:t1', $event.target.value)"
    //     @input="$emit('update:t2', $event.target.value)"
    template: `
    <input type="text" 
    :value="t1"
    @input="$emit('update:t1', $event.target.value)"
    class="form-control">

    <input type="text" 
    :value="t2" 
    @input="$emit('update:t2', $event.target.value)"
    class="form-control">
    `,
  });

  app.mount("#app");
</script>

混合元件方法 mixins

// HTML
<div id="app">
  <h2>單一混合、多個混合</h2>
  <card></card>

  <p>重點:</p>
  <ul>
    <li>可以重複混合</li>
    <li>生命週期可以重複觸發</li>
    <li>同名的變數、方法則會被後者覆蓋</li>
  </ul>
</div>
// JS
<script>
  const mixComponent1 = {
    data() {
      return {
        name: "混合的元件",
      };
    },
    created() {
      console.log("混合的元件生命週期");
    },
  };
  const mixComponent2 = {
    data() {
      return {
        name: "混合的元件 2",
      };
    },
    created() {
      console.log("混合的元件生命週期 2");
    },
  };

  const app = Vue.createApp({});

  app.component("card", {
    // 優先順序
    // mixins 的優先順序是比較低的
    // 元件本身的優先順序會是比較高的
    data() {
      return {
        name: "card",
      };
    },
    template: `
      <div class="card">
        <div class="card-body">{{ name }}</div>
      </div>
    `,
    // 1-1. mixins 是一個陣列
    // 1-2. 陣列內容把其他元件的內容混合進來
    mixins: [mixComponent1, mixComponent2],
    created() {
      console.log("card 的元件生命週期");
    },
  });

  app.mount("#app");
</script>

擴展元件方法 extend

// HTML
<div id="app">
  <h2>單一擴展</h2>
  <!-- <card></card> -->

  <h3>權重</h3>
  <card2></card2>

  <h3>重點:</h3>
  <ul>
    <li>擴展為單一擴展</li>
    <li>生命週期可以與 mixins 重複觸發</li>
    <li>權重:元件屬性 > mixins > extend</li>
    <li>同名的變數、方法則會依據權重決定</li>
  </ul>
</div>
// JS
<script>
  const extendComponent1 = {
    data() {
      return {
        name: "擴展的元件",
      };
    },
    created() {
      console.log("擴展的元件生命週期");
    },
  };
  const mixinComponent = {
    data() {
      return {
        name: "混合的元件",
      };
    },
    created() {
      console.log("混合的元件生命週期");
    },
  };

  const app = Vue.createApp({});

  app.component("card", {
    template: `
      <div class="card">
        <div class="card-body">{{ name }}</div>
      </div>
    `,
    // extends 後面直接加入一個物件,這物件是元件的本身
    extends: extendComponent1,
    created() {
      console.log("card 的元件生命週期");
    },
  });

  app.component("card2", {
    template: `
      <div class="card">
        <div class="card-body">{{ name }}</div>
      </div>
    `,
    data() {
      return {
        name: "元件資料狀態",
      };
    },
    mixins: [mixinComponent],
    extends: extendComponent1,
    created() {
      console.log("card 的元件生命週期");
    },
  });

  app.mount("#app");
</script>

自定義指令 directive

// HTML
<div id="app">
  <h2>自訂義指令</h2>
  <h3>用途</h3>
  <ul>
    <li>實戰中屬於進階功能,初學可先了解有此功能即可</li>
    <li>可從延伸套件中看到相關的運用</li>
    <li>多用於 HTML 上的便利操作,複雜功能還是會搭配元件</li>
  </ul>

  <h3>結構</h3>
  <ul>
    <li>v-{自訂義名稱}</li>
    <li>
      主要透過生命週期來觸發變動,可參考:<a
        href="https://vue3js.cn/docs/zh/guide/custom-directive.html#%E5%8A%A8%E6%80%81%E6%8C%87%E4%BB%A4%E5%8F%82%E6%95%B0"
        >https://vue3js.cn/docs/zh/guide/custom-directive.html#%E5%8A%A8%E6%80%81%E6%8C%87%E4%BB%A4%E5%8F%82%E6%95%B0</a
      >
    </li>
  </ul>
  <!-- 1. v-validator -->
  <input type="email" v-model="text" v-validator="'form-control'" />
</div>
// JS
<script type="module">
  const app = Vue.createApp({
    data() {
      return {
        text: "",
      };
    },
  });

  // 註冊指令
  app.directive("validator", {
    // directive 生命週期
    mounted(el, binding) {
      el.focus();

      // 將外部的值改為
      console.log(binding);
      el.className = binding.value;
    },
    updated: function (el, binding, vnode) {
      // el 元素本體
      // binding 綁定的資源狀態
      // vnode 虛擬 DOM 節點
      console.log("update", el, binding, vnode);
      const className = binding.value;

      // 尋找當前的 model 名稱(取得 key 值,並帶入第一個)
      const currentModel = Object.keys(binding.instance)[0];
      // console.log(currentModel);

      // 從當前 Model 取值
      const value = binding.instance[currentModel];
      console.log(currentModel, value);

      // Email validate
      // 正規表達式
      const re =
        /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;

      if (!re.test(value)) {
        el.className = `${className} is-invalid`;
      } else {
        el.className = `${className} is-valid`;
      }
    },
  });

  app.mount("#app");
</script>

<script>
  // ESM 版本的差異(需要 Webpack)
  // import { Field, Form, ErrorMessage } from 'vee-validate';
  //
  // export default {
  //   components: {
  //     Field,
  //     Form,
  //     ErrorMessage,
  //   },
  //   methods: {
  //     isRequired(value) {
  //       if (value && value.trim()) {
  //         return true;
  //       }
  //
  //       return 'This is required';
  //     },
  //   },
  // };
</script>

擴充插件 plugins

// HTML
<div id="app">
  <p>外部套件匯入方式</p>
  <ul>
    <li>載入方式:使用 CDN 或使用 npm</li>
    <li>
      運用方式:<a href="https://www.npmjs.com/package/vue-axios">app.use()</a>
      或
      <a
        href="https://vee-validate.logaretm.com/v4/guide/components/validation#field-level-validation"
        >元件的形式載入</a
      >
      啟用。(另有指令等各種 Vue 的語法形式)
    </li>
  </ul>

  <h3>使用外部套件注意事項:</h3>
  <ul>
    <li>需多注意可搭配的版本號</li>
    <li>更新頻率</li>
    <li>使用人數</li>
  </ul>

  <h3>範例:載入 VeeValidate 驗證套件</h3>
  <!-- 1-1. form 改成 v-form -->
  <!-- 1-2. input 改成 v-field -->
  <!-- 2. v-form 加上 v-slot -->

  <v-form @submit="onSubmit" v-slot="{ errors }">
    {{ errors }}

    <!-- 3-1. v-field 會加上規則 -->
    <!-- 3-4. name 也可以自定義名稱 -->
    <v-field
      name="欄位名稱"
      type="text"
      placeholder="Who are you"
      :rules="isRequired"
    ></v-field>
    <!-- 3-5. error-message -->
    <!-- 3-6. 在 error-message 加上 name="欄位名稱" 對應到 v-field -->
    <error-message name="欄位名稱">請填寫此欄位</error-message>

    <button>Submit</button>
  </v-form>

  <p>比對與 ESM 版本上的差異</p>
</div>
// JS
<script src="https://cdnjs.cloudflare.com/ajax/libs/vee-validate/4.1.17/vee-validate.min.js"></script>
<script type="module">
  console.log(VeeValidate);
  // VeeValidate CDN 版本
  const app = Vue.createApp({
    components: {
      // Components were renamed to avoid conflicts of HTML form element without a vue compiler
      VForm: VeeValidate.Form,
      VField: VeeValidate.Field,
      ErrorMessage: VeeValidate.ErrorMessage,
    },
    methods: {
      onSubmit(value) {
        // 3-7. 在 console 加上數值 1
        // 3-8. 在 onSubmit() 會自動帶上一個參數
        // console.log(1);
        console.log(value);
      },
      // 3-2. 規則會定義在方法裡面,參數會傳入一個值
      isRequired(value) {
        // 3-3. 驗證值有沒有填寫
        if (!value) {
          return "此欄是必填的";
        }
        return true;
      },
    },
  });

  app.mount("#app");
</script>

<script>
  // ESM 版本的差異(需要 Webpack)
  // import { Field, Form, ErrorMessage } from 'vee-validate';
  //
  // export default {
  //   components: {
  //     Field,
  //     Form,
  //     ErrorMessage,
  //   },
  //   methods: {
  //     isRequired(value) {
  //       if (value && value.trim()) {
  //         return true;
  //       }
  //
  //       return 'This is required';
  //     },
  //   },
  // };
</script>

表單驗證套件 vee-validation

如何為單一表單(input) 進行驗證

  1. 加入 VeeValidation 相關資源
  2. 註冊元件
    • 註冊全域的表單驗證元件 (VForm, VField, ErrorMessage)
  3. 定義規則
    • 選擇加入特定規則,全規則可參考
  4. 加入多國語系
  5. 套用 v-form 並加入 v-slot
  6. 套用 v-field 及 error-message
  7. 加入自訂驗證、送出表單等行為
// HTML
<div id="app">
  <h2>套用一個現成的流程</h2>
  <p>
    參考文件:<a href="https://hackmd.io/FFv0a5cBToOATP7uI5COMQ"
      >https://hackmd.io/FFv0a5cBToOATP7uI5COMQ</a
    >
  </p>

  <h3>範例:載入 VeeValidate 驗證套件</h3>
  <!-- 5. 套用 v-form 並加入 v-slot -->
  <v-form @submit="onSubmit" v-slot="{ errors }">
    {{ errors }}
    <div class="mb-3">
      <label for="email" class="form-label">Email</label>
      <!-- 6. 套用 v-field 及 error-message -->
      <!-- 6-2. 規則加入 rules 是對應到先前所載入的規則 -->
      <!-- 6-3. 加上 input 樣式,使用 :class 方式 -->
      <!-- 8-1. 可以直接補上 v-model 對應 user.email 進行驗證 -->
      <v-field
        id="email"
        name="email"
        type="email"
        class="form-control"
        rules="email|required"
        v-model="user.email"
        placeholder="請輸入 Email"
        :class="{ 'is-invalid': errors['email'] }"
      ></v-field>
      <!-- 6-1. error-message 必須對應到 v-field 的 name 欄位 -->
      <error-message name="email" class="invalid-feedback"></error-message>
    </div>

    <div class="mb-3">
      <label for="name" class="form-label">姓名</label>
      <v-field
        id="name"
        name="姓名"
        type="text"
        class="form-control"
        rules="required"
        v-model="user.name"
        placeholder="請輸入姓名"
        :class="{ 'is-invalid': errors['姓名']}"
      ></v-field>
      <error-message name="姓名" class="invalid-feedback"></error-message>
    </div>

    <div class="mb-3">
      <label for="phone" class="form-label">電話</label>
      <!-- 7. 加入自訂驗證、送出表單等行為… -->
      <!-- 7-1. input 改成 v-field -->
      <!-- 7-2 規則加上 rules,這裡使用的是 :rules="isPhone" -->
      <!-- 綁定的是下方的方法 isPhone(value) -->
      <v-field
        id="phone"
        name="電話"
        type="text"
        class="form-control"
        :rules="isPhone"
        v-model="user.phone"
        placeholder="請輸入電話"
        :class="{ 'is-invalid': errors['電話']}"
      ></v-field>
      <error-message name="電話" class="invalid-feedback"></error-message>
    </div>

    <div class="mb-3">
      <label for="region" class="form-label">地區</label>
      <v-field
        id="region"
        name="地區"
        class="form-control"
        :class="{ 'is-invalid': errors['地區']}"
        placeholder="請輸入地區"
        rules="required"
        v-model="user.region"
        as="select"
      >
        <option value="">請選擇地區</option>
        <option value="台北市">台北市</option>
        <option value="高雄市">高雄市</option>
      </v-field>
      <error-message name="地區" class="invalid-feedback"></error-message>
    </div>

    <div class="mb-3">
      <label for="address" class="form-label">地址</label>
      <v-field
        id="address"
        name="地址"
        type="text"
        class="form-control"
        rules="required"
        v-model="user.address"
        placeholder="請輸入地址"
        :class="{ 'is-invalid': errors['地址']}"
      ></v-field>
      <error-message name="地址" class="invalid-feedback"></error-message>
    </div>

    <button class="btn btn-primary" type="submit">送出</button>
  </v-form>
</div>
// JS
<!-- 1. 加入 VeeValidation 相關資源 -->
<!-- 主套件 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/vee-validate/4.1.17/vee-validate.min.js"></script>
<!-- i18n -->
<script src="https://cdn.jsdelivr.net/npm/@vee-validate/i18n@4.1.17/dist/vee-validate-i18n.min.js"></script>
<!-- rules -->
<script src="https://cdn.jsdelivr.net/npm/@vee-validate/rules@4.1.17/dist/vee-validate-rules.min.js"></script>
<script type="module">
  // 3. 定義規則
  // 選擇加入特定規則,全規則可參考
  // 參考: https://vee-validate.logaretm.com/v4/guide/global-validators#vee-validaterules
  VeeValidate.defineRule("email", VeeValidateRules["email"]);
  VeeValidate.defineRule("required", VeeValidateRules["required"]);

  // 4. 加入多國語系
  // 將外部資源儲存至本地
  // 外部資源: https://github.com/logaretm/vee-validate/blob/vee-validate%404.1.16/packages/i18n/src/locale/zh_TW.json
  // 讀取外部的資源
  VeeValidateI18n.loadLocaleFromURL("./zh_TW.json");

  // Activate the locale
  VeeValidate.configure({
    generateMessage: VeeValidateI18n.localize("zh_TW"), // 切換成中文版
    validateOnInput: true, // 調整為:輸入文字時,就立即進行驗證
  });

  const app = Vue.createApp({
    data() {
      return {
        // 8. 有加上資料欄位
        user: {
          email: "",
          name: "",
          address: "",
          phone: "",
          region: "",
        },
      };
    },
    methods: {
      onSubmit() {
        console.log(this.user);
      },
      isPhone(value) {
        const phoneNumber = /^(09)[0-9]{8}$/;
        return phoneNumber.test(value) ? true : "需要正確的電話號碼";
      },
    },
    created() {
      console.log(this);
    },
  });

  // 2. 註冊元件
  // 註冊全域的表單驗證元件(VForm, VField, ErrorMessage)
  app.component("VForm", VeeValidate.Form);
  app.component("VField", VeeValidate.Field);
  app.component("ErrorMessage", VeeValidate.ErrorMessage);

  app.mount("#app");
</script>

進階 API 章節延伸資源

Vue Axios

Vee Validation v4 連結