Skip to content

b0yblake/Vue3-Pro-Tips-Components-Design

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 

Repository files navigation

CONTENT


Chap -1 : THE COMPONENTS DESIGN PATTERNS

NOTE:


Chap 0 : HANDLING THE ERRORS WITH PROPS

NOTE:
✔ Khi làm việc với props, việc không tin bất cứ data nào truyền vào từ parent là có cơ sở (lỗi API, lỗi logic bên BE, lỗi name file,...)
✔ Việc cover các case xuất hiện với data truyền vào là thực sự cần thiết đối với việc handing errors on production
✔ Sử dụng các mệnh đề có sẵn: type required default validator


NOTE:
✔ Xảy ra trường hợp, data điền đúng kiểu nhưng bị sai value (sai định dạng, sai kiểu value,...)
✔ Validator dữ liệu có thể gây dài dòng, nhưng sẽ là tuyệt với nếu bạn handling errors ngay từ lúc khởi điểm


Extend:
✔ Vậy chúng ta có thể tạo 1 helper (custom hook) cho việc validator này đúng không nhỉ
✔ Error mặc định của Vue thường không rõ ràng, chúng ta có thể customize lại message
✔ Ex: validator type of images (allow *.jpg || *.png)

// file component want to use helper
import { validatorImageType } from '../helpers/validatorImageType.js'

// file helper validatorImageType
const validatorImageType = (propString) => {
  const hasImagesDirectory = propString.indexOf('/images/') > -1
  const isPNG = prop.endWith('.png')
  const isJPG = prop.endWith('.jpeg') || prop.endWith('.jpg')

  return hasImagesDirectory && isPNG && isJPG
}
export default validatorImageType

Chap 1 : BUILD CONTROLLED COMPONENTS

SANBOX CODE: building-controlled-components

NOTE:
✔ Bản chất của v-model là việc lắng nghe input(:modelValue="pageTitle") và emit dữ liệu(@update:modelValue="pageTitle = $event")
v-model chỉ nên dùng cho inputcomponent => không nên sử dụng cho các thành phần khác
✔ Sử dụng emitgộp các emit
form tối ưu bằng cách loại bỏ real-time sync in input @input.once => chỉ check khi click submit
✔ Sử dụng Object.fromEntries(new FormData(event.target)) thay cho v-model nếu dùng với form nhiều thành phần
✔ Khi emitdata, chỉ sử dụng từ update:someModelValue khi thao tác với v-model => với các component thì bỏ từ update để tránh gây hiểu nhầm

  • Khi thao tác với form elements(input, checkbox, select,..) hãy linh động trong việc xử lý các $emitprops => hãy tách các form elements thành các components riêng lẻ và để trong global components / common components

  • Hãy sử dụng emit gộp của Vue3 để việc control được hiệu quả hơn

https://v3.vuejs.org/guide/component-custom-events.html#event-names
===================

// Emit with Vue3: setup(...)
emits: ['your-event', 'handle-confirm-del-data'],
setup(props, { emit }) { 
  ...
  emit('your-event', dataWantToEmit); // Hoặc sử dụng context(attrs, slots, emit) cho nó ngắn: setup(props, context) {} || context.emit('yourEvent', dataWantToEmit);
  emit("handle-confirm-del-data", false); //data to close del dialog
}
<your-child @your-event="onYourEvent" />
onYourEvent(dataWantToEmit) {
  ...
}

// Gộp các emit trong 1 setup
<ChildComponent v-model="pageTitle" />

export default {
  props: {
    modelValue: String // previously was `value: String`
  },
  emits: ['update:modelValue'],
  methods: {
    changePageTitle(title) {
      this.$emit('update:modelValue', title) // previously was `this.$emit('input', title)`
    }
  }
}
  • Sử dụng v-model làm tối giản lại phong cách code và nhanh gọn
    Vue 2:

Vue 3 thay đổi syntax của v-model:

<ChildComponent v-model="pageTitle" />

<!-- shorthand for: -->
<ChildComponent
  :modelValue="pageTitle"
  @update:modelValue="pageTitle = $event"
/>

  • v-model có thể sử dụng với các element không thuộc hệ thống form elements => với điều kiện chỉ có 1 element duy nhất có trong component
