Design and Implementation of the NutUI Calendar Component with Vertical Switching
This article explains the design choices, data processing, virtual‑list optimization, scroll handling, and extensible slot/prop architecture used to build a high‑performance vertical‑switching calendar component in the NutUI front‑end library.
In most client applications, date selection is a common feature, and using a calendar component is an efficient solution. Two typical design approaches exist: horizontal switching that renders a single month and allows month changes via buttons or swipes, and vertical switching that renders multiple months with up‑down scrolling.
The article focuses on the vertical‑switching implementation of the NutUI Calendar component, aiming to balance visual intuitiveness with rendering performance on modern mobile browsers.
The implementation follows several key ideas: initializing raw date data once, segment‑rendering nodes within the visible area, applying a virtual‑list technique to reduce rendering cost, handling scroll events and boundary conditions, and enriching the component with slots, props, and events for extensibility.
Parameter handling starts with defining the selectable date range and the currently selected date, then computing the original date data, the current selection, the display range, and container dimensions needed for scrolling calculations.
// 获取单个月的日期与状态
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);
};
};When the data volume is large, rendering all nodes can cause severe performance issues, especially in mini‑programs where frequent setData calls lead to UI lag. A virtual list renders only the visible viewport and creates nodes for off‑screen items on demand, dramatically improving responsiveness.
The component’s layout consists of a scrollWrapper (the overall scroll container), a monthsWrapper (holds the rendered months), and a viewport (the visible area). As the user scrolls, the scrollWrapper moves, and monthsWrapper updates its height and content while preserving the viewport’s visual stability.
{{ month.title }}Event handling relies on scroll listeners to detect month changes, avoiding heavy touchmove handling that would cause frequent setData updates. Boundary conditions are calculated by measuring each month’s height (which varies with the number of weeks) and using a baseline date‑cell height to estimate the current month index.
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; 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;
};After the basic scrolling calendar is functional, further enhancements add slots for custom date rendering, slots for the header, props for titles, button texts, and callbacks for date selection, closing, and other interactions.
In conclusion, the article demonstrates how to build a performant, vertically scrolling calendar component in NutUI by combining careful data initialization, virtual‑list rendering, precise scroll‑boundary calculations, and a flexible slot/prop architecture, providing a solid reference for front‑end developers.
JD Retail Technology
Official platform of JD Retail Technology, delivering insightful R&D news and a deep look into the lives and work of technologists.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.