foreword
NutUI, everyone should be familiar with [Grimace], students in front-end development must have some understanding. NutUI is a JD-style mobile component library, which uses Vue language to write applications that can be used on H5 and small program platforms.
At present, NutUI has 70+ components, supports on-demand reference, supports TypeScript, supports custom themes and other functions, and of course supports the latest Vue3 syntax, which can effectively help developers improve efficiency and improve development experience in development.
Closer to home, today we will learn about the implementation and design of the Collapse panel in NutUI , as well as the new knowledge points learned during the development process.
Folding panel design
In fact, the foldable panel component is a relatively common component whether it is in PC or M. As the name suggests, it is a content area that can be folded/expanded. The usage scenarios are also relatively wide, such as navigation, text-based details, filtering and classification, etc.;
In the component development stage, we usually conduct comparative analysis and learn from each other's strengths. Therefore, we simply enter the development of components through functional comparison.
folding panel | vant | antd | tdesign | elementui | varlet | vuetify | naiveui | iview | balam | nutui |
---|---|---|---|---|---|---|---|---|---|---|
ContentExpand Collapse | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
animation effect | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ |
accordion mode | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
fold icon | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ |
Fold icon color | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ |
Fold icon size | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
title icon position | ✓ | ✓ | ✗ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✓ |
Rotation angle | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
subtitle | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ | ✓ |
Disable mode is supported | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ |
Fixed content can be set | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
The essence of components is to improve development efficiency. We achieve business requirements through deconstruction and combined configuration of business scenarios. For example, the component library is a toolbox. Each component is a wrench, pliers and other tools in the box. It provides various tools for business scenarios. How to create a suitable tool for work requires us to have some experience in the usual business development. Understand and think.
Let's explore together~
Realize Expand Collapse
The basic interaction of components is clear, so the layout of our title and content is relatively simple. Now we need to complete the development of the interaction, that is, the function of expanding and collapsing.
It is actually very simple to realize the function of unfolding and folding, that is, to control the display and hiding of the content through a variable. Without considering other factors, this method is indeed the most efficient way.
<template>
<div class="container">
<div class="title" @click="handle">
标题
</div>
<div class="content" v-show="show">
测试内容测试内容测试内容测试内容测试内容测试内容
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const show = ref(false);
const handle = () => {
show.value = !show.value;
}
</script>
However, adopting this method may not be very friendly to our later functional expansion and interaction effects. So my solution is to change the folded content height
. Of course, this method is easy to understand.
We mainly deal with the content of content
. For this style, we have height
the default is 0, that is, the content is folded. Because each collapsed content cannot be determined, we need to dynamically calculate the height of the content after filling, which is also an adaptation scheme.
The purpose of my dynamic calculation is to achieve the back animation effect and improve the user experience. I use the method of height
+ transform
to achieve, and use the property will-change of css
to optimize the animation effect.
will-change provides a way for web developers to tell the browser what changes will be made to the element, so that the browser can prepare for optimizations in advance of the actual changes to the element's properties. This optimization can prepare a part of the complex calculation work in advance, making the page more responsive and responsive.
// 组件部分核心代码
const wrapperRefEle: any = wrapperRef.value;
const contentRefEle: any = contentRef.value;
if (!wrapperRefEle || !contentRefEle) {
return;
}
const offsetHeight = contentRefEle.offsetHeight || 'auto';
if (offsetHeight) {
const contentHeight = `${offsetHeight}px`;
wrapperRefEle.style.willChange = 'height';
wrapperRefEle.style.height = !proxyData.openExpanded ? 0 : contentHeight;
}
The above code calculates the height of the content by obtaining the element DOM
offsetHeight
and assigning it, and combining the height change with transform
realizes the animation effect of closing and expanding.
flexible title bar
The second is the improvement of the title bar function, adding icons, custom positions and related animation functions. Let's first look at the icon on the right side of the basic usage. It corresponds to the collapse and expansion of the content. When the interaction is expanded, the up arrow is the down arrow when it is collapsed. Then we can use an arrow icon as a variable according to whether the expanded state is a variable. The solution is to use the css3
rotate
attribute of ---fbf87243e891ddf6b75793dae4d62837---, and reverse 180°
.
if (parent.props.icon && !proxyData.openExpanded) {
proxyData.iconStyle['transform'] = 'rotate(0deg)';
} else {
proxyData.iconStyle['transform'] = 'rotate(' + parent.props.rotate + 'deg)';
}
For higher user customization and better ability to expand components, APIs for icon configuration are exposed externally, such as custom icons, icon rotation angles, etc. These configurations refer to different scenarios, for example, the content of some news reports is folded and rotated by 90°.
Of course, the title bar text can also be configured with related icons, including the location, color, and size of the icons. This function increases the user's personalized configuration, which can be used to display some important messages, new message reminders, unchecked information and other scenarios.
Developers of some component libraries may not have this configuration. First of all, personal feelings are irrelevant to components. The design of the component needs to be connected with the business, and some functions are abstracted, so that the function of the component can be better improved, including the extension of the component in the later stage, etc., which all grow in the development of the business.
Configuration item upgrade
In the later use process, we optimized and upgraded the component functions according to certain scenarios.
First, the configuration of the subtitle is added, which can be easily set by sub-title
(PS: You can see the example in the picture above 👆).
The search classification function in the shopping mall mobile terminal, such as the scene in the figure below. It will have the default content displayed on the outside, and the rest of the content will be folded or expanded after it is folded, so a new slot:extraRender
API is added to let this part of the content exist in the form of a slot, which is convenient for developers to define different Display form, easy to adjust the style, etc.
The implementation of the above functions is relatively simple, just add a slot
tag to the code to receive the incoming content.
<view v-if="$slots.extraRender" class="collapse-extraWrapper">
<div class="collapse-extraRender">
<slot name="extraRender"></slot>
</div>
</view>
Since I mentioned slot
here, I'll make it more verbose [smile]. Regarding the display of the above-mentioned titles and contents, the design considers that the developers can save time and effort, and have more operability. Basically, the input parameters are received in the form of slot
( Only for this component, content display related), so even if the back-end or front-end processing data carries HTMl
tags can be easily identified without redundant processing.
Since the panels can be expanded and collapsed, vice versa is also prohibited. I provide a simple property setting disabled
to determine whether it is operable. The way to achieve this is by setting the style of style
.
.nut-collapse-item-disabled {
color: #c8c9cc;
cursor: not-allowed;
pointer-events: none;
}
development and design
Using variables in Scss
You must be familiar with this function. To put it bluntly, you can control the style of CSS
through JS
. At present, Vue3
supports our use in CSS
Use variables in the code directly.
<template>
<span>NutUI</sapn>
</template>
<script>
export default {
data () {
return {
color: 'red'
}
}
}
</script>
<style vars="{ color }" scoped>
span {
color: var(--color);
}
</style>
Is it very simple? In fact, similar writing methods have been supported by similar plug-ins before.
- emotion
- jss
- styled-components
- aphrodite
- radium
- glamor
If you are interested in these plugins, you can try them. The editor has used styled-components
, and it is very easy to get started. Before getting started, I suggest that you understand the concept of CSS-in-JS
.
Component development and adaptation
Want to be a contributor to NutUI ? If you also want to contribute your own components to NutUI , here are some points for adapting the applet~
It is relatively easy to obtain the H5
DOM
element during development of ---decfeb326aafc08a7365bbf660a7bb18---, through document
or ref
. But we can't get it in this way when we adapt the applet, we need to get it according to the method provided by Taro
.
import Taro, { eventCenter, getCurrentInstance as getCurrentInstanceTaro } from '@tarojs/taro';
eventCenter.once((getCurrentInstanceTaro() as any).router.onReady, () => {
const query = Taro.createSelectorQuery();
query.selectAll('.collapse-content').boundingClientRect();
query.exec((res) => {
console.log(res);
});
});
通过以上方法可以获取到节点的信息,包括width
、 height
、 x
、 y
等,大家可以体验试一下查看information obtained. Another point to note is that when setting the style
style to the element, it is best to use the style
variable in the component to receive, do not assign values directly.
// 类似这种方式改变 style
const style = reactive({
color: 'red',
height: '100px',
});
const change = () => {
style.color = 'blue';
}
vue3 component communication
During component development, because nut-collapse
nut-collapse-item
parent and child components need to communicate, I use the provide/inject
method, so this communication method is simple Learn to understand.
Regarding the way of component communication, props、emit、attrs
and so on, everyone must already be familiar with it, so I won't be embarrassed. Now I will briefly share with you the parameter transfer form of provide/inject
, this API already existed in vue2.
//a.vue 组件
//创建一个 provide
import { defineComponent, provide } from 'vue';
export default defineComponent({
setup () {
const msg: string = 'Hello NutUI';
// provide 出去
provide('msg', msg);
}
})
//b.vue 组件
//接收数据
import { defineComponent, inject } from 'vue'
export default defineComponent({
setup () {
const msg: string = inject('msg') || '';
}
})
Through the above 2 examples, the operation is very simple, but it should be noted that provide
is not responsive, if you want to make it responsive, you need to pass in responsive data.
provide
The data provided does not consider the component hierarchy, that is, the component that initiates provide
can be used as a dependency provider for all its subordinate components.
The realization principle of provide
and inject
is mainly realized by using prototype and prototype chain.
In Vue3 provide
function is to add key-value pair---a625f1868e2b489ebc19c37bab165e3a provides
to the object property of the current component instance key/value
. Another place is that if the current component and the parent component provides
are the same, in the current component instance provides
object and the parent, then establish a link, that is, the prototype prototype
.
function provide(key, value) {
if (!currentInstance) {
if ((process.env.NODE_ENV !== 'production')) {
warn(`provide() can only be used inside setup().`);
}
}
else {
// 获取当前组件实例的 provides 属性
let provides = currentInstance.provides;
// 获取当前父级组件的 provides 属性
const parentProvides = currentInstance.parent && currentInstance.parent.provides;
if (parentProvides === provides) {
// Object.create() es6创建对象的一种方式,可以理解为继承一个对象,添加的属性是在原型下。
provides = currentInstance.provides = Object.create(parentProvides);
}
provides[key] = value;
}
}
I won't go into details about the implementation of inject
. If you are interested, you can go to the source code for a more in-depth understanding.
From the following code, you can get a general understanding, inject
First get the instance object of the current component, and then determine whether it is a root component, if it is a root component, return to appContext
of provides
, otherwise it returns provides
of the parent component. If the current key
provides
, return the value, otherwise, judge whether there is a default content, if the default content is a function, execute and pass call
--method binds the proxy object of the component instance to the this
of the function, otherwise it returns the default content directly.
function inject(key, defaultValue, treatDefaultAsFactory = false) {
// 如果是被一个函数式组件调用则取 currentRenderingInstance
const instance = currentInstance || currentRenderingInstance;
if (instance) {
// 如果intance位于根目录下,则返回到appContext的provides,否则就返回父组件的provides
const provides = instance.parent == null
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides;
if (provides && key in provides) {
return provides[key];
}
// 如果参数大于1个 第二个则是默认值 ,第三个参数是 true,并且第二个值是函数则执行函数。
else if (arguments.length > 1) {
return treatDefaultAsFactory && isFunction(defaultValue)
? defaultValue.call(instance.proxy)
: defaultValue;
}
}
}
大致可以这么理解provide
API 调用的时候,设置父级的provides
provides
对象上的属性, inject
When obtaining provides
the attribute value in the object, the priority is to obtain provides
the attribute of the object itself. If it cannot be found by itself, it will go to the previous object along the prototype chain to find it.
Summarize
This article mainly introduces the design ideas and implementation principles of components in NutUI 折叠面板
, and shares some problems encountered in development, hoping to help you in development.
If you encounter problems during development, you can always mention issue
, and the students in the NutUI team will take it seriously and solve the problem. If you have good components, both business and general, you can submit them to the NutUI component library PR
, and you are very welcome to participate in the joint construction.
Finally, I would like to thank the team and students who have always supported NutUI . Your needs and suggestions have made our component library better and better, and we will continue to work hard to achieve a higher level!
Come and support us with a Star ❤️~
Article reference link:
- Implementation principle of Provide / Inject in Vue3: https://juejin.cn/post/7064904368730374180
- I read the vuex4 source code in one article. It turns out that provide/inject uses the prototype chain magically? : https://juejin.cn/post/6963802316713492516
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。