Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

AltStyle によって変換されたページ (->オリジナル) /