NOTE:
✔
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
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 input
và component
=> không nên sử dụng cho các thành phần khác
✔ Sử dụng emit
và gộ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 emit
data, 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$emit
vàprops
=> hãy tách cácform elements
thành các components riêng lẻ và để trongglobal 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)`
}
}
}
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ốngform elements
=> với điều kiện chỉ có 1 element duy nhất có trongcomponent
// 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
khiend-user
thao tác trên form chỉ thực sự đúng đắn khi làm việc vớireactive 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 khisubmit 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
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);'>
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
- Đôi khi, việc thêm 1 thư viện ngoài(datePicker) vào để gọi trong input gây phiền toái nhất định: $event chọn ngày không cập nhật với biến ref thông thường. Vì chúng không được tạo ra để sync với biến đó => hãy sử dụng các custom event của lib (onSelect()) để xác định event click day.
- https://github.com/b0yblake/Vue3-Form-Best-Practice/blob/main/src/components/common/form/DatepickerPikaday.vue
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();
}
})
}
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
);
});
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>
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
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>
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>
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: "[email protected]"
});
}
return { form, setForm, resetForm };
}
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>
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
)
]
)
}
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>
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
...)
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 },
});
}
});
SANBOX CODE
: data-provider-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:
✔
✔
✔
✔
✔
✔
SANBOX CODE
: element-queries-as-a-data-provider-component
SANBOX CODE
: building-compound-components-with-provide-inject
NOTE:
✔ https://github.com/wnr/element-resize-detector
SANBOX CODE
: building-a-compound-sortable-list-component
NOTE:
✔ https://github.com/SortableJS/Vue.Draggable
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:
✔
✔
✔
✔
✔