Although now many component libraries, convenient for our development. But their own components of the package, the understanding of components can not be less. Let’s take a look at packaging some common components.
For convenience, we use bootstrap for our styles, which is convenient for development.
The drop-down menu
We know that the drop-down menu needs to be composed of each item, so we can encapsulate a component of the drop-down item and its parent, the drop-down.
A drop-down component that only needs to provide the text that triggers the drop-down menu. It also provides default slots for customizing different drop-down items.
<div class="dropdown" ref="refDom">
<a
class="btn btn-outline-light dropdown-toggle"
href="javascript:;"
@click="openMenu"
>
hi, {{ name }}
</a>
<! -- Default dropdown in bootstrap is display: none -->
<div class="dropdown-menu" style="display: block" v-if="isOpen">
<slot></slot>
</div>
</div>
Copy the code
The important thing to note here is that the dropdown menu closes when we click outside of the drop-down component. Otherwise it won’t close. At this point, we need a JS API. contains
This part of logic we can extract as an hooks. Its main implementation is to pass in a drop-down menu root object, the root of a drop-down component, and then judge it and return a Boolean value.
import { ref, onMounted, onUnmounted, Ref } from "vue";
const useClickOutside = (refDom: Ref<null | HTMLElement>) = > {
const isClickOutside = ref(false);
const handler = (e: MouseEvent) = > {
// Prevent the node from being retrieved
if (refDom.value) {
// This function checks whether the click area is a dropdown menu.
if (refDom.value.contains(e.target as HTMLElement)) {
isClickOutside.value = false;
} else {
isClickOutside.value = true; }}}; onMounted(() = > {
window.addEventListener("click", handler);
});
onUnmounted(() = > {
window.removeEventListener("click", handler);
});
return {
isClickOutside
};
};
export default useClickOutside;
Copy the code
Let’s implement the logical part of the drop-down component
<script lang="ts">
import { defineComponent, ref, watch } from 'vue'
import useClickOutside from '.. /hooks/useClickOutside'
export default defineComponent({
name: 'DropDown'.props: {
name: {
type: String.required: true,}},setup() {
const isOpen = ref(false)
const openMenu = () = >{ isOpen.value = ! isOpen.value }const refDom = ref<null | HTMLElement>(null)
const { isClickOutside } = useClickOutside(refDom)
watch(isClickOutside, () = > {
// When the click is outside the drop-down menu and the drop-down menu is expanded.
if (isOpen.value && isClickOutside.value) {
isOpen.value = false}})return {
isOpen,
openMenu,
refDom,
}
},
})
</script>
Copy the code
For the drop-down-item component, it only needs to provide the default slot. And customize it based on the jump URL that comes in from the outside.
<template>
<div class="drop-down-item">
<a class="dropdown-item" :href="path">
<slot></slot>
</a>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: 'DropDownItem'.props: {
path: {
type: String.required: true}}})</script>
<style scoped>
.drop-down-item {
cursor: pointer;
}
</style>
Copy the code
use
<drop-down :name="user.username">
<drop-down-item path="/create">New articles</drop-down-item>
<drop-down-item :path="`/column/${user.column}`">My column</drop-down-item>
<drop-down-item path="/edit">Edit data</drop-down-item>
<drop-down-item path="/" @click="logout">Log out</drop-down-item>
</drop-down>
Copy the code
Form components
We know that forms components are used a lot. And, more often than not, we go back to using third-party component libraries for this part of the presentation. So let’s wrap the form component ourselves. Includes form validation.
Validate – form components:
<template>
<div class="validate-input pb-2">
<! -- :value="inputVal.val" @input="updateValue" -->
<! Div.validate-input Pb-2 if we don't set the inheritAttribute to false, attributes that are not inherent in the child component will be mounted directly to the immediate parent element.
<div class="mb-3">
<label class="form-label">{{ inputLabel }}</label>
<input
v-if="tag === 'input'"
class="form-control"
:class="{ 'is-invalid': inputVal.error }"
@blur="validate"
v-bind="$attrs"
v-model="inputVal.val"
/>
<textarea
v-else-if="tag ! == 'textarea'"
class="form-control"
:class="{ 'is-invalid': inputVal.error }"
@blur="validate"
v-bind="$attrs"
v-model="inputVal.val"
placeholder="Please enter the content of the article, support markdown syntax"
></textarea>
<small
id="emailHelp"
class="form-text text-muted invalid-feedback"
v-if="inputVal.error"
>{{ inputVal.message }}</small
>
</div>
</div>
</template>
Copy the code
It has the following attributes for a single form element.
// Constraints in the input box
interface InputProps {
// Form binding value
val: string
// Verify all errors
error: boolean
// Error message
message: string
}
Copy the code
You also need to have the following form validation rule properties
// Verify the constraints of the rule
interface RuleProps {
// You can pass in the form validation type as needed
type: 'required' | 'email' | 'password' | 'custom'
// Form validation error message
message: string
// If type is custom, pass it in to define the validation function.valdator? :() = > boolean
}
Copy the code
Here we are introduced to the two form type, ‘input’ | ‘textarea. If you want to extend it, just go ahead and add it to template.
type Tag = 'input' | 'textarea';
Copy the code
The validate-INPUT component needs to pass in the following props.
props: {
// Array of rules required for form validation
rules: Array as PropType<RulesProps>,
// The value of the V-model implementation
modelValue: String.// Form type
tag: {
type: String as PropType<Tag>,
default: 'input',},// Represents the label value of the input box.
inputLabel: {
type: String.required: true,}}Copy the code
Implement bidirectional binding of form values.
const inputVal: InputProps = reactive({
val: computed({
get: () = > props.modelValue || ' '.set: val= > {
emit('update:modelValue', val)
}
}),
error: false.message: ' '
})
Copy the code
Implement form validation functions.
const validate = () = > {
if (props.rules) {
const allPassed = props.rules.every(rule= > {
let passed = true
inputVal.message = rule.message
switch (rule.type) {
case 'required': passed = (inputVal.val.trim() ! = =' ')
break
case 'email':
passed = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(inputVal.val)
break
case 'password':
passed = / ^ (? =.*[a-z])(? =.*[A-Z])(? =. * \ d) [^] $/ dec {8}.test(inputVal.val)
break
// If a custom validation function is passed in, we can execute it directly.
case 'custom':
passed = rule.validator ? rule.validator() : true
break
default:
break
}
return passed
})
// Set error to false after all validation is completeinputVal.error = ! allPassedreturn allPassed
}
return true
}
Copy the code
In fact, we can also customize the event that triggers the validation. By default, we specify the time when blur is out of focus, so we don’t change it.
Then we need to save all the validation functions and send them to the validate-form component. When the button is clicked, we will determine whether the validation has passed and then block or send the request. So we need to use emITT library to serve us. This is because it triggers the button in the parent component and then passes the event to the child component.
onMounted(() = > {
emitter.emit('all-true', validate)
})
Copy the code
Let’s look at how the validate-form component is implemented. Here we need to define a default slot to hold several forms. There is also a named slot for form submission.
<template>
<div class="validate-form">
<form>
<slot name="default"></slot>
<div class="submit-area" @click.prevent="FormSubmit">
<slot name="submit">
<button type="submit" class="btn btn-primary">The login</button>
</slot>
</div>
</form>
</div>
</template>
Copy the code
The complete code for the validate-input and validate-form components is shown below
// validate-form
<template>
<div class="validate-form">
<form>
<slot name="default"></slot>
<div class="submit-area" @click.prevent="FormSubmit">
<slot name="submit">
<button type="submit" class="btn btn-primary">The login</button>
</slot>
</div>
</form>
</div>
</template>
<script lang="ts">
import { defineComponent, onUnmounted } from 'vue'
import emitter from '.. /mitt'
type Func = () = > boolean
export default defineComponent({
name: 'ValidateForm'.emits: ['form-submit'].setup(props, context) {
let funcArr: Func[] = []
const FormSubmit = () = > {
// Call each item in the array and check if there is false
const val = funcArr.map((item) = > item()).every((element) = > element)
context.emit('form-submit', val)
}
// All validation functions are stored in an array.
const callback = (func? : Func) = > {
if (func) {
funcArr.push(func)
}
}
emitter.on('all-true', callback)
onUnmounted(() = > {
emitter.off('all-true', callback)
// Empty the array
funcArr = []
})
return {
FormSubmit,
}
},
})
</script>
<style scoped>
.submit-area {
margin-top: 30px;
margin-bottom: 20px;
}
</style>
Copy the code
<template>
<div class="validate-input pb-2">
<div class="mb-3">
<label class="form-label">{{ inputLabel }}</label>
<input
v-if="tag === 'input'"
class="form-control"
:class="{ 'is-invalid': inputVal.error }"
@blur="validate"
v-bind="$attrs"
v-model="inputVal.val"
/>
<textarea
v-else-if="tag ! == 'textarea'"
class="form-control"
:class="{ 'is-invalid': inputVal.error }"
@blur="validate"
v-bind="$attrs"
v-model="inputVal.val"
></textarea>
<small
id="emailHelp"
class="form-text text-muted invalid-feedback"
v-if="inputVal.error"
>{{ inputVal.message }}</small
>
</div>
</div>
</template>
<script lang="ts">
import {
defineComponent,
PropType,
reactive,
onMounted,
ref,
watch,
computed,
} from 'vue'
import emitter from '.. /mitt'
// Constraints in the input box
interface InputProps {
val: string
error: boolean
message: string
}
// Verify the constraints of the rule
interface RuleProps {
type: 'required' | 'email' | 'password' | 'custom'
message: string valdator? :() = > boolean
}
export type RulesProps = RuleProps[]
// Determine whether the input box is normal or multi-line input box
type Tag = 'input' | 'textarea'
export default defineComponent({
name: 'ValidateInput'.// Do not mount properties not in props to the root component
inheritAttrs: false.props: {
rules: Array as PropType<RulesProps>,
// The value of the V-model implementation
modelValue: String.tag: {
type: String as PropType<Tag>,
default: 'input',},// Represents the label value of the input box.
inputLabel: {
type: String.required: true,}},setup(props, context) {
const inputVal: InputProps = reactive({
val: computed({
get() {
return props.modelValue || ' '
},
set(val: string) {
context.emit('update:modelValue', val)
},
}),
error: false.message: ' ',})const validate = () = > {
if (props.rules) {
const allPassed = props.rules.every(rule= > {
let passed = true
inputVal.message = rule.message
switch (rule.type) {
case 'required': passed = (inputVal.val.trim() ! = =' ')
break
case 'email':
passed = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(inputVal.val)
break
case 'password':
passed = / ^ (? =.*[a-z])(? =.*[A-Z])(? =. * \ d) [^] $/ dec {8}.test(inputVal.val)
break
// If a custom validation function is passed in, we can execute it directly.
case 'custom':
passed = rule.validator ? rule.validator() : true
break
default:
break
}
return passed
})
// Set error to false after all validation is completeinputVal.error = ! allPassedreturn allPassed
}
return true
}
onMounted(() = > {
emitter.emit('all-true', validate)
})
return {
inputVal,
validate
}
},
})
</script>
<style scoped>
.form-text {
color: #dc3545 ! important;
}
</style>
Copy the code
Loading component
When we send a request, we need to control the display of loading to improve user experience.
Take a look at its template below.
<template>
<teleport to="#loader">
<div class="loader">
<div class="loader-mask"></div>
<div class="container">
<div class="spinner-border text-primary" role="status">
<span class="sr-only"></span>
</div>
</div>
</div>
</teleport>
</template>
Copy the code
Since the loading component is independent of each component, it should be attached to the body tag as a direct child element. This is where vuE3’s built-in Teleport component comes in.
<script>
import { defineComponent, onUnmounted } from "vue";
export default defineComponent({
name: 'Loader'.setup() {
const oLoader = document.createElement('div');
oLoader.id = 'loader'
document.body.appendChild(oLoader)
onUnmounted(() = > {
document.body.removeChild(oLoader)
})
}
})
</script>
Copy the code
Here’s what it looks like.
<style scoped>
.loader {
width: 100%;
height: 100%;
}
.loader-mask {
position: fixed;
z-index: 9;
left: 0;
right: 0;
top: 0;
background: # 000000;
opacity:.4;
width: 100%;
height: 100%;
}
.spinner-border {
position: absolute;
top: 50%;
left: 50%;
}
</style>
Copy the code
The message component
This component is also quite common and can be used to alert the user when the user enters an error message or does something wrong.
He just needs to pass in the prompt and the prompt type to customize the Message component. This component is very easy to encapsulate, and the code is shown directly below.
<template>
<teleport to="#message">
<div class=" message alert message-info fixed-top mx-auto d-flex justify-content-between mt-2">
<div class="alert" :class="`alert-${type}`" role="alert">
{{message}}
</div>
</div>
</teleport>
</template>
<script lang="ts">
import { defineComponent, onUnmounted, PropType } from "vue";
export type MessageType = 'success' | 'error' | 'default'
export default defineComponent({
name: 'Message'.props: {
type: {
type: String as PropType<MessageType>,
required: true
},
message: String
},
setup() {
const oDiv = document.createElement('div');
oDiv.id = "message"
document.body.appendChild(oDiv)
onUnmounted(() = > {
document.body.removeChild(oDiv)
})
}
})
</script>
<style scoped>
.message {
margin: 0 auto;
}
.alert {
width:500px;
text-align: center;
}
</style>
Copy the code
But think about it for a moment. Our hint is that we generally want to call it from a function. Easy to operate. Because when we get an error, it’s all in the logical code, we can just call the function and create a Message component.
At this point, you need to know about createAppAPI. Please visit the
- This function takes a root component option object as its first argument
- With the second argument, we can pass the root prop to the application
Let’s look at how the createMessage function component is implemented.
import { createApp } from 'vue'
import Message from './Message.vue'
export type MessageType = 'success' | 'error' | 'default'
const createMessage = (
message: string.type: MessageType,
timeout = 2000
) = > {
const messageInstance = createApp(Message, {
message,
type
})
const mountNode = document.createElement('div')
document.body.appendChild(mountNode)
messageInstance.mount(mountNode)
// Remove the message component within the specified time
setTimeout(() = > {
messageInstance.unmount(mountNode)
document.body.removeChild(mountNode)
}, timeout)
}
export default createMessage
Copy the code
To be continued…