// Normal with form elements
<input @input="email = $event.target.value">

// Special with element not belong to form
<some-component :modelValue="newLetter" @update:modelValue="(newValue) => { newLetter = newValue }"></some-component>
//Child component
setup(props, { emit }) {
   ...
   emit('your-event', dataWantToEmit);
}

//Parent component
<your-child @your-event="onYourEvent" />

onYourEvent(dataWantToEmit) {
  console.log(dataWantToEmit)
}
  • Việc lắng nghe liên tục mọi keydown || clicked khi end-user thao tác trên form chỉ thực sự đúng đắn khi làm việc với reactive form (form muốn response trực tiếp hành động của user) => hãy tối giản bằng việc khi submit mới lắng nghe => giải phóng được 1 phần bộ nhớ và giảm tình trạng lag nếu làm với super form.
@input.once

Chap 2 : CUSTOMIZING CONTROLLED COMPONENT BINDINGS

SANBOX CODE: customizing-controlled-component-bindings

NOTE:
v-model default sẽ không được tự nhiên vì sử dụng modelValue & update:modelValue => có thể custom bằng các value cụ thể

//Parent
<Custom-component v-model:message="message" />

//Child
<input type="text" :value="message" @input='emit("update:message", $event.target.value);'>

Chap 3 : WRAPPING EXTERNAL LIBRARIES AS VUE COMPONENTS

SANBOX CODE: wrapping-external-libraries-as-vue-components

NOTE:
✔ Hãy chú ý sử dụng các method có sẵn của lib để custom lại các event

Chap 4 : ENCAPSULATING EXTERNAL BEHAVIOR CLOSING ON ESCAPE

SANBOX CODE: encapsulating-external-behavior-closing-on-escape

NOTE:
Web accessibility luôn luôn đặt lên hàng đầu mỗi khi thao tác với dialog
✔ Dialog khi bật lên cần được kiểm soát cả ở phần keyboard: close = esc, enter, blankspace
✔ Hành vi người dùng cần được chú trọng khi họ dùng keyboard
✔ Đối với những DOM sinh ra sau lifecycle:created, nếu muốn control được, nên sử dụng nextTick hoặc sử dụng vanila js tại thời điểm click
✔ Hãy linh động trong việc sử dụng js, chú ý đến việc tối ưu hiệu suất (dùng js tối ưu được hiệu suất ngay tại component do không phải v-model 2way-binding)

Way1: sử dụng keydown = esc button, enable tabindex để có thể focus được 
@keydown.esc="handleEsc" tabindex="0" ref="dialog"

Way2: sử dụng vanila js nhằm bắt sự kiện click tại thời điểm dialog đã sinh ra (không cần care về việc sinh ra hay sau dom update)

=======================
methods: {
  createClickEvent: function() {
    let self = this;

    document
      .querySelector(".util_per_pay_rate .btn_open_dialog")
      .addEventListener("click", function() {
        self.dialog = true;
      });
  }
},
mounted() {
  this.createClickEvent();
}
========================
created() {
  document.addEventListener('keydown', (e) => {
    if(e.key === 'Escape' && this.show) {
      this.createClickEvent();
    }
  })
}

Chap 5 : ENCAPSULATING EXTERNAL BEHAVIOR BACKGROUND SCROLLING

SANBOX CODE: encapsulating-external-behavior-background-scrolling

NOTE:
✔ Khi bật Dialog vấn đề gặp phải là chúng ta cần remove scroll: hãy chú ý về cách sử dụng bằng class toggle tại body => chúng sẽ hữu hiệu khi chúng ta handle được, còn không => hãy sử dụng vanila js
✔ Hãy cố gắng cover 1-2 case tiếp theo sau này khi mở rộng app

watchEffect(() => {
  props.active ? props.preventBackgroundScrolling && document.body.style.setProperty('overflow', 'hidden') : props.preventBackgroundScrolling && document.body.style.setProperty('overflow')
})
watch(() => props.selected, (first, second) => {
  console.log(
    "Watch props.selected function called with args:",
    first,
    second
  );
});

Chap 6 : ENCAPSULATING EXTERNAL BEHAVIOR PORTALS

SANBOX CODE: encapsulating-external-behavior-portals

