Kaynağa Gözat

beginnings of uni-directional rendering flow

Adam Shaw 7 yıl önce
ebeveyn
işleme
b33b539c5f

+ 162 - 183
src/Calendar.ts

@@ -20,7 +20,7 @@ import { DateEnv, DateInput } from './datelib/env'
 import { DateMarker, startOfDay } from './datelib/marker'
 import { createFormatter } from './datelib/formatting'
 import { Duration, createDuration } from './datelib/duration'
-import { CalendarState, INITIAL_STATE, reduce } from './reducers/main'
+import { CalendarState, reduce } from './reducers/main'
 import { parseSelection, SelectionInput } from './reducers/selection'
 
 export default class Calendar {
@@ -45,7 +45,6 @@ export default class Calendar {
 
   view: View // current View object
   viewsByType: { [viewName: string]: View } // holds all instantiated view instances, current or not
-  currentDate: DateMarker // private (public API should use getDate instead)
   theme: Theme
   optionsManager: OptionsManager
   viewSpecManager: ViewSpecManager
@@ -66,9 +65,11 @@ export default class Calendar {
   footer: Toolbar
   toolbarsManager: Iterator
 
-  state: CalendarState = INITIAL_STATE
+  state: CalendarState
   isReducing: boolean = false
   actionQueue = []
+  isSkeletonRendered: boolean = false
+  renderingPauseDepth: number = 0
 
 
   constructor(el: HTMLElement, overrides: OptionsInput) {
@@ -83,7 +84,7 @@ export default class Calendar {
     this.optionsManager = new OptionsManager(this, overrides)
     this.viewSpecManager = new ViewSpecManager(this.optionsManager, this)
     this.initDateEnv() // needs to happen after options hash initialized
-    this.initCurrentDate()
+    this.initToolbars()
 
     this.constructed()
     this.hydrate()
@@ -121,6 +122,46 @@ export default class Calendar {
   // -----------------------------------------------------------------------------------------------------------------
 
 
+  hydrate() {
+    this.state = this.buildInitialState()
+
+    let rawSources = this.opt('eventSources') || []
+    let singleRawSource = this.opt('events')
+
+    if (singleRawSource) {
+      rawSources.unshift(singleRawSource)
+    }
+
+    this.pauseRendering()
+
+    for (let rawSource of rawSources) {
+      this.dispatch({ type: 'ADD_EVENT_SOURCE', rawSource })
+    }
+
+    this.dispatch({ type: 'SET_VIEW_TYPE', viewType: this.opt('defaultView') })
+
+    this.resumeRendering()
+  }
+
+
+  buildInitialState(): CalendarState {
+    return {
+      loadingLevel: 0,
+      currentDate: this.getInitialDate(),
+      dateProfile: null,
+      eventSources: {},
+      eventStore: {
+        defs: {},
+        instances: {}
+      },
+      selection: null,
+      dragState: null,
+      eventResizeState: null,
+      businessHoursDef: false
+    }
+  }
+
+
   dispatch(action) {
     this.actionQueue.push(action)
 
@@ -139,39 +180,33 @@ export default class Calendar {
       let newState = this.state
       this.isReducing = false
 
-      if (this.view) {
-        this.view.set('eventStore', newState.eventStore)
-        // TODO: when to unset?
-      }
-
       if (!oldState.loadingLevel && newState.loadingLevel) {
         this.publiclyTrigger('loading', [ true, this.view ])
       } else if (oldState.loadingLevel && !newState.loadingLevel) {
         this.publiclyTrigger('loading', [ false, this.view ])
       }
+
+      if (!this.renderingPauseDepth) {
+        this.renderView()
+      }
     }
   }
 
 
-  reduce(state: CalendarState, action: object, calendar: Calendar): CalendarState {
-    return reduce(state, action, calendar)
+  pauseRendering() {
+    this.renderingPauseDepth++
   }
 
 
-  hydrate() {
-    let rawSources = this.opt('eventSources') || []
-    let singleRawSource = this.opt('events')
-
-    // TODO: prevent rerenders for each thing
-    // should pause rendering
-
-    if (singleRawSource) {
-      rawSources.unshift(singleRawSource)
+  resumeRendering() {
+    if (!(--this.renderingPauseDepth)) {
+      this.renderView()
     }
+  }
 
-    rawSources.forEach((rawSource) => {
-      this.dispatch({ type: 'ADD_EVENT_SOURCE', rawSource })
-    })
+
+  reduce(state: CalendarState, action: object, calendar: Calendar): CalendarState {
+    return reduce(state, action, calendar)
   }
 
 
@@ -207,6 +242,19 @@ export default class Calendar {
   // -----------------------------------------------------------------------------------------------------------------
 
 
+  installNewView(viewType) {
+
+    if (this.view) {
+      this.view.removeElement()
+      this.toolbarsManager.proxyCall('deactivateButton', this.view.type)
+    }
+
+    this.view =
+      this.viewsByType[viewType] ||
+      (this.viewsByType[viewType] = this.instantiateView(viewType))
+  }
+
+
   // Given a view name for a custom view or a standard view, creates a ready-to-go View object
   instantiateView(viewType: string): View {
     let spec = this.viewSpecManager.getViewSpec(viewType)
@@ -225,7 +273,8 @@ export default class Calendar {
   }
 
 
-  changeView(viewName: string, dateOrRange: RangeInput | DateInput) {
+  changeView(viewType: string, dateOrRange: RangeInput | DateInput) {
+    let dateMarker = null
 
     if (dateOrRange) {
       if ((dateOrRange as RangeInput).start && (dateOrRange as RangeInput).end) { // a range
@@ -233,26 +282,29 @@ export default class Calendar {
           visibleRange: dateOrRange
         })
       } else { // a date
-        this.currentDate = this.dateEnv.createMarker(dateOrRange as DateInput) // just like gotoDate
+        dateMarker = this.dateEnv.createMarker(dateOrRange as DateInput) // just like gotoDate
       }
     }
 
-    this.renderView(viewName)
+    this.dispatch({ type: 'SET_VIEW_TYPE', viewType, dateMarker })
   }
 
 
   // Forces navigation to a view for the given date.
   // `viewType` can be a specific view name or a generic one like "week" or "day".
   // needs to change
-  zoomTo(newDate: DateMarker, viewType?: string) {
+  zoomTo(dateMarker: DateMarker, viewType?: string) {
     let spec
 
     viewType = viewType || 'day' // day is default zoom
     spec = this.viewSpecManager.getViewSpec(viewType) ||
       this.viewSpecManager.getUnitViewSpec(viewType)
 
-    this.currentDate = newDate
-    this.renderView(spec ? spec.type : null)
+    if (spec) {
+      this.dispatch({ type: 'SET_VIEW_TYPE', viewType: spec.type, dateMarker })
+    } else {
+      this.dispatch({ type: 'NAVIGATE_DATE', dateMarker })
+    }
   }
 
 
@@ -260,73 +312,62 @@ export default class Calendar {
   // -----------------------------------------------------------------------------------------------------------------
 
 
-  initCurrentDate() {
+  getInitialDate() {
     let defaultDateInput = this.opt('defaultDate')
 
     // compute the initial ambig-timezone date
     if (defaultDateInput != null) {
-      this.currentDate = this.dateEnv.createMarker(defaultDateInput)
+      return this.dateEnv.createMarker(defaultDateInput)
     } else {
-      this.currentDate = this.getNow() // getNow already returns unzoned
+      return this.getNow() // getNow already returns unzoned
     }
   }
 
 
   prev() {
-    let view = this.view
-    let prevInfo = view.dateProfileGenerator.buildPrev(view.dateProfile)
-
-    if (prevInfo.isValid) {
-      this.currentDate = prevInfo.date
-      this.renderView()
-    }
+    this.dispatch({ type: 'NAVIGATE_PREV' })
   }
 
 
   next() {
-    let view = this.view
-    let nextInfo = view.dateProfileGenerator.buildNext(view.dateProfile)
-
-    if (nextInfo.isValid) {
-      this.currentDate = nextInfo.date
-      this.renderView()
-    }
+    this.dispatch({ type: 'NAVIGATE_NEXT' })
   }
 
 
   prevYear() {
-    this.currentDate = this.dateEnv.addYears(this.currentDate, -1)
-    this.renderView()
+    this.dispatch({ type: 'NAVIGATE_PREV_YEAR' })
   }
 
 
   nextYear() {
-    this.currentDate = this.dateEnv.addYears(this.currentDate, 1)
-    this.renderView()
+    this.dispatch({ type: 'NAVIGATE_NEXT_YEAR' })
   }
 
 
   today() {
-    this.currentDate = this.getNow() // should deny like prev/next?
-    this.renderView()
+    this.dispatch({ type: 'NAVIGATE_TODAY' })
   }
 
 
   gotoDate(zonedDateInput) {
-    this.currentDate = this.dateEnv.createMarker(zonedDateInput)
-    this.renderView()
+    this.dispatch({
+      type: 'NAVIGATE_DATE',
+      dateMarker: this.dateEnv.createMarker(zonedDateInput)
+    })
   }
 
 
   incrementDate(delta) { // is public facing
-    this.currentDate = this.dateEnv.add(this.currentDate, createDuration(delta))
-    this.renderView()
+    this.dispatch({
+      type: 'NAVIGATE_DELTA',
+      delta: createDuration(delta)
+    })
   }
 
 
   // for external API
   getDate(): Date {
-    return this.dateEnv.toDate(this.currentDate)
+    return this.dateEnv.toDate(this.state.currentDate)
   }
 
 
@@ -357,13 +398,19 @@ export default class Calendar {
   }
 
 
-  // High-level Rendering
+  // Rendering
   // -----------------------------------------------------------------------------------
 
 
   render() {
-    if (!this.contentEl) {
-      this.initialRender()
+    if (!this.isSkeletonRendered) {
+      this.renderSkeleton()
+      this.renderHeader()
+      this.renderFooter()
+      this.isSkeletonRendered = true
+      this.renderView()
+      this.trigger('initialRender')
+      Calendar.trigger('initialRender', this)
     } else if (this.elementVisible()) {
       // mainly for the public API
       this.calcSize()
@@ -371,7 +418,57 @@ export default class Calendar {
     }
   }
 
-  initialRender() {
+
+  renderView(forces?) {
+    let { view } = this
+
+    if (view && this.isSkeletonRendered) {
+      let viewScroll
+
+      if (!view.el) {
+        view.setElement(
+          createElement('div', { className: 'fc-view fc-' + view.type + '-view' })
+        )
+      }
+
+      if (!view.el.parentNode) {
+        this.contentEl.appendChild(view.el)
+      } else {
+        viewScroll = view.queryScroll()
+      }
+
+      this.freezeContentHeight()
+
+      // toolbar (updates every time unforunately)
+      this.setToolbarsTitle(this.view.title) // view.title set via updateMiscDateProps
+      this.updateToolbarButtons(this.state.dateProfile)
+      this.toolbarsManager.proxyCall('activateButton', view.type)
+
+      view.render(this.state, forces)
+
+      this.thawContentHeight()
+      this.updateViewSize() // TODO: respect isSizeDirty
+
+      if (viewScroll) {
+        this.view.applyScroll(viewScroll)
+      }
+    }
+  }
+
+
+  destroy() {
+    if (this.isSkeletonRendered) {
+      this.unrenderSkeleton()
+      this.isSkeletonRendered = false
+      this.view.removeElement()
+      this.view = null
+      this.trigger('destroy')
+      Calendar.trigger('destroy', this)
+    }
+  }
+
+
+  renderSkeleton() {
     let el = this.el
 
     el.classList.add('fc')
@@ -427,11 +524,6 @@ export default class Calendar {
 
     prependToElement(el, this.contentEl = createElement('div', { className: 'fc-view-container' }))
 
-    this.initToolbars()
-    this.renderHeader()
-    this.renderFooter()
-    this.renderView(this.opt('defaultView'))
-
     if (this.opt('handleWindowResize')) {
       window.addEventListener('resize',
         this.windowResizeProxy = debounce( // prevents rapid calls
@@ -440,18 +532,10 @@ export default class Calendar {
         )
       )
     }
-
-    this.trigger('initialRender')
-    Calendar.trigger('initialRender', this)
   }
 
 
-  destroy() {
-    let wasRendered = Boolean(this.contentEl && this.contentEl.parentNode)
-
-    if (this.view) {
-      this.clearView()
-    }
+  unrenderSkeleton() {
 
     this.toolbarsManager.proxyCall('removeElement')
     removeElement(this.contentEl)
@@ -472,12 +556,7 @@ export default class Calendar {
       this.windowResizeProxy = null
     }
 
-    if (wasRendered) {
-      GlobalEmitter.unneeded()
-
-      this.trigger('destroy')
-      Calendar.trigger('destroy', this)
-    }
+    GlobalEmitter.unneeded()
   }
 
 
@@ -486,90 +565,6 @@ export default class Calendar {
   }
 
 
-  // View Rendering
-  // -----------------------------------------------------------------------------------
-
-
-  // Renders a view because of a date change, view-type change, or for the first time.
-  // If not given a viewType, keep the current view but render different dates.
-  // Accepts an optional scroll state to restore to.
-  renderView(viewType?: string) {
-    let oldView = this.view
-    let newView
-
-    this.freezeContentHeight()
-
-    if (oldView && viewType && oldView.type !== viewType) {
-      this.clearView()
-    }
-
-    // if viewType changed, or the view was never created, create a fresh view
-    if (!this.view && viewType) {
-      newView = this.view =
-        this.viewsByType[viewType] ||
-        (this.viewsByType[viewType] = this.instantiateView(viewType))
-
-      newView.startBatchRender() // so that setElement+setDateProfile rendering are joined
-
-      let viewEl = createElement('div', { className: 'fc-view fc-' + viewType + '-view' })
-      this.contentEl.appendChild(viewEl)
-      newView.setElement(viewEl)
-
-      this.toolbarsManager.proxyCall('activateButton', viewType)
-    }
-
-    if (this.view) {
-      let newDateProfile = this.view.computeNewDateProfile(this.currentDate)
-      if (newDateProfile) {
-        this.view.setDateProfile(newDateProfile)
-        this.setToolbarsTitle(this.view.title)
-        this.currentDate = newDateProfile.date // might have been constrained by view dates
-        this.updateToolbarButtons(newDateProfile)
-        this.dispatch({
-          type: 'SET_DATE_PROFILE',
-          dateProfile: newDateProfile
-        })
-      }
-
-      if (newView) {
-        newView.stopBatchRender()
-      }
-    }
-
-    this.thawContentHeight()
-  }
-
-
-  // Unrenders the current view and reflects this change in the Header.
-  // Unregsiters the `view`, but does not remove from viewByType hash.
-  clearView() {
-    let currentView = this.view
-
-    this.toolbarsManager.proxyCall('deactivateButton', currentView.type)
-
-    currentView.removeElement()
-
-    this.view = null
-  }
-
-
-  // Destroys the view, including the view object. Then, re-instantiates it and renders it.
-  // Maintains the same scroll state.
-  // TODO: maintain any other user-manipulated state.
-  reinitView() {
-    let oldView = this.view
-    let scroll = oldView.queryScroll() // wouldn't be so complicated if Calendar owned the scroll
-    this.freezeContentHeight()
-
-    this.clearView()
-    this.calcSize()
-    this.renderView(oldView.type) // needs the type to freshly render
-
-    this.view.applyScroll(scroll)
-    this.thawContentHeight()
-  }
-
-
   // Resizing
   // -----------------------------------------------------------------------------------
 
@@ -667,13 +662,6 @@ export default class Calendar {
 
 
   freezeContentHeight() {
-    if (!(this.freezeContentHeightDepth++)) {
-      this.forceFreezeContentHeight()
-    }
-  }
-
-
-  forceFreezeContentHeight() {
     applyStyle(this.contentEl, {
       width: '100%',
       height: this.contentEl.offsetHeight,
@@ -683,19 +671,11 @@ export default class Calendar {
 
 
   thawContentHeight() {
-    this.freezeContentHeightDepth--
-
-    // always bring back to natural height
     applyStyle(this.contentEl, {
       width: '',
       height: '',
       overflow: ''
     })
-
-    // but if there are future thaws, re-freeze
-    if (this.freezeContentHeightDepth) {
-      this.forceFreezeContentHeight()
-    }
   }
 
 
@@ -761,8 +741,8 @@ export default class Calendar {
     let now = this.getNow()
     let view = this.view
     let todayInfo = view.dateProfileGenerator.build(now)
-    let prevInfo = view.dateProfileGenerator.buildPrev(view.dateProfile)
-    let nextInfo = view.dateProfileGenerator.buildNext(view.dateProfile)
+    let prevInfo = view.dateProfileGenerator.buildPrev(dateProfile)
+    let nextInfo = view.dateProfileGenerator.buildNext(dateProfile)
 
     this.toolbarsManager.proxyCall(
       (todayInfo.isValid && !dateProfile.currentUnzonedRange.containsDate(now)) ?
@@ -863,7 +843,6 @@ export default class Calendar {
 
 
   initDateEnv() {
-
     // not really date-env
     this.defaultAllDayEventDuration = createDuration(this.opt('defaultAllDayEventDuration'))
     this.defaultTimedEventDuration = createDuration(this.opt('defaultTimedEventDuration'))

+ 8 - 149
src/View.ts

@@ -2,9 +2,8 @@ import { assignTo } from './util/object'
 import { elementClosest } from './util/dom-manip'
 import { isPrimaryMouseButton } from './util/dom-event'
 import { parseFieldSpecs } from './util/misc'
-import RenderQueue from './common/RenderQueue'
 import Calendar from './Calendar'
-import DateProfileGenerator, { DateProfile } from './DateProfileGenerator'
+import { default as DateProfileGenerator, DateProfile } from './DateProfileGenerator'
 import InteractiveDateComponent from './component/InteractiveDateComponent'
 import GlobalEmitter from './common/GlobalEmitter'
 import UnzonedRange from './models/UnzonedRange'
@@ -27,10 +26,7 @@ export default abstract class View extends InteractiveDateComponent {
   calendar: Calendar // owner Calendar object
   viewSpec: any
   options: any // hash containing all options. already merged with view-specific-options
-  dateProfile: DateProfile
 
-  renderQueue: RenderQueue
-  batchRenderDepth: number = 0
   queuedScroll: object
 
   isSelected: boolean = false // boolean whether a range of time is user-selected or not
@@ -74,7 +70,6 @@ export default abstract class View extends InteractiveDateComponent {
     // .name is deprecated
     this.name = this.type
 
-    this.initRenderQueue()
     this.initHiddenDays()
     this.dateProfileGenerator = new this.dateProfileGeneratorClass(this)
     this.bindBaseRenderHandlers()
@@ -97,57 +92,8 @@ export default abstract class View extends InteractiveDateComponent {
   ------------------------------------------------------------------------------------------------------------------*/
 
 
-  initRenderQueue() {
-    this.renderQueue = new RenderQueue(this.opt('eventRenderWait'))
-
-    this.renderQueue.on('start', this.onRenderQueueStart.bind(this))
-    this.renderQueue.on('stop', this.onRenderQueueStop.bind(this))
-
-    this.on('before:change', this.startBatchRender)
-    this.on('change', this.stopBatchRender)
-  }
-
-
-  onRenderQueueStart() {
-    this.calendar.freezeContentHeight()
-    this.addScroll(this.queryScroll())
-  }
-
-
-  onRenderQueueStop() {
-    if (this.calendar.updateViewSize()) { // success?
-      this.popScroll()
-    }
-    this.calendar.thawContentHeight()
-  }
-
-
-  startBatchRender() {
-    if (!(this.batchRenderDepth++)) {
-      this.renderQueue.pause()
-    }
-  }
-
-
-  stopBatchRender() {
-    if (!(--this.batchRenderDepth)) {
-      this.renderQueue.resume()
-    }
-  }
-
-
-  requestRender(func) {
-    this.renderQueue.queue(func)
-  }
-
-
   // given func will auto-bind to `this`
   whenSizeUpdated(func) {
-    if (this.renderQueue.isRunning) {
-      this.renderQueue.one('stop', func.bind(this))
-    } else {
-      func.call(this)
-    }
   }
 
 
@@ -215,7 +161,7 @@ export default abstract class View extends InteractiveDateComponent {
   // -----------------------------------------------------------------------------------------------------------------
 
 
-  computeNewDateProfile(date: DateMarker) {
+  computeNewDateProfile(date: DateMarker): DateProfile {
     let currentDateProfile = this.dateProfile
     let newDateProfile = this.dateProfileGenerator.build(date, undefined, true) // forceToValid=true
 
@@ -228,7 +174,7 @@ export default abstract class View extends InteractiveDateComponent {
   }
 
 
-  setDateProfile(dateProfile) {
+  updateMiscDateProps(dateProfile) {
     let dateEnv = this.getDateEnv()
 
     this.title = this.computeTitle(dateProfile)
@@ -237,54 +183,27 @@ export default abstract class View extends InteractiveDateComponent {
     this.end = dateEnv.toDate(dateProfile.activeUnzonedRange.end)
     this.intervalStart = dateEnv.toDate(dateProfile.currentUnzonedRange.start)
     this.intervalEnd = dateEnv.toDate(dateProfile.currentUnzonedRange.end)
-
-    this.dateProfile = dateProfile
-    this.set('dateProfile', dateProfile) // for rendering watchers
   }
 
 
-  // Date High-level Rendering
+  // Date Rendering
   // -----------------------------------------------------------------------------------------------------------------
 
 
-  requestDateRender() {
-    this.requestRender(() => {
-      this.executeDateRender()
-    })
-  }
-
-
-  requestDateUnrender() {
-    this.requestRender(() => {
-      this.executeDateUnrender()
-    })
-  }
-
-
   // if dateProfile not specified, uses current
-  executeDateRender() {
-    super.executeDateRender()
-
-    if (this['render']) {
-      this['render']() // TODO: deprecate
-    }
-
+  renderDates() {
+    super.renderDates()
     this.trigger('datesRendered')
     this.addScroll({ isDateInit: true })
     this.startNowIndicator() // shouldn't render yet because updateSize will be called soon
   }
 
 
-  executeDateUnrender() {
+  unrenderDates() {
     this.unselect()
     this.stopNowIndicator()
     this.trigger('before:datesUnrendered')
-
-    if (this['destroy']) {
-      this['destroy']() // TODO: deprecate
-    }
-
-    super.executeDateUnrender()
+    super.unrenderDates()
   }
 
 
@@ -325,45 +244,6 @@ export default abstract class View extends InteractiveDateComponent {
   }
 
 
-  // Event High-level Rendering
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  requestRenderEvents(eventStore) {
-    this.requestRender(() => {
-      this.renderEvents(eventStore)
-      this.whenSizeUpdated(
-        this.triggerAfterEventsRendered
-      )
-    })
-  }
-
-
-  requestUnrenderEvents() {
-    this.requestRender(() => {
-      this.triggerBeforeEventsDestroyed()
-      this.unrenderEvents()
-    })
-  }
-
-
-  // Business Hour High-level Rendering
-  // -----------------------------------------------------------------------------------------------------------------
-
-
-  requestBusinessHoursRender() {
-    this.requestRender(() => {
-      this.renderBusinessHours(this.opt('businessHours'))
-    })
-  }
-
-  requestBusinessHoursUnrender() {
-    this.requestRender(() => {
-      this.unrenderBusinessHours()
-    })
-  }
-
-
   // Misc view rendering utils
   // -----------------------------------------------------------------------------------------------------------------
 
@@ -821,24 +701,3 @@ export default abstract class View extends InteractiveDateComponent {
 
 View.prototype.usesMinMaxTime = false
 View.prototype.dateProfileGeneratorClass = DateProfileGenerator
-
-
-View.watch('displayingDates', [ 'isInDom', 'dateProfile' ], function(deps) {
-  this.requestDateRender()
-}, function() {
-  this.requestDateUnrender()
-})
-
-
-View.watch('displayingBusinessHours', [ 'displayingDates' ], function() {
-  this.requestBusinessHoursRender()
-}, function() {
-  this.requestBusinessHoursUnrender()
-})
-
-
-View.watch('displayingEvents', [ 'displayingDates', 'eventStore' ], function(deps) {
-  this.requestRenderEvents(deps.eventStore)
-}, function() {
-  this.requestUnrenderEvents()
-})

+ 77 - 61
src/agenda/AgendaView.ts

@@ -14,7 +14,10 @@ import DayGrid from '../basic/DayGrid'
 import { createDuration } from '../datelib/duration'
 import { createFormatter } from '../datelib/formatting'
 import { EventStore } from '../reducers/event-store'
-import { Selection } from '../reducers/selection'
+import { DateComponentRenderState } from '../reducers/main'
+import { DragState } from '../reducers/drag'
+import { EventResizeState } from '../reducers/event-resize'
+import reselector from '../util/reselector'
 
 const AGENDA_ALL_DAY_EVENT_LIMIT = 5
 const WEEK_HEADER_FORMAT = createFormatter({ week: 'short' })
@@ -41,6 +44,9 @@ export default class AgendaView extends View {
   axisWidth: any // the width of the time axis running down the side
   usesMinMaxTime: boolean = true // indicates that minTime/maxTime affects rendering
 
+  splitEventStore: any
+  splitUiState: any
+
 
   constructor(calendar, viewSpec) {
     super(calendar, viewSpec)
@@ -57,6 +63,9 @@ export default class AgendaView extends View {
       overflowX: 'hidden',
       overflowY: 'auto'
     })
+
+    this.splitEventStore = reselector(splitEventStore)
+    this.splitUiState = reselector(splitUiState)
   }
 
 
@@ -158,6 +167,47 @@ export default class AgendaView extends View {
   }
 
 
+  /* Render Delegation
+  ------------------------------------------------------------------------------------------------------------------*/
+
+
+  renderChildren(renderState: DateComponentRenderState, forces: any) {
+    let allDaySeletion = null
+    let timedSelection = null
+    let eventStoreGroups = this.splitEventStore(renderState.eventStore)
+    let dragStateGroups = this.splitUiState(renderState.dragState)
+    let eventResizeStateGroups = this.splitUiState(renderState.eventResizeState)
+
+    if (renderState.selection) {
+      if (renderState.selection.isAllDay) {
+        allDaySeletion = renderState.selection
+      } else {
+        timedSelection = renderState.selection
+      }
+    }
+
+    this.timeGrid.render({
+      dateProfile: renderState.dateProfile,
+      eventStore: eventStoreGroups.timed,
+      selection: timedSelection,
+      dragState: dragStateGroups.timed,
+      eventResizeState: eventResizeStateGroups.timed,
+      businessHoursDef: renderState.businessHoursDef
+    }, forces)
+
+    if (this.dayGrid) {
+      this.dayGrid.render({
+        dateProfile: renderState.dateProfile,
+        eventStore: eventStoreGroups.allDay,
+        selection: allDaySeletion,
+        dragState: dragStateGroups.allDay,
+        eventResizeState: eventResizeStateGroups.allDay,
+        businessHoursDef: renderState.businessHoursDef
+      }, forces)
+    }
+  }
+
+
   /* Now Indicator
   ------------------------------------------------------------------------------------------------------------------*/
 
@@ -284,64 +334,6 @@ export default class AgendaView extends View {
     }
   }
 
-
-  /* Event Rendering
-  ------------------------------------------------------------------------------------------------------------------*/
-
-  renderEvents(eventStore: EventStore) {
-    let groups = divideEventStoreByAllDay(eventStore)
-
-    this.timeGrid.renderEvents(groups.timed)
-
-    if (this.dayGrid) {
-      this.dayGrid.renderEvents(groups.allDay)
-    }
-  }
-
-
-  /* Dragging/Resizing Routing
-  ------------------------------------------------------------------------------------------------------------------*/
-
-
-  // A returned value of `true` signals that a mock "helper" event has been rendered.
-  renderDrag(eventStore: EventStore, origSeg, isTouch) {
-    let groups = divideEventStoreByAllDay(eventStore)
-    let renderedHelper = false
-
-    renderedHelper = this.timeGrid.renderDrag(groups.timed, origSeg, isTouch)
-
-    if (this.dayGrid) {
-      renderedHelper = this.dayGrid.renderDrag(groups.allDay, origSeg, isTouch) || renderedHelper
-    }
-
-    return renderedHelper
-  }
-
-
-  renderEventResize(eventStore: EventStore, origSeg, isTouch) {
-    let groups = divideEventStoreByAllDay(eventStore)
-
-    this.timeGrid.renderEventResize(groups.timed, origSeg, isTouch)
-
-    if (this.dayGrid) {
-      this.dayGrid.renderEventResize(groups.allDay, origSeg, isTouch)
-    }
-  }
-
-
-  /* Selection
-  ------------------------------------------------------------------------------------------------------------------*/
-
-
-  // Renders a visual indication of a selection
-  renderSelection(selection: Selection) {
-    if (!selection.isAllDay) {
-      this.timeGrid.renderSelection(selection)
-    } else if (this.dayGrid) {
-      this.dayGrid.renderSelection(selection)
-    }
-  }
-
 }
 
 
@@ -357,7 +349,7 @@ agendaTimeGridMethods = {
     let view = this.view
     let calendar = view.calendar
     let dateEnv = calendar.dateEnv
-    let weekStart = this.getDateProfile().renderUnzonedRange.start
+    let weekStart = this.dateProfile.renderUnzonedRange.start
     let weekText
 
     if (this.opt('weekNumbers')) {
@@ -422,7 +414,7 @@ agendaDayGridMethods = {
 }
 
 
-function divideEventStoreByAllDay(eventStore: EventStore) {
+function splitEventStore(eventStore: EventStore) {
   let allDay: EventStore = { defs: {}, instances: {} }
   let timed: EventStore = { defs: {}, instances: {} }
 
@@ -449,3 +441,27 @@ function divideEventStoreByAllDay(eventStore: EventStore) {
 
   return { allDay, timed }
 }
+
+
+function splitUiState(state: DragState | EventResizeState) {
+  let allDay = null
+  let timed = null
+
+  if (state) {
+    let eventStoreGroups = splitEventStore(state.eventStore)
+
+    allDay = {
+      eventStore: eventStoreGroups.allDay,
+      origSeg: state.origSeg,
+      isTouch: state.isTouch
+    }
+
+    timed = {
+      eventStore: eventStoreGroups.timed,
+      origSeg: state.origSeg,
+      isTouch: state.isTouch
+    }
+  }
+
+  return { allDay, timed }
+}

+ 3 - 3
src/agenda/TimeGrid.ts

@@ -247,7 +247,7 @@ export default class TimeGrid extends InteractiveDateComponent {
     let dateEnv = this.getDateEnv()
     let theme = this.getTheme()
     let isRTL = this.isRTL
-    let dateProfile = this.getDateProfile()
+    let dateProfile = this.dateProfile
     let html = ''
     let dayStart = startOfDay(dateProfile.renderUnzonedRange.start)
     let slotTime = dateProfile.minTime
@@ -289,7 +289,7 @@ export default class TimeGrid extends InteractiveDateComponent {
 
 
   renderColumns() {
-    let dateProfile = this.getDateProfile()
+    let dateProfile = this.dateProfile
     let theme = this.getTheme()
     let dateEnv = this.getDateEnv()
 
@@ -504,7 +504,7 @@ export default class TimeGrid extends InteractiveDateComponent {
   // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
   computeTimeTop(timeMs: number) {
     let len = this.slatEls.length
-    let dateProfile = this.getDateProfile()
+    let dateProfile = this.dateProfile
     let slatCoverage = (timeMs - asRoughMs(dateProfile.minTime)) / asRoughMs(this.slotDuration) // floating-point value of # of slots covered
     let slatIndex
     let slatRemainder

+ 3 - 3
src/basic/BasicView.ts

@@ -70,12 +70,12 @@ export default class BasicView extends View {
   }
 
 
-  executeDateRender() {
+  renderDates() {
     this.dayGrid.breakOnWeeks = /year|month|week/.test(
-      this.getDateProfile().currentRangeUnit
+      this.dateProfile.currentRangeUnit
     )
 
-    super.executeDateRender()
+    super.renderDates()
   }
 
 

+ 1 - 1
src/basic/DayGrid.ts

@@ -238,7 +238,7 @@ export default class DayGrid extends InteractiveDateComponent {
     let view = this.view
     let dateEnv = this.getDateEnv()
     let html = ''
-    let isDateValid = this.getDateProfile().activeUnzonedRange.containsDate(date) // TODO: called too frequently. cache somehow.
+    let isDateValid = this.dateProfile.activeUnzonedRange.containsDate(date) // TODO: called too frequently. cache somehow.
     let isDayNumberVisible = this.getIsDayNumbersVisible() && isDateValid
     let classes
     let weekCalcFirstDow

+ 0 - 21
src/component/Component.ts

@@ -9,14 +9,10 @@ export default class Component extends Model {
   setElement(el: HTMLElement) {
     this.el = el
     this.bindGlobalHandlers()
-    this.renderSkeleton()
-    this.set('isInDom', true)
   }
 
 
   removeElement() {
-    this.unset('isInDom')
-    this.unrenderSkeleton()
     this.unbindGlobalHandlers()
 
     removeElement(this.el)
@@ -35,21 +31,4 @@ export default class Component extends Model {
     // subclasses can override
   }
 
-
-  /*
-  NOTE: Can't have a `render` method. Read the deprecation notice in View::executeDateRender
-  */
-
-
-  // Renders the basic structure of the view before any content is rendered
-  renderSkeleton() {
-    // subclasses should implement
-  }
-
-
-  // Unrenders the basic structure of the view
-  unrenderSkeleton() {
-    // subclasses should implement
-  }
-
 }

+ 153 - 47
src/component/DateComponent.ts

@@ -13,6 +13,10 @@ import { EventStore } from '../reducers/event-store'
 import { BusinessHourDef, buildBusinessHourEventStore } from '../reducers/business-hours'
 import { DateEnv } from '../datelib/env'
 import Theme from '../theme/Theme'
+import { DragState } from '../reducers/drag'
+import { EventResizeState } from '../reducers/event-resize'
+import { DateComponentRenderState } from '../reducers/main'
+import { assignTo } from '../util/object'
 
 
 export default abstract class DateComponent extends Component {
@@ -37,7 +41,20 @@ export default abstract class DateComponent extends Component {
 
   hasAllDayBusinessHours: boolean = false // TODO: unify with largeUnit and isTimeScale?
 
+  isSkeletonRendered: boolean = false
   isDatesRendered: boolean = false
+  isBusinessHoursRendered: boolean = false
+  isSelectionRendered: boolean = false
+  isEventsRendered: boolean = false
+  isDragRendered: boolean = false
+  isEventResizeRendered: boolean = false
+  isSizeDirty: boolean = false
+  dateProfile: DateProfile
+  businessHoursDef: BusinessHourDef
+  selection: Selection
+  eventStore: EventStore
+  dragState: DragState
+  eventResizeState: EventResizeState
 
 
   constructor(_view, _options?) {
@@ -125,24 +142,141 @@ export default abstract class DateComponent extends Component {
   }
 
 
-  // Date
+  // Root Rendering
+  // -----------------------------------------------------------------------------------------------------------------
+
+
+  render(renderState: DateComponentRenderState, forces: any) {
+    if (!forces) {
+      forces = {}
+    }
+
+    let isSkeletonDirty = forces === true ||
+      !this.isSkeletonRendered
+    let isDatesDirty = forces === true ||
+      isSkeletonDirty ||
+      renderState.dateProfile !== this.dateProfile
+    let isBusinessHoursDirty = forces === true ||
+      isDatesDirty ||
+      renderState.businessHoursDef !== this.businessHoursDef
+    let isSelectionDirty = forces === true ||
+      isDatesDirty ||
+      renderState.selection !== this.selection
+    let isEventsDirty = forces === true || forces.events ||
+      isDatesDirty ||
+      renderState.eventStore !== this.eventStore
+    let isDragDirty = forces === true ||
+      isDatesDirty ||
+      renderState.dragState !== this.dragState
+    let isEventResizeDirty = forces === true ||
+      isDatesDirty ||
+      renderState.eventResizeState !== this.eventResizeState
+
+    // unrendering
+    if (isEventResizeDirty && this.isEventResizeRendered) {
+      this.unrenderEventResize()
+      this.isEventResizeRendered = false
+      this.isSizeDirty = false
+    }
+    if (isDragDirty && this.isDatesRendered) {
+      this.unrenderDrag()
+      this.isDragRendered = false
+      this.isSizeDirty = true
+    }
+    if (isEventsDirty && this.isEventsRendered) {
+      this.unrenderEvents()
+      this.isEventsRendered = false
+      this.isSizeDirty = true
+    }
+    if (isSelectionDirty && this.isSkeletonRendered) {
+      this.unrenderSelection()
+      this.isSkeletonRendered = false
+      this.isSizeDirty = true
+    }
+    if (isBusinessHoursDirty && this.isBusinessHoursRendered) {
+      this.unrenderBusinessHours()
+      this.isBusinessHoursRendered = false
+      this.isSizeDirty = true
+    }
+    if (isDatesDirty && this.isDatesRendered) {
+      this.unrenderDates()
+      this.isDatesRendered = false
+      this.isSizeDirty = true
+    }
+    if (isSkeletonDirty && this.isSkeletonRendered) {
+      this.unrenderSkeleton()
+      this.isSkeletonRendered = false
+      this.isSizeDirty = true
+    }
+
+    assignTo(this, renderState)
+
+    // rendering
+    if (isSkeletonDirty) {
+      this.renderSkeleton()
+      this.isSkeletonRendered = true
+      this.isSizeDirty = true
+    }
+    if (isDatesDirty && renderState.dateProfile) {
+      this.renderDates() // pass in dateProfile too?
+      this.isDatesRendered = true
+      this.isSizeDirty = true
+    }
+    if (isBusinessHoursDirty && renderState.businessHoursDef && this.isDatesRendered) {
+      this.renderBusinessHours(renderState.businessHoursDef)
+      this.isBusinessHoursRendered = true
+      this.isSizeDirty = true
+    }
+    if (isSelectionDirty && renderState.selection && this.isDatesRendered) {
+      this.renderSelection(renderState.selection)
+      this.isSelectionRendered = true
+      this.isSizeDirty = true
+    }
+    if (isEventsDirty && renderState.eventStore && this.isDatesRendered) {
+      this.renderEvents(renderState.eventStore)
+      this.isEventsRendered = true
+      this.isSizeDirty = true
+    }
+    if (isDragDirty && renderState.dragState && this.isDatesRendered) {
+      let { dragState } = renderState
+      this.renderDrag(dragState.eventStore, dragState.origSeg, dragState.isTouch)
+      this.isDragRendered = true
+      this.isSizeDirty = true
+    }
+    if (isEventResizeDirty && renderState.eventResizeState && this.isDatesRendered) {
+      let { eventResizeState } = renderState
+      this.renderEventResize(eventResizeState.eventStore, eventResizeState.origSeg, eventResizeState.isTouch)
+      this.isEventResizeRendered = true
+      this.isSizeDirty = true
+    }
+
+    this.renderChildren(renderState, forces)
+  }
+
+
+  renderChildren(renderState: DateComponentRenderState, forces: any) {
+    this.callChildren('render', arguments)
+  }
+
+
+  // Skeleton
   // -----------------------------------------------------------------------------------------------------------------
 
 
-  executeDateRender() {
-    this.renderDates()
-    this.isDatesRendered = true
-    this.callChildren('executeDateRender', arguments)
+  renderSkeleton() {
+    // subclasses should implement
   }
 
 
-  executeDateUnrender() { // wrapper
-    this.callChildren('executeDateUnrender', arguments)
-    this.unrenderDates()
-    this.isDatesRendered = false
+  unrenderSkeleton() {
+    // subclasses should implement
   }
 
 
+  // Date
+  // -----------------------------------------------------------------------------------------------------------------
+
+
   // date-cell content only
   renderDates() {
     // subclasses should implement
@@ -189,21 +323,17 @@ export default abstract class DateComponent extends Component {
           buildBusinessHourEventStore(
             businessHoursDef,
             this.hasAllDayBusinessHours,
-            this.getDateProfile().activeUnzonedRange,
+            this.dateProfile.activeUnzonedRange,
             this.getCalendar()
           )
         )
       )
     }
-
-    this.callChildren('renderBusinessHours', arguments)
   }
 
 
   // Unrenders previously-rendered business-hours
   unrenderBusinessHours() {
-    this.callChildren('unrenderBusinessHours', arguments)
-
     if (this.businessHourRenderer) {
       this.businessHourRenderer.unrender()
     }
@@ -235,25 +365,18 @@ export default abstract class DateComponent extends Component {
 
 
   renderEvents(eventStore: EventStore) {
-
     if (this.eventRenderer) {
       this.eventRenderer.rangeUpdated() // poorly named now
       this.eventRenderer.renderSegs(
         this.eventStoreToSegs(eventStore)
       )
     }
-
-    this.callChildren('renderEvents', arguments)
   }
 
 
   unrenderEvents() {
-    this.callChildren('unrenderEvents', arguments)
-
     if (this.eventRenderer) {
       this.eventRenderer.unrender()
-    } else if (this['destroyEvents']) { // legacy
-      this['destroyEvents']()
     }
   }
 
@@ -379,21 +502,13 @@ export default abstract class DateComponent extends Component {
   // If an external-element, seg will be `null`.
   // Must return elements used for any mock events.
   renderDrag(eventStore: EventStore, origSeg?, isTouch = false) {
-    let renderedHelper = false
-
-    this.iterChildren(function(child) {
-      if (child.renderDrag(eventStore, origSeg, isTouch)) {
-        renderedHelper = true
-      }
-    })
-
-    return renderedHelper
+    // subclasses can implement
   }
 
 
   // Unrenders a visual indication of an event or external-element being dragged.
   unrenderDrag() {
-    this.callChildren('unrenderDrag', arguments)
+    // subclasses can implement
   }
 
 
@@ -422,14 +537,14 @@ export default abstract class DateComponent extends Component {
 
 
   // Renders a visual indication of an event being resized.
-  renderEventResize(eventStore: EventStore, seg, isTouch) {
-    this.callChildren('renderEventResize', arguments)
+  renderEventResize(eventStore: EventStore, origSeg: any, isTouch: boolean) {
+    // subclasses can implement
   }
 
 
   // Unrenders a visual indication of an event being resized.
   unrenderEventResize() {
-    this.callChildren('unrenderEventResize', arguments)
+    // subclasses can implement
   }
 
 
@@ -440,16 +555,12 @@ export default abstract class DateComponent extends Component {
   // Renders a visual indication of the selection
   renderSelection(selection: Selection) {
     this.renderHighlightSegs(this.selectionToSegs(selection))
-
-    this.callChildren('renderSelection', arguments)
   }
 
 
   // Unrenders a visual indication of selection
   unrenderSelection() {
     this.unrenderHighlight()
-
-    this.callChildren('unrenderSelection', arguments)
   }
 
 
@@ -482,7 +593,7 @@ export default abstract class DateComponent extends Component {
 
 
   eventStoreToSegs(eventStore: EventStore): Seg[] {
-    let activeUnzonedRange = this.getDateProfile().activeUnzonedRange
+    let activeUnzonedRange = this.dateProfile.activeUnzonedRange
     let eventRenderRanges = sliceEventStore(eventStore, activeUnzonedRange)
     let allSegs: Seg[] = []
 
@@ -541,11 +652,6 @@ export default abstract class DateComponent extends Component {
   }
 
 
-  getDateProfile(): DateProfile {
-    return this.view.dateProfile
-  }
-
-
   getTheme(): Theme {
     return this.getCalendar().theme
   }
@@ -611,12 +717,12 @@ export default abstract class DateComponent extends Component {
     let todayStart: DateMarker
     let todayEnd: DateMarker
 
-    if (!this.getDateProfile().activeUnzonedRange.containsDate(date)) {
+    if (!this.dateProfile.activeUnzonedRange.containsDate(date)) {
       classes.push('fc-disabled-day') // TODO: jQuery UI theme?
     } else {
       classes.push('fc-' + DAY_IDS[date.getUTCDay()])
 
-      if (view.isDateInOtherMonth(date, this.getDateProfile())) { // TODO: use DateComponent subclass somehow
+      if (view.isDateInOtherMonth(date, this.dateProfile)) { // TODO: use DateComponent subclass somehow
         classes.push('fc-other-month')
       }
 
@@ -645,7 +751,7 @@ export default abstract class DateComponent extends Component {
   // Will return `0` if there's not a clean whole interval.
   currentRangeAs(unit) { // PLURAL :(
     let dateEnv = this.getDateEnv()
-    let range = this.getDateProfile().currentUnzonedRange
+    let range = this.dateProfile.currentUnzonedRange
     let res = null
 
     if (unit === 'years') {

+ 3 - 3
src/component/DayTableMixin.ts

@@ -40,7 +40,7 @@ export default class DayTableMixin extends Mixin implements DayTableInterface {
   updateDayTable() {
     let t = (this as any)
     let view = t.view
-    let dateProfile = t.getDateProfile()
+    let dateProfile = t.dateProfile
     let date: DateMarker = dateProfile.renderUnzonedRange.start
     let end: DateMarker = dateProfile.renderUnzonedRange.end
     let dayIndex = -1
@@ -319,7 +319,7 @@ export default class DayTableMixin extends Mixin implements DayTableInterface {
     let t = (this as any)
     let view = t.view
     let dateEnv = t.getDateEnv()
-    let dateProfile = t.getDateProfile()
+    let dateProfile = t.dateProfile
     let isDateValid = dateProfile.activeUnzonedRange.containsDate(date) // TODO: called too frequently. cache somehow.
     let classNames = [
       'fc-day-header',
@@ -410,7 +410,7 @@ export default class DayTableMixin extends Mixin implements DayTableInterface {
     let t = (this as any)
     let view = t.view
     let dateEnv = t.getDateEnv()
-    let dateProfile = t.getDateProfile()
+    let dateProfile = t.dateProfile
     let isDateValid = dateProfile.activeUnzonedRange.containsDate(date) // TODO: called too frequently. cache somehow.
     let classes = t.getDayClasses(date)
 

+ 1 - 1
src/list/ListView.ts

@@ -74,7 +74,7 @@ export default class ListView extends View {
 
 
   renderDates() {
-    let dateProfile = this.getDateProfile()
+    let dateProfile = this.dateProfile
     let dayStart = startOfDay(dateProfile.renderUnzonedRange.start)
     let viewEnd = dateProfile.renderUnzonedRange.end
     let dayDates = []

+ 7 - 0
src/reducers/drag.ts

@@ -0,0 +1,7 @@
+import { EventStore } from './event-store'
+
+export interface DragState {
+  eventStore: EventStore
+  origSeg: any
+  isTouch: boolean
+}

+ 7 - 0
src/reducers/event-resize.ts

@@ -0,0 +1,7 @@
+import { EventStore } from './event-store'
+
+export interface EventResizeState {
+  eventStore: EventStore
+  origSeg: any
+  isTouch: boolean
+}

+ 94 - 35
src/reducers/main.ts

@@ -4,60 +4,119 @@ import { EventSourceHash, reduceEventSourceHash } from './event-sources'
 import { EventStore, reduceEventStore } from './event-store'
 import { Selection } from './selection'
 import { BusinessHourDef } from './business-hours'
+import { DragState } from './drag'
+import { EventResizeState } from './event-resize'
+import { DateMarker } from '../datelib/marker'
 
-export interface CalendarState {
-  loadingLevel: number
+export interface DateComponentRenderState {
   dateProfile: DateProfile
-  eventSources: EventSourceHash
   eventStore: EventStore
-  selection: Selection | null,
-  dragState: {
-    eventStore: EventStore
-    origSeg: any
-    isTouch: boolean
-  } | null
-  eventResizeState: {
-    eventStore: EventStore
-    origSeg: any
-    isTouch: boolean
-  } | null
+  selection: Selection | null
+  dragState: DragState | null
+  eventResizeState: EventResizeState | null
   businessHoursDef: BusinessHourDef
 }
 
-export const INITIAL_STATE: CalendarState = {
-  loadingLevel: 0,
-  dateProfile: null,
-  eventSources: {},
-  eventStore: {
-    defs: {},
-    instances: {}
-  },
-  selection: null,
-  dragState: null,
-  eventResizeState: null,
-  businessHoursDef: false
+export interface CalendarState extends DateComponentRenderState {
+  loadingLevel: number
+  eventSources: EventSourceHash
+  currentDate: DateMarker
 }
 
 export function reduce(state: CalendarState, action: any, calendar: Calendar): CalendarState {
-  return {
+  let newState = {
     loadingLevel: reduceLoadingLevel(state.loadingLevel, action),
-    dateProfile: reduceDateProfile(state.dateProfile, action),
     eventSources: reduceEventSourceHash(state.eventSources, action, calendar),
     eventStore: reduceEventStore(state.eventStore, action, calendar),
+    dateProfile: state.dateProfile,
+    currentDate: state.currentDate,
     selection: state.selection,
     dragState: state.dragState,
     eventResizeState: state.eventResizeState,
-    businessHoursDef: state.businessHoursDef
+    businessHoursDef: state.businessHoursDef,
   }
-}
 
-function reduceDateProfile(currentDateProfile, action: any) {
-  switch (action.type) {
+  switch(action.type) {
+
+    case 'SET_VIEW_TYPE':
+      if (!calendar.view || calendar.view.type !== action.viewType) {
+        calendar.installNewView(action.viewType)
+        calendar.dispatch({
+          type: 'SET_DATE_PROFILE',
+          dateProfile: calendar.view.computeNewDateProfile(
+            action.dateMarker || state.currentDate
+          ) || calendar.view.dateProfile // ummmm.... to get same reference
+        })
+      }
+      break
+
     case 'SET_DATE_PROFILE':
-      return action.dateProfile
-    default:
-      return currentDateProfile
+      if (action.dateProfile.isValid) {
+        newState.dateProfile = action.dateProfile
+        newState.currentDate = action.dateProfile.date // might have been constrained by view dates
+
+        calendar.view.updateMiscDateProps(action.dateProfile)
+      }
+      break
+
+    case 'NAVIGATE_PREV':
+      calendar.dispatch({
+        type: 'SET_DATE_PROFILE',
+        dateProfile: calendar.view.dateProfileGenerator.buildPrev(newState.dateProfile)
+      })
+      break
+
+    case 'NAVIGATE_NEXT':
+      calendar.dispatch({
+        type: 'SET_DATE_PROFILE',
+        dateProfile: calendar.view.dateProfileGenerator.buildNext(newState.dateProfile)
+      })
+      break
+
+    case 'NAVIGATE_TODAY':
+      calendar.dispatch({
+        type: 'SET_DATE_PROFILE',
+        dateProfile: calendar.view.computeNewDateProfile(calendar.getNow())
+      })
+      break
+
+    case 'NAVIGATE_PREV_YEAR':
+      calendar.dispatch({
+        type: 'SET_DATE_PROFILE',
+        dateProfile: calendar.view.computeNewDateProfile(
+          calendar.dateEnv.addYears(newState.currentDate, -1)
+        )
+      })
+      break
+
+    case 'NAVIGATE_NEXT_YEAR':
+      calendar.dispatch({
+        type: 'SET_DATE_PROFILE',
+        dateProfile: calendar.view.computeNewDateProfile(
+          calendar.dateEnv.addYears(newState.currentDate, 1)
+        )
+      })
+      break
+
+    case 'NAVIGATE_DATE':
+      calendar.dispatch({
+        type: 'SET_DATE_PROFILE',
+        dateProfile: calendar.view.computeNewDateProfile(action.dateMarker)
+      })
+      break
+
+    case 'NAVIGATE_DELTA':
+      calendar.dispatch({
+        type: 'SET_DATE_PROFILE',
+        dateProfile: calendar.view.computeNewDateProfile(
+          calendar.dateEnv.add(newState.currentDate, action.delta)
+        )
+      })
+      break
+
   }
+
+  return newState
 }
 
 function reduceLoadingLevel(level: number, action): number {

+ 117 - 0
src/reducers/options.ts

@@ -0,0 +1,117 @@
+
+const OPTIONS = {
+  allDaySlot
+  columnHeader
+  eventLimit
+  scrollTime
+  weekNumbers
+  slotDuration
+  snapDuration
+  slotLabelFormat
+  slotLabelInterval
+  agendaEventMinHeight
+  selectHelper
+  slotEventOverlap
+  weekNumbers
+  weekNumbersWithinDays
+  columnHeader
+  eventLimit
+  weekLabel
+  eventLimitClick
+  popoverViewportConstrain
+  dayPopoverFormat
+  eventLimitText
+  nextDayThreshold
+  isRTL
+  navLinks
+  allDayHtml
+  allDayText
+  columnHeaderFormat
+  columnFormat
+  columnHeaderHtml
+  columnHeaderText
+  eventResizableFromStart
+  eventTimeFormat
+  timeFormat
+  displayEventTime
+  displayEventEnd
+  eventBackgroundColor
+  eventColor
+  eventBorderColor
+  eventColor
+  eventTextColor
+  dragOpacity
+  noEventsMessage
+  listDayFormat
+  listDayAltFormat // how will work with plugins???
+  eventOrder
+  eventRenderWait
+  titleFormat
+  titleRangeSeparator
+  businessHours // ???
+  nowIndicator
+  unselectAuto
+  unselectCancel
+  hiddenDays
+  weekends
+
+  theme
+  themeSystem
+  isRTL
+  locale
+  timezone
+  firstDay
+  weekNumberCalculation
+  weekLabel
+
+
+  // dateprofile
+  fixedWeekCount
+  defaultDate
+  minTime
+  maxTime
+  dayCount
+  dateAlignment
+  dateIncrement
+
+  // event source
+  eventSources
+  events
+  lazyFetching
+  allDayDefault
+  forceEventDuration
+  defaultAllDayEventDuration
+  defaultTimedEventDuration // for other things too?
+  startParam
+  endParam
+  timezoneParam
+
+  navLink*
+  defaultView
+  handleWindowResize
+  windowResizeDelay
+  header
+  footer
+
+  now
+
+  // height
+  contentHeight
+  height
+  aspectRatio
+
+  // publiclyTrigger
+  dayRender
+  eventLimitClick
+  loading
+  windowResize
+  eventAfterAllRender
+  eventAfterRender
+  eventDestroy
+  eventRender
+  viewRender
+  viewDestroy
+  select
+  unselect
+
+}

+ 16 - 0
src/util/reselector.ts

@@ -0,0 +1,16 @@
+import { isArraysEqual } from './array'
+
+
+export default function(workerFunc) {
+  let prevArgs
+  let prevResult
+
+  return function() {
+    if (!prevArgs || !isArraysEqual(prevArgs, arguments)) {
+      prevArgs = arguments
+      prevResult = workerFunc.apply(this, arguments)
+    }
+
+    return prevResult
+  }
+}