foreword
In most client applications, date selection and operation is a common function, and using the calendar component to implement this function is often an efficient solution. For the design and development of calendar components, in common open source projects, there are usually two design ideas:
- Horizontal switching display, a single month is rendered by default, and the month can be switched by pressing the button or sliding left and right;
- Vertically switch the display, the default rendering displays multiple months, and slide up and down to switch the month;
For example, adding a picker for view switching, adding custom buttons, date radio/multi-selection, custom copywriting, date range restrictions, etc., these are basically functional extensions based on two ideas.
In daily application, both methods have their own advantages and disadvantages:
- Horizontal switching, fewer nodes for initial rendering, and better rendering performance;
- Vertical switching, more intuitive visual experience, better interactive operation;
However, it is impossible to have both, and the choice between interactive experience and performance is a problem that must always be faced. With the continuous development of mobile devices and the continuous improvement of mobile browsers, the compatibility and operating efficiency of user devices have been significantly improved. Therefore, this article mainly describes the NutUI Calendar component implemented by vertical switching. .
Theme introduction
Today's topic is the design and implementation of the NutUI Calendar component. The Calendar component is a calendar component of NutUI. It is used to provide users with an intuitive way to select dates, switch months by sliding, and support the selection of a single date and date range. Custom date content and other functions. Today, let's take a look at how to realize the function of the component step by step in the development process of the component.
Component Design Ideas
The calendar component, no matter how the interaction is designed, the processing of date and time data is essential, after all, the view also serves the data information. The vertical switching display method adopted in this article also means that we need to make some optimization adjustments in the rendering performance of nodes. So our implementation ideas mainly have the following points:
- Date data processing, initialize the original data at one time, and render node elements in segments in the visible area.
- Reduce the rendering cost of node elements by applying virtual lists
- Handling of scroll events and boundary conditions
- Complete functions, enrich Slots, Props, Events, etc., to improve scalability
Component implementation principle
Basic parameter requirements
When processing date data, we need to clarify the basic time input parameters we need, such as: the optional time range of the calendar component, the currently selected time.
Through the analysis and processing of the incoming parameters, the data content we need is obtained, and in the subsequent development process, the rendering of the component content and event processing are completed.
Here I drew a picture for your better understanding:
- Raw date data : is the raw data we calculated based on the date range
- Currently selected date : The current month is displayed in the visible range. It is necessary to determine whether the selected date is within the date range.
- Display range interval : processed according to the currently selected date, which is the current data range that needs to be rendered
- Container size information : used to calculate the displacement information when the date is scrolled and switched
Date data processing
The calculation of date data requires multiple processing procedures. First, we need to calculate whether the incoming date range exists. If it does not exist, the time range of the latest year is used by default. Then count how many months exist. Iterate through the generated date data according to the number of months.
When calculating the date of a single month, the number of weeks of 第一天
and 最后一天
in each month is different, we need to base on the different 星期数
, the previous month The date of the next month will be completed. In this way, the calculation of the offset of the starting position of No. 1 can be omitted, and it can also pave the way for function expansion.
// 获取单个月的日期与状态
const getDaysStatus = (currMonthDays: number, dateInfo: any) => {
let { year, month } = dateInfo;
return Array.from(Array(currMonthDays), (v, k) => {
return {
day: k + 1,
type: "curr",
year,
month,
};
});
// 获取上一个月的最后一周天数,填充当月空白
const getPreDaysStatus = (
preCurrMonthDays: number
weekNum: number,
dateInfo: any,
) => {
let { year, month } = dateInfo;
if ( weekNum >= 7) {
weekNum -= 7;
}
let months = Array.from(Array(preCurrMonthDays), (v, k) => {
return {
day: k + 1,
type: "prev",
year,
month,
};
});
return months.slice(preCurrMonthDays - weekNum);
};
};
The processed data is as follows:
virtual list
When the amount of data we generate or load is very large, serious performance issues can arise, causing the view to be unresponsive to operations for a period of time. The rendering problem of the view is more obvious in the applet. To solve this problem, the virtual list is a good solution: instead of the view generated by the full rendering data, you can only render the view of the current visible viewport, Views in non-viewable areas are rendered when the user scrolls to the visible area.
For example, long list rendering (virtual list) in Taro :
Of course, the above is just a simple application, and the construction of the calendar component needs to be optimized based on this. As shown in the figure below, the months wrapper is the container that needs to display the months. This is set up because there will be more than one month in our viewport. At the same time, because a single month contains many nodes, when rendering is performed after passing 视口边界
, there may be a blank phenomenon, so we can reserve part of the content of the month, and perform node changes and rendering in the invisible area.
As shown in FIG,
- scrollWarpper : is a container with a height of the total month height, mainly used as a scroll container in the viewport;
- monthsWrapper : The container for the currently rendered month;
- viewport : is the current viewport range;
When the scroll event fires, the scrollWrapper moves down or up. After reaching the boundary, the month information within the monthsWrapper changes, and its overall height may also change. By modifying the transition of monthsWrapper, it is guaranteed that after the month is changed, the content in the viewport remains unchanged, and the data outside the viewport is updated.
While applying the virtual list, combined with the current mainstream framework, the data is added to the responsive data of the framework. The framework uses the diff algorithm or other mechanisms to reuse DOM nodes to a certain extent according to different data, reducing the number of DOM nodes. Add and delete elements. After all, frequent DOM additions and deletions are a relatively performance-intensive thing.
<!-- 视口 -->
<view class="nut-calendar-content" ref="months" @scroll="mothsViewScroll">
<!-- 整体容器-设置一个总体高度用以撑起视口 -->
<view class="calendar-months-panel" ref="monthsPanel">
<!-- 月份容器 -->
<view
class="viewArea"
ref="viewArea"
:style="{ transform: `translateY(${translateY}px)` }"
>
<view
class="calendar-month"
v-for="(month, index) of compConthsData"
:key="index"
>
<view class="calendar-month-title">{{ month.title }}</view>
<view class="calendar-month-con">
<view
class="calendar-month-item"
:class="type === 'range' ? 'month-item-range' : ''"
>
<template v-for="(day, i) of month.monthData" :key="i">
<view
class="calendar-month-day"
:class="getClass(day, month)"
@click="chooseDay(day, month)"
>
<!-- 日期显示slot -->
<view class="calendar-day">
<slot name="day" :date="day.type == 'curr' ? day : ''">
{{ day.type == 'curr' ? day.day : '' }}
</slot>
</view>
<view
class="calendar-curr-tip-curr"
v-if="!bottomInfo && showToday && isCurrDay(day)"
>
今天
</view>
<view
class="calendar-day-tip"
:class="{ 'calendar-curr-tips-top': rangeTip(day, month) }"
v-if="isStartTip(day, month)"
>
{{ startText }}
</view>
<view class="calendar-day-tip" v-if="isEndTip(day, month)"
>{{ endText }}</view
>
</view>
</template>
</view>
</view>
</view>
</view>
</view>
</view>
Event Handling and Boundary States
event selection
In the Calendar component, the switching of the month is implemented by listening to the scroll event.
The use of scroll events is considered because of the compatible processing for converting Taro to WeChat applet. The touchmove event can also realize the loading and switching interaction, but to achieve the scrolling effect, the touch event needs to frequently trigger the event to modify the position of the element. pause.
Boundary conditions
After the event is determined, the judgment of boundary conditions is a problem we need to consider: the height occupied by each month is not necessarily the same. Each month contains several weeks, not necessarily the same. As a result, the height occupied by each month is not necessarily the same. Therefore, in order to accurately judge the position information of the current scroll, it is necessary to find a similar point to judge.
Here we use the height of a single date as the reference value, calculate the height of the month through the height of a single date, and obtain the average height of a single month. The scroll position is divided by the average height to approximate current.
As shown below:
In the process of calculating the height, because the unit of the applet is rpx and h5 is rem, it is necessary to convert the px.
let titleHeight, itemHeight;
//计算单个日期高度
//对小程序与H5,rpx与rem转换px处理
if (TARO_ENV === "h5") {
titleHeight = 46 * scalePx.value + 16 * scalePx.value * 2;
itemHeight = 128 * scalePx.value;
} else {
titleHeight =
Math.floor(46 * scalePx.value) + Math.floor(16 * scalePx.value) * 2;
itemHeight = Math.floor(128 * scalePx.value);
}
monthInfo.cssHeight =
titleHeight +
(monthInfo.monthData.length > 35 ? itemHeight * 6 : itemHeight * 5);
let cssScrollHeight = 0;
//保存月份位置信息
if (state.monthsData.length > 0) {
cssScrollHeight =
state.monthsData[state.monthsData.length - 1].cssScrollHeight +
state.monthsData[state.monthsData.length - 1].cssHeight;
}
monthInfo.cssScrollHeight = cssScrollHeight;
When we get the current average current, we can judge the boundary conditions.
const mothsViewScroll = (e: any) => {
const currentScrollTop = e.target.scrollTop;
// 获取平均current
let current = Math.floor(currentScrollTop / state.avgHeight);
if (current == 0) {
if (currentScrollTop >= state.monthsData[current + 1].cssScrollHeight) {
current += 1;
}
} else if (current > 0 && current < state.monthsNum - 1) {
if (currentScrollTop >= state.monthsData[current + 1].cssScrollHeight) {
current += 1;
}
if (currentScrollTop < state.monthsData[current].cssScrollHeight) {
current -= 1;
}
} else {
// 获取视口高度 判断是否已经到最后一个月
const viewPosition = Math.round(currentScrollTop + viewHeight.value);
if (
viewPosition <
state.monthsData[current].cssScrollHeight +
state.monthsData[current].cssHeight &&
currentScrollTop < state.monthsData[current].cssScrollHeight
) {
current -= 1;
}
if (
current + 1 <= state.monthsNum &&
viewPosition >=
state.monthsData[current + 1].cssScrollHeight +
state.monthsData[current + 1].cssHeight
) {
current += 1;
}
if (currentScrollTop < state.monthsData[current - 1].cssScrollHeight) {
current -= 1;
}
}
if (state.currentIndex !== current) {
state.currentIndex = current;
setDefaultRange(state.monthsNum, current);
}
//设置月份标题信息
state.yearMonthTitle = state.monthsData[current].title;
};
Let's take a look at the effect:
Perfect function
Through the above process, we have completed a basic rolling calendar component. On this basis, we need to make some improvements to extend the generality of the component.
- Add slots for date information, allowing custom display of date information
- Slots are provided at the title. User-friendly insertion of custom actions
- Title, button, date range copy and other information provide props settings
- Add callback methods, such as selecting a date, clicking a date, closing the calendar, etc.
// 未传入的slot不进行加载,减少无意义的dom
<view
class="calendar-curr-tips calendar-curr-tips-top"
v-if="topInfo"
>
<slot name="topInfo" :date="day.type == 'curr' ? day : ''"></slot>
</view>
Epilogue
This article introduces the design ideas and implementation principles of the Calendar component in NutUI, hoping to provide you with some inspiration and ideas. Finally, let's mention our NutUI component library. For a long time, the team's small partners have been dedicated to maintaining NutUI. In the days to come, this kind of persistence will not give up. We will still actively maintain and iterate, provide technical support for students in need, and will also publish some related articles from time to time to help everyone better understand and use Our component library.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。