NOTE:
✔ Với các dialog trên từng component, hãy cứ viết ở trên các components để dễ handle data, sau đó sử dụng teleport kết hợp với slot
✔ Việc handle data của tất cả các popup ở cùng 1 component trung gian đem lại hiệu quả rõ rệt so với việc handle data tại các component common
https://github.com/b0yblake/Vue3-Form-Best-Practice/blob/main/src/views/Form.vue

//index.html
<body>
  <div id="app"></div>
  <!-- Use teleport to move dialog to here -->
  <div id="layer"></div>
</body>

// Component
<teleport to="#layer">
  <some-component-dialog :data="data" @click="some-element" />
</teleport>

Chap 7 : ENCAPSULATING EXTERNAL RESING PORTALS

SANBOX CODE: encapsulating-external-behavior-reusing-portals

NOTE:
teleport hiệu quả với multiple => cái nào được move trước sẽ xuất hiện trước

Chap 8 : INJECTING CONTENT USING SLOTS

SANBOX CODE: injecting-content-using-slots

DON'T LET YOURSELF TURN INTO THIS CASE:
✔ Bạn đinh sloved 1 required đơn giản với nhiều case tại 1 button
✔ Button đó có các case spin title icon left icon right ,..
✔ Hãy đúng đắn suy nghĩ trước khi làm 1 component


NOTE:
SLOT là 1 tính năng cực kỳ hay ho cho việc tái sử dụng tối đa số lần components xuất hiện.
✔ Việc kết hợp cùng với class tại component tag cũng đem lại sự tiện lợi cho việc tái xử dụng component


// Tái sử dụng với trường hợp đặt các case cố định cho các vùng của header
// Parent
<dialog>
  <template #header>
    <h1>Dialog main</h1>
  </template>
  <template #default>
    <h1>Dialog main</h1>
  </template>
  ...
</dialog>

// Child popup
<template>
  <div class="c-base-popup">
    <div v-if="??" class="c-base-popup__header">
      <slot name="header"></slot>
    </div>
    <div v-if="??" class="c-base-popup__subheader">
      <slot name="subheader"></slot>
    </div>
    <div class="c-base-popup__body">

      //Default slot
      <slot></slot> 

      <h1>{{ title }}</h1>
      <p v-if="description">{{ description }}</p>
    </div>
    <div v-if="??" class="c-base-popup__actions">
      <slot name="actions"></slot>
    </div>
    <div v-if="??" class="c-base-popup__footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

Chap 9 : NATIVE STYLE BUTTONS USING SLOTS AND CLASS MERGING

SANBOX CODE: native-style-buttons-using-slots-and-class-merging

NOTE:
✔ CLass có thể merging giữa component tag & first-element-in-component

<!-- Common dialog -->
<BadgeDialog :dataDialog="form" v-model:active="activeDialog" v-show="activeDialog" class="flex">

// BadgeDialog component
<template>
  <div class="nes-dialog abc" id="badge-dialog"></div>
</template>

// Result
<div class="nes-dialog abc flex" id="badge-dialog"></div>

Chap 10 : EXTENDING COMPONENTS USING COMPOSITION (SPECIAL)

SANBOX CODE: extending-components-using-composition

NOTE:
✔ Chú ý khi sử dụng compositionAPI => cấu trúc ref & reactive sẽ gây khó khăn
✔ Việc tái sử dụng component luôn được đặt lên hàng đầu => hãy xem kỹ ví dụ để thấy được hiệu quả khi sử dụng component trung gian


//Sử dụng Object.assign cho custom hook (composables)

  setup() {
    const initialState = {
      name: "",
      lastName: "",
      email: ""
    };

    const form = reactive({ ...initialState });

    function resetForm() {
      Object.assign(form, initialState);
    }

    function setForm() {
      Object.assign(form, {
        name: "John",
        lastName: "Doe",
        email: "john@doe.com"
      });
    }

    return { form, setForm, resetForm };
  }

Chap 11 : PASSING DATA UP USING SCOPED SLOTS

SANBOX CODE: passing-data-up-using-scoped-slots

