6
头图

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:

思路图

  1. Date data processing, initialize the original data at one time, and render node elements in segments in the visible area.
  2. Reduce the rendering cost of node elements by applying virtual lists
  3. Handling of scroll events and boundary conditions
  4. 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.

  1. Add slots for date information, allowing custom display of date information
  2. Slots are provided at the title. User-friendly insertion of custom actions
  3. Title, button, date range copy and other information provide props settings
  4. 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.

Come and support us with a Star ❤️~


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

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