3
头图

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.


collapse 布局

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.


collapse 效果图

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°.


title icon

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.


源码中 slot 部分

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~


NutUI 秘笈

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);
  });
});

通过以上方法可以获取到节点的信息,包括widthheightxy等,大家可以体验试一下查看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:

  1. Implementation principle of Provide / Inject in Vue3: https://juejin.cn/post/7064904368730374180
  2. 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

京东设计中心JDC
696 声望1k 粉丝

致力为京东零售消费者提供完美的购物体验。以京东零售体验设计为核心,为京东集团各业务条线提供设计支持, 包括线上基础产品体验设计、营销活动体验设计、品牌创意设计、新媒体传播设计、内外部系统产品设计、企...