NOTE:
✔ Khi ta sử dụng data tại Child (vì nhiều lý do), mà parent là nơi call component tag:
✔ NEW way: props đôi khi có thể truyền dưới dạng function (thay vì data như trước) => Chỉ là cách tham khảo, ít người thích dùng kiểu này vì rườm ra và k flexable
✔ HIGH RECOMMEND: Sử dụng slot như 1 dạng flexiable code, để layout có thể tùy chỉnh theo parent mà vẫn sử dụng data tại child


======= Sử dụng data tại child như props ===============
// Parent
<contact-list :pseudo-slot="({ contact }) => contact.name.first"></contact-list>

// Child
<div class="child">
  {{ pseudoSlot({ contact: contact }) }}
</div>
======= Sử dụng data tại child => passing data ngược lại parent thông qua slot ===============
// Parent
// Có thể custom layout như này
<contact-list>
  <a slot-scope="{ contact }" :href="`/contacts/${contact.id}`">
    {{ contact.name.first }}
  </a>
</contact-list>

// Hoặc như này
<contact-list>
  <div slot-scope="{ contact }">
    <strong class="user-title">{{ contact.name.first }}</strong>
  </div>
</contact-list>

// Child
<div class="child">
  <slot :contact="contact"></slot>
</div>

Chap 12 : RENDER FUNCTIONS 101

SANBOX CODE: render-functions-101

NOTE: CHÚNG TA CẦN HIỂU ĐỂ SỬ DỤNG 1 CÁCH HIỆU QUẢ
✔ Using full power of JS (Sử dụng tất cả sức mạnh của JS) ✔ Dynamically creating HTML tags (Tự động tạo các tag HTML) ✔ Good for library creators (Sẽ hiệu quả nếu dùng với các thư viện tự động) ❌ Sẽ phức tạp hơn khi lạm dụng vì 1 số code không cần thiết (html tĩnh, passing only data, ...) ❌ Lỗi sinh ra trong im lặng (silently failed) ❌ Gây lú lẫn vì nhiều syntax ❌ Lồng vào nhau nhiều thứ chứ không tách bạc như template 🦟 Fix: hãy chia nhỏ các thành phần và sử lý từng phần 1


//Parent
<RenderFuncEx heading="'1'" />

//Child
<script>
import {
  h,
} from 'vue';

export default {
  props: {
    heading: {
      type: Number,
      required: true,
      default: 2,
      validator: propValue => {
        const isNumber = isNumber(propValue)
        return isNumber && false
      }
    }
  },
  setup(props, { context }) {

    return () => h(
      `h${props.heading}`,
      {
        class: 'text-lg title',
        style: 'color: red',
      },
      'Simple Form Example'
    )
  }
}
</script>

NOTE: ✔ return () => h(element, attributes, children)
✔ Có thể sử dụng được tính reactivity của compositionAPI
✔ Multiple render function là có cơ sở, hãy làm tuần tự

setup(props, { context }) {

  const count = ref(0);
  const increament = () => {
    return count.value++
  }

  return () => h(
    `h${props.heading}`,
    {
      class: 'text-lg title',
      style: 'color: red',
      onClick: increament
    },
    [
      'Simple Form Example',
      h(
        `h${props.heading + 1}`,
        {
          style: 'color: green',
        },
        count.value
      )
    ]
  )
}


Chap 13 : RENDER FUNCTIONS AND COMPONENTS

SANBOX CODE: render-functions-and-components

NOTE:
v-model không thể dùng trong render-function => hãy dùng các cú pháp của render-func: https://v3.vuejs.org/guide/render-function.html#v-model
https://v3.vuejs.org/guide/render-function.html

<script>
import {
  h,
} from 'vue';
import TextButton from '@/components/common/button/TextButton.vue';

export default {
  name: "RenderFuncEx",
  components: {
    TextButton,
  },
  setup(props, { context }) {

    //<TextButton :title="'PressMe'" />
    return () => h(
      TextButton,
      {
        title: 'Simple Form Example',
        onClick: () => alert('clicked')
      }
      
    )
  }
}
</script>

Chap 14 : RENDER FUNCTIONS AND CHILDREN + Chap 15 : RENDER FUNCTIONS AND SLOTS

SANBOX CODE: render-functions-and-children
SANBOX CODE: render-functions-and-slots

NOTE:
✔ Vậy thì với các vòng lặp đơn giản (v-if v-for v-show ...)



Chap 15.1: RENDER FUNCTION FACTORY

