Rax 日历表单组件 rax-calendar 的内部实现机制

一、描述

rax-calendar 是 rax 内置的用于进行日历选择的表单组件,相比较于 rax-datepicker 组件, rax-calendar 是 Rax 自己内部实现的一种方案,而不基于 weex 的内置 module。

文档地址:

如果只是使用 rax-calendar 组件还是很简单,没有什么复杂的东西,只是它的 props 也挺多,具体可以看文档,这里不重复。

先上个效果图:(底部出现的白色内容是因为 gif 录制的时候压缩造成的)

GIF.gif

二、源码

rax-calendar 的源码很多,因为整个 Calender 都是内部实现掉的,源码文件列表:

  • Day.js:单个日期组件
  • format.js:日期格式化库
  • index.js:组件入口
  • moment.js:日期库
  • styles.js:组件样式(主组件和 Day 组件样式都在这个文件中)

1、Day.js 单日期组件

Day 是一个无状态组件,但是 rax 还是 extends Component, 对这种不严谨的态度表示不满,不认为 Day.js 后面会进行扩展或者需要维护自身状态。

rax-calendar 中,显示每一个日期使用的是 <Day>(注意是每一个日期) ,组件的最外层是 Touchable,用于可点击,而其他部分则是由 View 或者 Text组成。

上面的示例中可以发现,Day 组件的主要状态包括:选中、未选中、disable,如果 props 传入的 isSelectedtrue,则样式会变成带圆圈背景的日期。

Day 组件中,值得学习的地方是对样式的处理方法 dayCircleStyle(isWeekend, isSelected, isToday)dayTextStyle(isWeekend, isSelected, isToday, isDisabled)

目前Day.js 支持的 props 如下:

名称 类型 说明
caption PropTypes.any 日期文本
customStyle PropTypes.object 样式
filler PropTypes.bool 用来占位的空日期
hasEvent PropTypes.bool 目前不确定
isSelected PropTypes.bool 是否选中
isToday PropTypes.bool 是否是今天
isWeekend PropTypes.bool 是否是周末
isDisabled PropTypes.bool 是否禁用
onPress PropTypes.func press事件
usingEvents PropTypes.bool 目前不确定

1)filter

最开始我没搞懂,filter 是用来干嘛的,后面发现是用来占位的,因为 rax-calendar 是类似于表格的布局方式(每行7列,750/7,下面会详细说明样式的实现),而有时候开头或者结尾是没有日期的,比如某个月的 1 号是周三,那第一行的前三个(周日、1、2)都需要进行占位,占位不能拥有事件,不能触发事件,因此当 props.filter === true 的时候,渲染的内容如下:

<Touchable>
  <View style={[styles.dayButtonFiller, customStyle.dayButtonFiller]}>
    <Text style={[styles.day, customStyle.day]} />
  </View>
</Touchable>

2)isWeekend / isToday / isSelected

三者主要是用来控制样式的,当时当三者重叠的时候,比如今天是周末的某一天,并且还选中了今天,优先级如下:

isSelected > isWeekend > isToday

下面的效果图录制日期是 2018-08-25 周六,可以看到,weekend 的样式高于 today 的样式,但是 selected 的样式高于 weekend 的样式。

GIF.gif

3)isDisabled

isDisabled 是用来控制是否允许点击事件的,rax-calendar 组件有两个 props ,分别是 startDateendDate,当不在许可范围内时,仍旧可以查看日期,但是不会触发 onDateSelect 事件。

下面的示例中,endDate={'2018-10-25'},因此当点击 27 的时候,是无法触发事件的。

111111.gif

4)hasEvent & usingEvents

这个比较坑的一点就是,rax-calendar 的文档中支持的 props 少写了一个 eventDates,但是代码示例中却出现了,而在看源码的时候发现,hasEventusingEvents 是和 eventDates 相关的,是对日期的特殊标记,本身样式很简单,就是渲染出个样式而已。

{usingEvents &&
  <View style={[
    styles.eventIndicatorFiller,
    customStyle.eventIndicatorFiller,
    hasEvent && styles.eventIndicator,
    hasEvent && customStyle.eventIndicator]}
  />
}

可以看到,每个日期下面都有一个颜色为 #cccccc 的小圆点,这是由 eventDates 带过去的,本身他的类型是数组。

注意,eventDates 是用在Calendar 组件而不是 Day组件,因此是个数组,在 Calendar 组件中,对数组遍历并且挂载到 Day 组件上。

 <Calendar
    ref="calendar"
    eventDates={['2017-01-02', '2017-01-05', '2017-01-28', '2017-01-30']}
 />

