一、描述

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 如下:

名称类型说明
captionPropTypes.any日期文本
customStylePropTypes.object样式
fillerPropTypes.bool用来占位的空日期
hasEventPropTypes.bool目前不确定
isSelectedPropTypes.bool是否选中
isTodayPropTypes.bool是否是今天
isWeekendPropTypes.bool是否是周末
isDisabledPropTypes.bool是否禁用
onPressPropTypes.funcpress事件
usingEventsPropTypes.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