NOTE:
✔ Đôi khi, việc sử dụng render function là 1 điều quen thuộc, việc lặp đi lặp lại code sẽ dẫn tới sự nhàm chán, hãy thử factory render function ✔ Giải quyết được vấn đề về việc lặp code và sử dụng được pattern

// Normal way: in file ProductListing.vue
<template>
  <ListingContainer
    :service="productService"
  />
</template>

<script>
import productService from '../services/product';

import ListingContainer from './ListingContainer';

export default {
  name: 'ProductListing',
  components: {
    ListingContainer,
  },
  created() {
    this.productService = productService;
  },
};
</script>
// Use factory way: in file ProductListing.vue
<script>
import containerFactory from './factories/container';
import productService from '../services/product';
import ListingContainer from './ListingContainer';

export default containerFactory(ListingContainer, {
  service: productService
});
</script>

// In file factories/container.js
export default (Component, props) => ({
  functional: true,
  props: Component.props,
  render(h, context) {
    return h(Component, {
      props: { ...context.props, ...props },
    });
  }
});

Chap 16 : DATA PROVIDER COMPONENTS

SANBOX CODE: data-provider-components


Chap 17 : GETTING STARTED WITH RENDERLESS UI COMPONENTS

Chap 18 : PASSING DATA PROPS FROM RENDERLESS COMPONENTS

Chap 19 : PASSING ACTION PROPS FROM RENDERLESS COMPONENTS

Chap 20 : PASSING BINDING PROPS FROM RENDERLESS COMPONENTS

Chap 21 : RENDERLESS IO COMPONENTS FUNCTIONS AS BINDING PROPS

Chap 22 : IMPLEMENTING ALTERNATE LAYOUTS WITH RENDERLESS COMPONENTS

Chap 23 : CONFIGURING RENDERLESS COMPONENTS

Chap 24 : WRAPPING RENDERLESS COMPONENTS

SANBOX CODE: getting-started-with-renderless-ui-components
SANBOX CODE: passing-data-props-from-renderless-components
SANBOX CODE: passing-action-props-from-renderless-components
SANBOX CODE: passing-binding-props-from-renderless-components
SANBOX CODE: renderless-ui-components-functions-as-binding-props
SANBOX CODE: implementing-alternate-layouts-with-renderless-components
SANBOX CODE: configuring-renderless-components
SANBOX CODE: wrapping-renderless-components

NOTE:
✔ ✔ ✔ ✔ ✔ ✔

Chap 25 : ELEMENT QUERIES AS A DATA PROVIDER COMPONENT

SANBOX CODE: element-queries-as-a-data-provider-component

Chap 26 : BUILDING COMPOUND COMPONENTS WITH PROVIDE INJECT

SANBOX CODE: building-compound-components-with-provide-inject

NOTE:
https://github.com/wnr/element-resize-detector

Chap 27 : BUILDING A COMPOUND SORTABLE LIST COMPONENT

SANBOX CODE: building-a-compound-sortable-list-component

NOTE:
https://github.com/SortableJS/Vue.Draggable

Chap 28 : BUILDING A SEARCH SELECT DATA BINDINGS

Chap 29 : BUILDING A SEARCH SELECT FILTERING

Chap 30 : BUILDING A SEARCH SELECT FOCUS MANAGEMENT

Chap 31 : BUILDING A SEARCH SELECT MAKING IT CONTROLLED

Chap 32 : BUILDING A SEARCH SELECT KEYBOARD NAVIGATION

Chap 33 : BUILDING A SEARCH SELECT CLICK OUTSIDE COMPONENT

Chap 34 : BUILDING A SEARCH SELECT INTERGRATING POPPERJS

SANBOX CODE: building-a-search-select-data-bindings
SANBOX CODE: building-a-search-select-filtering
SANBOX CODE: building-a-search-select-focus-management
SANBOX CODE: building-a-search-select-making-it-controlled
SANBOX CODE: building-a-search-select-keyboard-navigation
SANBOX CODE: building-a-search-select-click-outside-component
SANBOX CODE: building-a-search-select-integrating-popperjs

NOTE:
✔ ✔ ✔ ✔ ✔

About

🚧 Let's define your own rule for developing Vuejs 🚧

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published