2222.jpg

2、moment.js 日期库

moment.js 是 rax-calendar 内部实现的一个日期处理库,因为日期处理的库很多,而且大多原理都一样,无非基于 Date 对象进行各种封装,加上整个源码有 160 行,就不一一展开叙述了,源码的仓库地址在:

不是很确定这个 moment.js 是什么时候实现的,以及是否是自己实现的(应该是自己实现的,不然就直接使用 npm 依赖了),因为它里面实现整个 Moment 对象,使用的还是 Function 的方式,而不是 ES6 的 Class。

主要暴露出来的属性或者方法列表如下:

  • proto.isValid = isValid;
  • proto.year = year;
  • proto.month = month;
  • proto.date = date;
  • proto.hour = hour;
  • proto.second = second;
  • proto.minute = minute;
  • proto.format = format;
  • proto.daysInMonth = daysInMonth;
  • proto.startOfMonth = startOfMonth;
  • proto.addMonth = addMonth;
  • proto.isoWeekday = isoWeekday;
  • proto.isSameMonth = isSameMonth;
  • proto.setDate = setDate;
  • proto.getTime = getTime;

其中 format 方法来自 format.js,下面介绍这个自实现的方法库。

如果要自己使用的话,没必要再去实现一个 moment,rax-calendar 应该是结合了 canlendar 的具体使用场景,对其进行了优化或者适配。

如果要自己使用,还是推荐使用 dayjshttp://www.ptbird.cn/day-js.html API 感觉都差不多,功能也差不多。

3、format.js 日期格式化库

前面说了, moment.js 中 format 的实现是引入了 format.js,也就是将 format 单独抽出来了。

日期格式化也没什么好说的,毕竟实现原理已经很明了,无非就是代码的实现过程,format 里面很关键的一点实际上是对 format Key 的识别,format.js 是通过 Regx 进行匹配,使用的 Tokens 如下:

var formattingTokens = /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g;

主要的匹配过程是:

function makeFormatFunction(format) {
  var array = format.match(formattingTokens), i, length;

  for (i = 0, length = array.length; i < length; i++) {
    if (formatTokenFunctions[array[i]]) {
      array[i] = formatTokenFunctions[array[i]];
    } else {
      array[i] = removeFormattingTokens(array[i]);
    }
  }

  return function(mom) {
    var output = '', i;
    for (i = 0; i < length; i++) {
      output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
    }
    return output;
  };
}

4、index.js 主组件

1)整体实现原理

index.jsrax-calendar 组件的入口及主要部分,但是代码很长,有 240 多行,代码仓库:

rax-calendar 主要维护了 currentMonthMomentselectedMoment 两个 state,一个是当前的月份,一个是当前选中的时间日期。

整个组件代码实现上来说没有什么太大的难度,对于 Day 组件了解之后,再去了解 index.js 主要了解其对于每个月份的渲染实现即可。

渲染实现主要集中在 renderMonthView(argMoment, eventDatesMap) 这个方法中,这个方法有 80 多行,就不贴源代码了,感兴趣的可以去仓库找找这个方法,这个方法主要功能是渲染一个月的日历。

注意,renderMonthView 只是用来渲染一个月的日历,当触发上一月或者下一个月的时候,会对当前维护的 state.currentMonthMoment 变动,然后渲染新的月份,因此,rax-calendar 组件实际上是可以无限渲染的,只不过不在 startDateendDate 之间的日期都是 disabled 的

一周7天,因此 Calendar 在渲染的时候,开一个 <View>,往里面 push <Day> 组件,每处理换 7 个日期,就重新开一个 <View>,然后继续 push,最后把所有的 <View> 都 push 到一起,形成最终的组件。(这和 rax-multirow 的实现方式差不对)

至于占位的问题,判断当月第一天是周几,然后进行 offset 位置计算,这个过程中还需要结合 this.props.weekStart ,每周起始日不同,自然偏移量也就不同,然后进行多余位置的占位。占位不仅只在月前开始,看了一下,rax-calendar 在每个月最后一行的日期也使用了占位。

renderMonthView() 方法中,实际上进行了 do..while 的循环,将一个月的日期进行循环输出,这期间的处理包括了 filter 占位、判断 today、判断 weekend、判断 selected 等。

其他的一些方法如 selectDate/onPrev/onNext 是对传入事件的处理,而 prepareEventDates 则是对 props.eventDates 的处理。

同样需要注意的是,在 selectDate 中,传入的参数 date 实际上是 day,不包括 month 和 year,因此需要继续拧格式化之后才能返回,所以其代码如下:

selectDate(date) {
    this.setState({ selectedMoment: date });
    this.props.onDateSelect && this.props.onDateSelect(date.format(this.props.dateFormat));
  }

2)showControls

默认的顶部样式如下:

333.jpg

顶部的渲染都在 renderTopBar 方法中,但是这个方法中使用了一个 props 是 showControls,这个在文档中也没有提到,默认值是 true,主要是用来控制是否显示 上一月下一月 按钮的。如果是 false,则样式如下:

555.jpg

至于其他的顶部的样式渲染都集中在 renderTopBar 方法中,很简单,不在这里重复。

3)onSwipeNext & onSwipePrev

在源码中发现了两个文档中没有提到的 props:

onSwipeNext: PropTypes.func,
onSwipePrev: PropTypes.func,

这两个 props 目前在源码(rax-calendar^0.6.5)中没有任何应用,应该是后续要支持的手势切换上下月。

4)showDayHeadings

这个 props 也是在文档中没有出现,但是在组件中已经可以应用了,主要是用来渲染每个周的顶部名称,默认值是 false,当传递为 true 的时候,整个 calendar 组件样式如下:

z.jpg

props.showDayHeading = true 的时候,会调用 this.setHeading,在模板渲染中,传入了 props.titleFormat ,但是 setHeading 方法目前不接受任何的参数,这应该也是后续的扩展,因为现在只能显示英文。
显示的英文来自于 defaultProps 中的 dayHeadings: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],

5、styles.js 样式

index.js 和 Day.js 的样式都在 styles.js 中,主要是需要维护 Day 的好几种不同的样式,其他得到没有什么复杂的地方。

之所以使用 js 来维护样式,是因为目前的设计是每周 7 天,因此最终是 DEVICE_WIDTH / 7 来计算每个 Date 的容器宽度的,应该是为了方便之后的扩展而设计的。

三、应用

1、代码

简单写了一下组件的应用,代码如下:

import {createElement, Component} from 'rax';
import View from 'rax-view';
import Text from 'rax-text';
import Calendar from 'rax-calendar';
import Touchable from 'rax-touchable';
import styles from './App.css';

class App extends Component {
  state = {
    selectedDate: '2018-08-24',
    calendar: false,
    navText: ''
  }
  dateSelectHandle = (date) => {
    console.log(date);
    this.setState({selectedDate: date});
    // this.setState({selectedDate: date, calendar: false});
  }
  touchPrevHandle = () => {
    this.setState({navText: '上翻页'});
  }
  touchNextHandle = () => {
    this.setState({navText: '下翻页'});
  }
  showCalendarHandle = () => {
    this.setState({
      calendar: !this.state.calendar
    });
  }
  renderCalendar = () => {
    return this.state.calendar
      ? <Calendar
          ref="calendar"
          selectedDate={this.state.selectedDate}
          startDate={'2017-01-01'}
          endDate={'2018-10-24'} 
          titleFormat={'YYYY年MM月'}
          prevButtonText={'上一月'}
          nextButtonText={'下一月'}
          weekStart={0}
          onDateSelect={this.dateSelectHandle}
          onTouchNext={this.touchPrevHandle}
          showDayHeadings={true}
          showControls={true}
          onTouchPrev={this.touchNextHandle}/>
      : null;
  }
  render() {
    const {selectedDate, calendar, navText} = this.state;
    const {renderCalendar, showCalendarHandle} = this;
    return (
      <View style={styles.app}>

        <View style={styles.selectTextWrapper}>
          <Touchable style={styles.btn} onPress={showCalendarHandle}>
            <Text>{selectedDate}</Text>
          </Touchable>
        </View>
        <Text>{navText}</Text>
        {renderCalendar()}

      </View>
    );
  }
}

export default App;

2、样式:

.app {
  flex: 1;
  justify-content: center;
  align-items: center;
}

.title{
  color:red;
  font-size:30;
  margin-bottom:50;
}

.btn{
  flex-direction: row;
  align-items: center;
  justify-content: center;
  width: 750;
  border:1px solid #aaaaaa;
  padding:20;
  margin-bottom: 50;
}

.selectTextWrapper{
  flex-direction: row;
  justify-content: space-around;
}

3、效果:

aAA.gif

欢迎评论。
lingkb » Rax 日历表单组件 rax-calendar 的内部实现机制

发表评论