소스 검색

new view-framework stuff. kill DomLocation in props

Adam Shaw 6 년 전
부모
커밋
f2db856bc1

+ 1 - 1
packages-premium

@@ -1 +1 @@
-Subproject commit 4057ba9bf13bb849a42bdb02e1826cabb951064a
+Subproject commit 6d3c485d27de0ba4b87a5facfaebcf228670d14e

+ 3 - 2
packages/core/src/Calendar.ts

@@ -37,7 +37,8 @@ import StandardTheme from './theme/StandardTheme'
 import { CmdFormatterFunc } from './datelib/formatting-cmd'
 import { CmdFormatterFunc } from './datelib/formatting-cmd'
 import { NamedTimeZoneImplClass } from './datelib/timezone'
 import { NamedTimeZoneImplClass } from './datelib/timezone'
 import { computeContextProps } from './component/ComponentContext'
 import { computeContextProps } from './component/ComponentContext'
-import { TaskRunner, renderer, DelayedRunner } from './view-framework'
+import { renderer } from './view-framework'
+import { TaskRunner, DelayedRunner } from './util/runner'
 import ViewApi from './ViewApi'
 import ViewApi from './ViewApi'
 
 
 export interface DateClickApi extends DatePointApi {
 export interface DateClickApi extends DatePointApi {
@@ -103,7 +104,7 @@ export default class Calendar {
   interactionsStore: { [componentUid: string]: Interaction[] } = {}
   interactionsStore: { [componentUid: string]: Interaction[] } = {}
   removeNavLinkListener: any
   removeNavLinkListener: any
 
 
-  windowResizeProxy: any
+  windowResizeProxy: any // TODO: use DelayedRunner for this instead of debounce!
   isHandlingWindowResize: boolean
   isHandlingWindowResize: boolean
 
 
   state: CalendarState
   state: CalendarState

+ 8 - 8
packages/core/src/CalendarComponent.ts

@@ -1,5 +1,5 @@
 import ComponentContext, { computeContextProps } from './component/ComponentContext'
 import ComponentContext, { computeContextProps } from './component/ComponentContext'
-import { Component, renderer, DomLocation } from './view-framework'
+import { Component, renderer } from './view-framework'
 import { ViewSpec } from './structs/view-spec'
 import { ViewSpec } from './structs/view-spec'
 import View, { ViewProps } from './View'
 import View, { ViewProps } from './View'
 import Toolbar from './Toolbar'
 import Toolbar from './Toolbar'
@@ -17,7 +17,7 @@ import { __assign } from 'tslib'
 import { listRenderer } from './view-framework'
 import { listRenderer } from './view-framework'
 
 
 
 
-export type CalendarComponentProps = DomLocation & CalendarState & {
+export interface CalendarComponentProps extends CalendarState {
   viewSpec: ViewSpec
   viewSpec: ViewSpec
   dateProfileGenerator: DateProfileGenerator // for the current view
   dateProfileGenerator: DateProfileGenerator // for the current view
   eventUiBases: EventUiHash
   eventUiBases: EventUiHash
@@ -74,7 +74,7 @@ export default class CalendarComponent extends Component<CalendarComponentProps,
       this.renderHeader(false)
       this.renderHeader(false)
     }
     }
 
 
-    let viewContainerEl = this.renderViewContainer(true)
+    let viewContainerEl = this.renderViewContainer({})
     this.renderView(props, viewContainerEl, context)
     this.renderView(props, viewContainerEl, context)
     innerEls.push(viewContainerEl)
     innerEls.push(viewContainerEl)
 
 
@@ -112,7 +112,7 @@ export default class CalendarComponent extends Component<CalendarComponentProps,
 
 
 
 
   _setClassNames(props: {}, context: ComponentContext) {
   _setClassNames(props: {}, context: ComponentContext) {
-    let classList = this.props.parentEl.classList
+    let classList = this.location.parentEl.classList
     let classNames: string[] = [
     let classNames: string[] = [
       'fc',
       'fc',
       'fc-' + context.options.dir,
       'fc-' + context.options.dir,
@@ -128,7 +128,7 @@ export default class CalendarComponent extends Component<CalendarComponentProps,
 
 
 
 
   _unsetClassNames(classNames: string[]) {
   _unsetClassNames(classNames: string[]) {
-    let classList = this.props.parentEl.classList
+    let classList = this.location.parentEl.classList
 
 
     for (let className of classNames) {
     for (let className of classNames) {
       classList.remove(className)
       classList.remove(className)
@@ -244,7 +244,7 @@ export default class CalendarComponent extends Component<CalendarComponentProps,
     } else if (typeof heightInput === 'function') { // exists and is a function
     } else if (typeof heightInput === 'function') { // exists and is a function
       this.viewHeight = heightInput() - this.queryToolbarsHeight()
       this.viewHeight = heightInput() - this.queryToolbarsHeight()
     } else if (heightInput === 'parent') { // set to height of parent element
     } else if (heightInput === 'parent') { // set to height of parent element
-      let parentEl = this.props.parentEl.parentNode as HTMLElement
+      let parentEl = this.location.parentEl.parentNode as HTMLElement
       this.viewHeight = parentEl.getBoundingClientRect().height - this.queryToolbarsHeight()
       this.viewHeight = parentEl.getBoundingClientRect().height - this.queryToolbarsHeight()
     } else {
     } else {
       this.viewHeight = Math.round(
       this.viewHeight = Math.round(
@@ -275,7 +275,7 @@ export default class CalendarComponent extends Component<CalendarComponentProps,
 
 
 
 
   freezeHeight() {
   freezeHeight() {
-    let rootEl = this.props.parentEl
+    let rootEl = this.location.parentEl
 
 
     applyStyle(rootEl, {
     applyStyle(rootEl, {
       height: rootEl.getBoundingClientRect().height,
       height: rootEl.getBoundingClientRect().height,
@@ -285,7 +285,7 @@ export default class CalendarComponent extends Component<CalendarComponentProps,
 
 
 
 
   thawHeight() {
   thawHeight() {
-    let rootEl = this.props.parentEl
+    let rootEl = this.location.parentEl
 
 
     applyStyle(rootEl, {
     applyStyle(rootEl, {
       height: '',
       height: '',

+ 2 - 2
packages/core/src/Toolbar.ts

@@ -1,7 +1,7 @@
 import { htmlEscape } from './util/html'
 import { htmlEscape } from './util/html'
 import { htmlToElement, appendToElement, findElements, createElement } from './util/dom-manip'
 import { htmlToElement, appendToElement, findElements, createElement } from './util/dom-manip'
 import ComponentContext from './component/ComponentContext'
 import ComponentContext from './component/ComponentContext'
-import { Component, renderer, DomLocation } from './view-framework'
+import { Component, renderer } from './view-framework'
 import { ViewSpec } from './structs/view-spec'
 import { ViewSpec } from './structs/view-spec'
 import Calendar from './Calendar'
 import Calendar from './Calendar'
 import Theme from './theme/Theme'
 import Theme from './theme/Theme'
@@ -10,7 +10,7 @@ import Theme from './theme/Theme'
 /* Toolbar with buttons and title
 /* Toolbar with buttons and title
 ----------------------------------------------------------------------------------------------------------------------*/
 ----------------------------------------------------------------------------------------------------------------------*/
 
 
-export interface ToolbarRenderProps extends DomLocation {
+export interface ToolbarRenderProps {
   extraClassName: string
   extraClassName: string
   layout: any
   layout: any
   title: string
   title: string

+ 2 - 2
packages/core/src/common/DayHeader.ts

@@ -1,4 +1,4 @@
-import { Component, DomLocation } from '../view-framework'
+import { Component } from '../view-framework'
 import ComponentContext from '../component/ComponentContext'
 import ComponentContext from '../component/ComponentContext'
 import { htmlToElement } from '../util/dom-manip'
 import { htmlToElement } from '../util/dom-manip'
 import { DateMarker } from '../datelib/marker'
 import { DateMarker } from '../datelib/marker'
@@ -6,7 +6,7 @@ import { DateProfile } from '../DateProfileGenerator'
 import { createFormatter } from '../datelib/formatting'
 import { createFormatter } from '../datelib/formatting'
 import { computeFallbackHeaderFormat, renderDateCell } from './table-utils'
 import { computeFallbackHeaderFormat, renderDateCell } from './table-utils'
 
 
-export interface DayHeaderProps extends DomLocation {
+export interface DayHeaderProps {
   dates: DateMarker[]
   dates: DateMarker[]
   dateProfile: DateProfile
   dateProfile: DateProfile
   datesRepDistinctDays: boolean
   datesRepDistinctDays: boolean

+ 2 - 2
packages/core/src/common/Scroller.ts

@@ -1,7 +1,7 @@
 import { computeEdges } from '../util/dom-geom'
 import { computeEdges } from '../util/dom-geom'
 import { createElement, applyStyle, applyStyleProp } from '../util/dom-manip'
 import { createElement, applyStyle, applyStyleProp } from '../util/dom-manip'
 import { ElementScrollController } from './scroll-controller'
 import { ElementScrollController } from './scroll-controller'
-import { Component, DomLocation } from '../view-framework'
+import { Component } from '../view-framework'
 
 
 export interface ScrollbarWidths {
 export interface ScrollbarWidths {
   left: number
   left: number
@@ -9,7 +9,7 @@ export interface ScrollbarWidths {
   bottom: number
   bottom: number
 }
 }
 
 
-export interface ScrollerProps extends DomLocation {
+export interface ScrollerProps {
   overflowX: string
   overflowX: string
   overflowY: string
   overflowY: string
 }
 }

+ 1 - 1
packages/core/src/component/DateComponent.ts

@@ -34,7 +34,7 @@ PURPOSES:
 - hook up to fg, fill, and mirror renderers
 - hook up to fg, fill, and mirror renderers
 - interface for dragging and hits
 - interface for dragging and hits
 */
 */
-export default abstract class DateComponent<Props, State={}, Snapshot={}> extends Component<Props, ComponentContext, State, Snapshot> {
+export default abstract class DateComponent<Props, State={}, RenderResult=void, Snapshot={}> extends Component<Props, ComponentContext, State, RenderResult, Snapshot> {
 
 
   // self-config, overridable by subclasses. must set on prototype
   // self-config, overridable by subclasses. must set on prototype
   fgSegSelector: string // lets eventRender produce elements without fc-event class
   fgSegSelector: string // lets eventRender produce elements without fc-event class

+ 107 - 0
packages/core/src/util/runner.ts

@@ -0,0 +1,107 @@
+
+
+export class DelayedRunner {
+
+  private isDirty: boolean = false
+  private timeoutId: number = 0
+  private pauseDepth: number = 0
+
+  constructor(
+    private drainedOption?: () => void
+  ) {
+  }
+
+  request(delay?: number) {
+    this.isDirty = true
+    this.clearTimeout()
+
+    if (delay == null) {
+      this.tryDrain()
+    } else {
+      this.timeoutId = setTimeout(this.tryDrain.bind(this), delay) as unknown as number // NOT OPTIMAL! TODO: look at debounce
+    }
+  }
+
+  pause() {
+    this.clearTimeout()
+    this.pauseDepth++
+  }
+
+  resume() {
+    this.pauseDepth--
+    this.tryDrain()
+  }
+
+  private clearTimeout() {
+    if (this.timeoutId) {
+      clearTimeout(this.timeoutId)
+      this.timeoutId = 0
+    }
+  }
+
+  private tryDrain() {
+    if (!this.pauseDepth && this.isDirty) {
+      this.isDirty = false
+      this.drained()
+    }
+  }
+
+  protected drained() {
+    if (this.drainedOption) {
+      this.drainedOption()
+    }
+  }
+
+}
+
+
+export class TaskRunner<Task> {
+
+  private isRunning = false
+  private queue: Task[] = []
+  private delayedRunner: DelayedRunner
+
+  constructor(
+    private runTaskOption?: (task: Task) => void,
+    private drainedOption?: (completedTasks: Task[]) => void
+  ) {
+    this.delayedRunner = new DelayedRunner(this.tryDrain.bind(this))
+  }
+
+  request(task: Task, delay?: number) {
+    this.queue.push(task)
+    this.delayedRunner.request(delay)
+  }
+
+  private tryDrain() {
+    let { queue } = this
+
+    if (!this.isRunning && queue.length) {
+      this.isRunning = true
+
+      let completedTasks: Task[] = []
+      let task: Task
+
+      while (task = queue.shift()) {
+        this.runTask(task)
+        completedTasks.push(task)
+      }
+
+      this.isRunning = false
+      this.drained(completedTasks)
+    }
+  }
+
+  protected runTask(task: Task) {
+    if (this.runTaskOption) {
+      this.runTaskOption(task)
+    }
+  }
+
+  protected drained(completedTasks: Task[]) {
+    if (this.drainedOption) {
+      this.drainedOption(completedTasks)
+    }
+  }
+
+}

+ 525 - 137
packages/core/src/view-framework.ts

@@ -1,29 +1,195 @@
+import { removeElement } from './util/dom-manip'
+import { isArraysEqual, removeMatching } from './util/array'
+import { isPropsEqual, filterHash } from './util/object'
+import { __assign } from 'tslib'
 
 
-export interface DomLocation {
-  parentEl: HTMLElement
-  prevSiblingEl?: HTMLElement
-  nextSiblingEl?: HTMLElement
-  prepend?: boolean
+let guid = 0
+
+
+// top-level renderer
+// ----------------------------------------------------------------------------------------------------
+
+
+export type DomRenderResult =
+  Node |
+  Node[] |
+  { rootEl: Node } |  // like a Component
+  { rootEls: Node[] } // "
+
+export type LocationAndProps<RenderRes, Props> = (RenderRes extends DomRenderResult ? Partial<DomLocation> : {}) & Props
+
+
+export function renderer<ComponentType>(componentClass: ComponentClass<ComponentType>): ((
+  inputProps: (ComponentType extends Component<infer Props, infer Context, infer State, infer RenderRes> ? LocationAndProps<RenderRes, Props> : never) | false,
+  context?: ComponentType extends Component<infer Props, infer Context> ? Context : never
+) => ComponentType) & {
+  current: ComponentType | null
 }
 }
+export function renderer<FuncProps, Context, FuncState>(
+  renderFunc: (funcProps: FuncProps, context?: Context) => FuncState,
+  unrenderFunc?: (funcState: FuncState, context?: Context) => void
+): ((
+  funcProps: LocationAndProps<FuncState, FuncProps> | false,
+  context?: Context
+) => FuncState) & {
+  current: FuncState | null
+}
+export function renderer(worker: any, unrenderFunc?: any) {
+  if (worker.prototype) { // a class
+    return componentRenderer(worker)
+  } else {
+    return funcRenderer(worker, unrenderFunc)
+  }
+}
+
 
 
+// function renderer
+// ----------------------------------------------------------------------------------------------------
 
 
-export type EqualityFunc = (val0: any, val1: any) => boolean
-export type EqualityFuncHash = { [name: string]: EqualityFunc }
 
 
-export type RenderDomResult =
-  HTMLElement[] |
-  HTMLElement |
-  { rootEl: HTMLElement } |
-  { rootEls: HTMLElement[] } |
-  Component<any, any>
+type FuncRenderer =
+  ((funcProps: any, context?: any) => any) &
+  { current: any | null }
 
 
 
 
-export abstract class Component<Props, Context={}, State={}, Snapshot={}> {
+function funcRenderer(
+  renderFunc: (funcProps: any, context?: any) => any,
+  unrenderFunc?: (funcState: any, context?: any) => void
+): FuncRenderer {
+  let currentProps // used as a flag for ever-rendered
+  let currentContext
+  let currentState
+  let currentLocation
+  let currentRootEls = []
 
 
-  uid: string
-  state: State
-  rootEl: HTMLElement
-  rootEls: HTMLElement[]
+  function render(location, props, context) {
+    let newRootEls
+
+    if (!currentProps) { // first time?
+      currentState = renderFunc(currentProps, currentContext)
+      newRootEls = normalizeRenderEls(currentState)
+
+    } else if ( // any changes?
+      !isPropsEqual(currentProps, props) ||
+      renderFunc.length > 1 && !isPropsEqual(currentContext, context)
+    ) {
+      if (unrenderFunc) {
+        unrenderFunc(currentState, context)
+      }
+
+      currentState = renderFunc(currentProps, currentContext)
+      update.current = currentState
+      newRootEls = normalizeRenderEls(currentState)
+    }
+
+    if (newRootEls && !isArraysEqual(newRootEls, currentRootEls)) {
+      currentRootEls.forEach(removeElement)
+      insertNodesAtLocation(newRootEls, location)
+
+    } else if (!isPropsEqual(location, currentLocation)) {
+      insertNodesAtLocation(currentRootEls, location)
+    }
+
+    currentProps = props
+    currentContext = context
+    currentLocation = location
+    currentRootEls = newRootEls
+  }
+
+  function unrender() {
+    if (currentProps && unrenderFunc) {
+      unrenderFunc(currentState, currentContext)
+    }
+
+    currentProps = null
+    currentContext = null
+    currentState = null
+    currentLocation = null
+    currentRootEls = []
+    update.current = null
+  }
+
+  let update = function(this: Component<any> | any, propsAndLocation: any, contextOverride?: any) {
+    handleUpdate(this, propsAndLocation, contextOverride, render, unrender)
+    return currentState
+  } as ComponentRenderer
+
+  return update
+}
+
+
+// component renderer
+// ----------------------------------------------------------------------------------------------------
+
+
+type ComponentRenderer =
+  ((propsAndLocation: any, context?: any) => any) &
+  { current: Component<any> | null }
+
+
+function componentRenderer(componentClass: ComponentClass<any>): ComponentRenderer {
+  let renderEngine: RenderEngine
+  let component: Component<any> | null = null
+
+  function render(location, props, context, isTopLevel) {
+    if (!renderEngine) {
+      renderEngine = isTopLevel ? new RenderEngine() : this.renderEngine
+    }
+
+    if (!component) {
+      component = update.current = new componentClass(props, context)
+      component.renderEngine = renderEngine
+    }
+
+    renderEngine.updateComponentExternal(
+      component,
+      location,
+      props,
+      context
+    )
+  }
+
+  function unrender() {
+    if (component) {
+      renderEngine.unmountComponent(component)
+      update.current = null
+      component = null
+    }
+  }
+
+  let update = function(this: Component<any> | any, propsAndLocation: any, contextOverride?: any) {
+    handleUpdate(this, propsAndLocation, contextOverride, render, unrender)
+    return component
+  } as ComponentRenderer
+
+  return update
+}
+
+
+// component class
+// ----------------------------------------------------------------------------------------------------
+
+
+export type PropEqualityFuncs<ComponentType> = ComponentType extends Component<infer Props> ? EqualityFuncs<Props> : never
+export type StateEqualityFuncs<ComponentType> = ComponentType extends Component<infer Props, infer State> ? EqualityFuncs<State> : never
+export type EqualityFuncs<ObjType> = {
+  [K in keyof ObjType]?: (a: ObjType[K], b: ObjType[K]) => boolean
+}
+
+
+export abstract class Component<Props, Context={}, State={}, RenderResult=void, Snapshot={}> {
+
+  propEquality: EqualityFuncs<Props>
+  stateEquality: EqualityFuncs<State>
+  renderEngine: RenderEngine
+  childUnmounts: (() => void)[] = []
+
+  uid = String(guid++)
+  isMounted = false
+  location: Partial<DomLocation> = {}
+  rootEls: Node[] = [] // TODO: rename to rootNodes?
+  rootEl: HTMLElement | null = null // TODO: rename to rootNode?
+  state: State = {} as State
 
 
   constructor(
   constructor(
     public props: Props,
     public props: Props,
@@ -31,20 +197,23 @@ export abstract class Component<Props, Context={}, State={}, Snapshot={}> {
   ) {
   ) {
   }
   }
 
 
-  abstract render(props: Props, context: Context, state: State):
-    Props extends DomLocation ? RenderDomResult : void
+  abstract render(props: Props, context: Context, state: State): RenderResult
 
 
   unrender() {}
   unrender() {}
 
 
-  setState(state: Partial<State>) {
+  setState(stateUpdates: Partial<State>) {
+    this.renderEngine.requestUpdateComponentInternal(this, stateUpdates)
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
   }
   }
 
 
-  // getSnapshotBeforeUpdate(prevProps: Props, prevState: State)
+  getSnapshotBeforeUpdate(prevProps: Props, prevState: State, prevContext: Context) {
+    return {} as Snapshot
+  }
 
 
-  shouldComponentUpdate(nextProps: Props, nextState: State) {
+  shouldComponentUpdate(nextProps: Props, nextState: State, prevContext: Context) {
+    return true
   }
   }
 
 
   componentDidUpdate(prevProps: Props, prevState: State, snapshot: Snapshot) {
   componentDidUpdate(prevProps: Props, prevState: State, snapshot: Snapshot) {
@@ -53,14 +222,23 @@ export abstract class Component<Props, Context={}, State={}, Snapshot={}> {
   componentWillUnmount() {
   componentWillUnmount() {
   }
   }
 
 
-  static addPropEquality(propEquality: EqualityFuncHash) {
+  static addPropEquality<ComponentType>(this: ComponentClass<ComponentType>, propEquality: PropEqualityFuncs<ComponentType>) {
+    let hash = Object.create(this.prototype.propEquality)
+    __assign(hash, propEquality)
+    this.prototype.propEquality = hash
   }
   }
 
 
-  static addStateEquality(stateEquality: EqualityFuncHash) {
+  static addStateEquality<ComponentType>(this: ComponentClass<ComponentType>, stateEquality: StateEqualityFuncs<ComponentType>) {
+    let hash = Object.create(this.prototype.stateEquality)
+    __assign(hash, stateEquality)
+    this.prototype.stateEquality = hash
   }
   }
 
 
 }
 }
 
 
+Component.prototype.propEquality = {}
+Component.prototype.stateEquality = {}
+
 
 
 export type ComponentClass<ComponentType> = (
 export type ComponentClass<ComponentType> = (
   new(
   new(
@@ -78,173 +256,383 @@ export type ComponentClass<ComponentType> = (
 }
 }
 
 
 
 
-type InputProps<Props> = Props extends DomLocation ? (Omit<Props, keyof DomLocation> & Partial<DomLocation>) : Props
+// component rendering engine
+// ----------------------------------------------------------------------------------------------------
 
 
 
 
-export function renderer<ComponentType>(componentClass: ComponentClass<ComponentType>): ((
-  inputProps:
-    (ComponentType extends Component<infer Props> ? InputProps<Props> : never) | false,
-  context?:
-    (ComponentType extends Component<infer Props, infer Context> ? Context : never)
-) => ComponentType) & {
-  current: ComponentType | null
+interface StateUpdate {
+  component: Component<any>
+  updates: any
 }
 }
-export function renderer<FuncProps, Context, FuncState>(
-  renderFunc: (funcProps: FuncProps, context?: Context) => FuncState,
-  unrenderFunc?: (funcState: FuncState, context?: Context) => void
-): ((
-  funcProps: InputProps<FuncProps> | false,
-  context?: Context
-) => FuncState) & {
-  current: FuncState | null
+
+interface AfterRender {
+  component: Component<any>
+  prevProps?: any
+  prevState?: any
+  prevContext?: any
+  snapshot?: any
 }
 }
-export function renderer(worker: any, unrenderFunc?: any) {
-  if (worker.prototype) { // a class
-    return componentRenderer(worker)
+
+
+class RenderEngine {
+
+  externalUpdateDepth = 0
+  stateUpdates: StateUpdate[] = []
+  afterRenders: AfterRender[] = []
+
+
+  updateComponentExternal(component: Component<any>, location: Partial<DomLocation>, props: any, context: any) {
+    this.externalUpdateDepth++
+
+    let { isMounted } = component
+    let prevProps = component.props
+    let prevContext = component.context
+
+    let massagedProps = isMounted ? recycleProps(prevProps, props, false, component.propEquality) : prevProps
+    let massagedContext = isMounted ? recycleProps(prevContext, context, false, {}) : prevContext
+
+    if (massagedProps || massagedContext) {
+      this.updateComponent(
+        component,
+        location,
+        massagedProps || prevProps,
+        massagedContext || prevContext,
+        component.state
+      )
+    } else if (location.parentEl && !isPropsEqual(component.location, location)) {
+      this.afterRenders.push(
+        relocateComponent(component, location as DomLocation)
+      )
+    }
+
+    this.externalUpdateDepth--
+    this.drain()
+  }
+
+
+  requestUpdateComponentInternal(component: Component<any>, stateUpdates: any) {
+    this.stateUpdates.push({ component, updates: stateUpdates })
+    this.drain()
+  }
+
+
+  unmountComponent(component: Component<any>) {
+    unmountComponent(component)
+
+    removeFromComponentQueue(this.stateUpdates, component)
+    removeFromComponentQueue(this.afterRenders, component)
+  }
+
+
+  private drain() {
+    if (!this.externalUpdateDepth) {
+      while (
+        drainQueue(this.stateUpdates, this.runStateUpdate) ||
+        drainQueue(this.afterRenders, this.runAfterRender)
+      ) {
+      }
+    }
+  }
+
+
+  private runStateUpdate = (task: StateUpdate) => {
+    let { component, updates } = task
+    let massagedState = recycleProps(component.state, updates, true, component.stateEquality) // additions=true
+
+    if (massagedState) {
+      this.updateComponent(component, component.location, component.props, component.context, massagedState)
+    }
+  }
+
+
+  private runAfterRender = (task: AfterRender) => {
+    let { component, prevProps, prevState, snapshot } = task
+
+    if (prevProps) {
+      component.componentDidUpdate(prevProps, prevState, snapshot)
+    } else {
+      component.componentDidMount()
+    }
+  }
+
+
+  private updateComponent(component: Component<any>, location: any, nextProps: any, nextContext: any, nextState: any) {
+    if (component.shouldComponentUpdate(nextProps, nextState, nextContext)) {
+      this.afterRenders.push(
+        updateComponent(component, location, nextProps, nextContext, nextState)
+      )
+    }
+  }
+
+}
+
+
+// component lifecycle executors
+// ----------------------------------------------------------------------------------------------------
+
+
+function updateComponent(component: Component<any>, location: Partial<DomLocation>, nextProps: any, nextContext: any, nextState: any): AfterRender {
+  component.childUnmounts = []
+
+  if (!component.isMounted) {
+
+    // component already has props/context from constructor
+    runRender(component, location, nextProps, nextContext, nextState)
+    component.isMounted = true
+
+    return { component }
+
   } else {
   } else {
-    return funcRenderer(worker, unrenderFunc)
+    let prevProps = component.props
+    let prevContext = component.context
+    let prevState = component.state
+    let snapshot = component.getSnapshotBeforeUpdate(prevProps, prevState, prevContext) || {}
+
+    component.unrender()
+    component.props = nextProps
+    component.context = nextContext
+    component.state = nextState
+    runRender(component, location, nextProps, nextContext, nextState)
+
+    return { component, prevProps, prevState, prevContext, snapshot }
   }
   }
 }
 }
 
 
 
 
-function componentRenderer<ComponentType>(componentClass: ComponentClass<ComponentType>): ((
-  inputProps:
-    (ComponentType extends Component<infer Props> ? InputProps<Props> : never) | false,
-  context?:
-    (ComponentType extends Component<infer Props, infer Context> ? Context : never)
-) => ComponentType) & {
-  current: ComponentType | null
-} {
-  return null as any
+function runRender(component: Component<any>, location: Partial<DomLocation>, nextProps: any, nextContext: any, nextState: any) {
+  let renderRes = component.render(nextProps, nextContext, nextState)
+  let rootEls = normalizeRenderEls(renderRes)
+
+  if (
+    !isArraysEqual(rootEls, component.rootEls) ||
+    !isPropsEqual(location, component.location)
+  ) {
+    component.rootEls.forEach(removeElement)
+
+    if (location.parentEl) {
+      insertNodesAtLocation(rootEls, location as DomLocation)
+    }
+
+    component.location = location
+    component.rootEls = rootEls
+    component.rootEl = rootEls[0] as HTMLElement || null
+  }
 }
 }
 
 
 
 
-function funcRenderer<FuncProps, Context, FuncState>(
-  renderFunc: (funcProps: FuncProps, context?: Context) => FuncState,
-  unrenderFunc?: (funcState: FuncState, context?: Context) => void
-): ((
-  funcProps: InputProps<FuncProps> | false,
-  context?: Context
-) => FuncState) & {
-  current: FuncState | null
-} {
-  return null as any
+function relocateComponent(component: Component<any>, location: DomLocation): AfterRender {
+  let prevProps = component.props
+  let prevContext = component.context
+  let prevState = component.state
+  let snapshot = component.getSnapshotBeforeUpdate(prevProps, prevState, prevContext) || {}
+
+  insertNodesAtLocation(component.rootEls, location) // dont need to remove first
+
+  component.location = location
+
+  return { component, prevProps, prevState, prevContext, snapshot }
+}
+
+
+function unmountComponent(component: Component<any>) {
+  component.unrender()
+  component.componentWillUnmount()
+
+  let { childUnmounts } = component
+  for (let i = childUnmounts.length - 1; i >= 0; i--) {
+    childUnmounts[i]()
+  }
+
+  component.rootEls.forEach(removeElement)
+  component.rootEls = null
+  component.rootEl = null
+}
+
+
+// function/component rendering helpers
+// ----------------------------------------------------------------------------------------------------
+
+
+const DOM_LOCATION_KEYS: { [P in keyof DomLocation]-?: true } = {
+  parentEl: true,
+  previousSibling: true,
+  nextSibling: true,
+  prepend: true
+}
+
+
+function handleUpdate(caller, propsAndLocation, contextOverride, update, unmount) {
+  let isTopLevel = !caller.renderEngine // TODO: naming collision for caller?
+
+  if (!propsAndLocation) {
+    unmount()
+
+  } else {
+    let location = whitelistProps(propsAndLocation, DOM_LOCATION_KEYS)
+
+    if (('parentEl' in location) && location.parentEl == null) {
+      unmount()
+
+    } else {
+      let props = blacklistProps(propsAndLocation, DOM_LOCATION_KEYS)
+
+      update(location, props, contextOverride || (isTopLevel ? {} : caller.context), isTopLevel)
+
+      if (!isTopLevel) {
+        ;(caller as Component<any>).childUnmounts.push(unmount)
+      }
+    }
+  }
+}
+
+
+function normalizeRenderEls(input: any): Node[] {
+  if (!input) {
+    return []
+
+  } else if (Array.isArray(input)) {
+    return input.filter(function(item) {
+      return item instanceof Node
+    })
+
+  } else if (input.rootEls) {
+    return input.rootEls as Node[]
+
+  } else if (input.rootEl) {
+    return [ input.rootEl as Node ]
+
+  } else if (input instanceof Node) {
+    return [ input ]
+  }
 }
 }
 
 
 
 
+// list rendering (TODO)
+// ----------------------------------------------------------------------------------------------------
+
+
 export interface ListRendererItem<ComponentType> {
 export interface ListRendererItem<ComponentType> {
   id: string
   id: string
   componentClass: ComponentClass<ComponentType>
   componentClass: ComponentClass<ComponentType>
   props: ComponentType extends Component<infer Props> ? Omit<Props, keyof DomLocation> : never
   props: ComponentType extends Component<infer Props> ? Omit<Props, keyof DomLocation> : never
 }
 }
 
 
-export function listRenderer(): (location: DomLocation, inputs: ListRendererItem<any>[], context?: any) => Component<any, any>[] {
+
+export function listRenderer(): (location: DomLocation, inputs: ListRendererItem<any>[], contextOverride?: any) => Component<any>[] {
   return null as any
   return null as any
 }
 }
 
 
 
 
-export class DelayedRunner {
+// queue
+// ----------------------------------------------------------------------------------------------------
 
 
-  private isDirty: boolean = false
-  private timeoutId: number = 0
-  private pauseDepth: number = 0
 
 
-  constructor(
-    private drainedOption?: () => void
-  ) {
-  }
-
-  request(delay?: number) {
-    this.isDirty = true
+function removeFromComponentQueue(queue: { component: Component<any> }[], component: Component<any>) {
+  return removeMatching(queue, function(task) {
+    return task.component === component
+  })
+}
 
 
-    if (delay == null) {
-      this.clearTimeout()
-      this.tryDrain()
 
 
-    } else if (!this.timeoutId) {
-      this.timeoutId = setTimeout(this.tryDrain.bind(this), delay) as unknown as number
-    }
-  }
+function drainQueue(queue: any[], runnerFunc) {
+  let completedCnt = 0
+  let task
 
 
-  pause() {
-    this.pauseDepth++
+  while (task = queue.shift()) {
+    runnerFunc(task)
+    completedCnt++
   }
   }
 
 
-  resume() {
-    this.pauseDepth--
-    this.tryDrain()
-  }
+  return completedCnt
+}
 
 
-  private clearTimeout() {
-    if (this.timeoutId) {
-      clearTimeout(this.timeoutId)
-      this.timeoutId = 0
-    }
-  }
 
 
-  private tryDrain() {
-    if (!this.pauseDepth && this.isDirty) {
-      this.isDirty = false
-      this.drained()
-    }
-  }
+// dom util
+// ----------------------------------------------------------------------------------------------------
 
 
-  protected drained() {
-    if (this.drainedOption) {
-      this.drainedOption()
-    }
-  }
 
 
+export interface DomLocation {
+  parentEl: HTMLElement
+  previousSibling?: Node
+  nextSibling?: Node
+  prepend?: boolean
 }
 }
 
 
 
 
-export class TaskRunner<Task> {
+export function insertNodesAtLocation(nodes: Node[], location: DomLocation) {
+  let { parentEl, previousSibling, nextSibling } = location
 
 
-  private isRunning = false
-  private queue: Task[] = []
-  private delayedRunner: DelayedRunner
+  if (location.prepend) {
+    nextSibling = parentEl.firstChild as HTMLElement
 
 
-  constructor(
-    private runTaskOption?: (task: Task) => void,
-    private drainedOption?: (completedTasks: Task[]) => void
-  ) {
-    this.delayedRunner = new DelayedRunner(this.tryDrain.bind(this))
+  } else if (previousSibling) {
+    nextSibling = previousSibling.nextSibling
+
+  } else if (!nextSibling) {
+    nextSibling = null // important for insertBefore
   }
   }
 
 
-  request(task: Task, delay?: number) {
-    this.queue.push(task)
-    this.delayedRunner.request(delay)
+  for (let node of nodes) {
+    parentEl.insertBefore(node, nextSibling)
   }
   }
+}
 
 
-  private tryDrain() {
-    let { queue } = this
 
 
-    if (!this.isRunning && queue.length) {
-      this.isRunning = true
+// object util
+// ----------------------------------------------------------------------------------------------------
 
 
-      let completedTasks: Task[] = []
-      let task: Task
 
 
-      while (task = queue.shift()) {
-        this.runTask(task)
-        completedTasks.push(task)
-      }
+function whitelistProps<ObjType>(props: ObjType, whitelist): Partial<ObjType> {
+  return filterHash(props, function(val, key) { // TODO: give typings
+    return whitelist[key]
+  })
+}
 
 
-      this.isRunning = false
-      this.drained(completedTasks)
-    }
+
+function blacklistProps<ObjType>(props: ObjType, blacklist): Partial<ObjType> {
+  return filterHash(props, function(val, key) { // TODO: give typings
+    return !blacklist[key]
+  })
+}
+
+
+function recycleProps(oldProps, newProps, isReset: boolean, equalityFuncs: EqualityFuncs<any>) {
+  let comboProps = {} as any // some old, some new
+  let anyChanges = false
+
+  if (isReset && oldProps === newProps) {
+    return null
   }
   }
 
 
-  protected runTask(task: Task) {
-    if (this.runTaskOption) {
-      this.runTaskOption(task)
+  for (let key in newProps) {
+    if (
+      key in oldProps && (
+        oldProps[key] === newProps[key] ||
+        (equalityFuncs[key] && equalityFuncs[key](oldProps[key], newProps[key]))
+      )
+    ) {
+      // equal to old? use old prop
+      comboProps[key] = oldProps[key]
+    } else {
+      comboProps[key] = newProps[key]
+      anyChanges = true
     }
     }
   }
   }
 
 
-  protected drained(completedTasks: Task[]) {
-    if (this.drainedOption) {
-      this.drainedOption(completedTasks)
+  // of new object is resetting the old object,
+  // check for props that were omitted in the new
+  if (isReset) {
+    for (let key in oldProps) {
+      if (!(key in newProps)) {
+        anyChanges = true
+        break
+      }
     }
     }
   }
   }
 
 
+  if (anyChanges) {
+    return comboProps
+  }
+
+  return null
 }
 }

+ 2 - 3
packages/daygrid/src/DayTable.ts

@@ -11,12 +11,11 @@ import {
   Slicer,
   Slicer,
   Hit,
   Hit,
   ComponentContext,
   ComponentContext,
-  renderer,
-  DomLocation
+  renderer
 } from '@fullcalendar/core'
 } from '@fullcalendar/core'
 import { default as Table, TableSeg, TableRenderProps } from './Table'
 import { default as Table, TableSeg, TableRenderProps } from './Table'
 
 
-export interface DayTableProps extends DomLocation {
+export interface DayTableProps {
   renderProps: TableRenderProps
   renderProps: TableRenderProps
   dateProfile: DateProfile | null
   dateProfile: DateProfile | null
   dayTableModel: DayTableModel
   dayTableModel: DayTableModel

+ 3 - 4
packages/daygrid/src/DayTile.ts

@@ -7,12 +7,11 @@ import {
   ComponentContext,
   ComponentContext,
   EventInstanceHash,
   EventInstanceHash,
   renderer,
   renderer,
-  DomLocation,
   htmlToElements
   htmlToElements
 } from '@fullcalendar/core'
 } from '@fullcalendar/core'
 import DayTileEvents from './DayTileEvents'
 import DayTileEvents from './DayTileEvents'
 
 
-export interface DayTileProps extends DomLocation {
+export interface DayTileProps {
   date: DateMarker
   date: DateMarker
   fgSegs: Seg[]
   fgSegs: Seg[]
   selectedInstanceId: string
   selectedInstanceId: string
@@ -49,7 +48,7 @@ export default class DayTile extends DateComponent<DayTileProps> {
     // HACK referencing parent's elements.
     // HACK referencing parent's elements.
     // also, if parent's elements change, this will break.
     // also, if parent's elements change, this will break.
     calendar.registerInteractiveComponent(this, {
     calendar.registerInteractiveComponent(this, {
-      el: this.props.parentEl, // HACK
+      el: this.location.parentEl, // HACK
       useEventCenter: false
       useEventCenter: false
     })
     })
   }
   }
@@ -70,7 +69,7 @@ export default class DayTile extends DateComponent<DayTileProps> {
           allDay: true,
           allDay: true,
           range: { start: date, end: addDays(date, 1) }
           range: { start: date, end: addDays(date, 1) }
         },
         },
-        dayEl: this.props.parentEl, // HACK
+        dayEl: this.location.parentEl, // HACK
         rect: {
         rect: {
           left: 0,
           left: 0,
           top: 0,
           top: 0,

+ 2 - 3
packages/daygrid/src/DayTileEvents.ts

@@ -2,8 +2,7 @@ import {
   Seg,
   Seg,
   ComponentContext,
   ComponentContext,
   BaseFgEventRendererProps,
   BaseFgEventRendererProps,
-  renderer,
-  DomLocation
+  renderer
 } from '@fullcalendar/core'
 } from '@fullcalendar/core'
 import CellEvents from './CellEvents'
 import CellEvents from './CellEvents'
 
 
@@ -34,6 +33,6 @@ export default class DayTileEvents extends CellEvents<DayTileEventsProps> {
 }
 }
 
 
 
 
-function attachSegs(props: { segs: Seg[] } & DomLocation) {
+function attachSegs(props: { segs: Seg[] }) {
   return props.segs.map((seg) => seg.el)
   return props.segs.map((seg) => seg.el)
 }
 }

+ 2 - 2
packages/daygrid/src/Popover.ts

@@ -6,10 +6,10 @@ import {
   createElement,
   createElement,
   applyStyle,
   applyStyle,
   listenBySelector,
   listenBySelector,
-  computeClippingRect, computeRect, Component, ComponentContext, DomLocation
+  computeClippingRect, computeRect, Component, ComponentContext
 } from '@fullcalendar/core'
 } from '@fullcalendar/core'
 
 
-export interface PopoverProps extends DomLocation {
+export interface PopoverProps {
   clippingEl: HTMLElement
   clippingEl: HTMLElement
   extraClassName?: string
   extraClassName?: string
   top?: number
   top?: number

+ 2 - 3
packages/daygrid/src/Table.ts

@@ -27,7 +27,6 @@ import TableMirrorEvents from './TableMirrorEvents'
 import TableFills from './TableFills'
 import TableFills from './TableFills'
 import DayTile from './DayTile'
 import DayTile from './DayTile'
 import { renderDayBgRowHtml } from './DayBgRow'
 import { renderDayBgRowHtml } from './DayBgRow'
-import { DomLocation } from '@fullcalendar/core/view-framework'
 
 
 const DAY_NUM_FORMAT = createFormatter({ day: 'numeric' })
 const DAY_NUM_FORMAT = createFormatter({ day: 'numeric' })
 const WEEK_NUM_FORMAT = createFormatter({ week: 'numeric' })
 const WEEK_NUM_FORMAT = createFormatter({ week: 'numeric' })
@@ -66,7 +65,7 @@ export interface CellModel {
   htmlAttrs?: string
   htmlAttrs?: string
 }
 }
 
 
-export interface TableProps extends DomLocation {
+export interface TableProps {
   renderProps: TableRenderProps
   renderProps: TableRenderProps
   dateProfile: DateProfile
   dateProfile: DateProfile
   cells: CellModel[][]
   cells: CellModel[][]
@@ -177,7 +176,7 @@ export default class Table extends Component<TableProps, ComponentContext, Table
       segPopoverState &&
       segPopoverState &&
       segPopoverState.origFgSegs === props.fgEventSegs // will close popover when events change
       segPopoverState.origFgSegs === props.fgEventSegs // will close popover when events change
     ) {
     ) {
-      let viewEl = context.calendar.component.view.rootEl // yuck
+      let viewEl = context.calendar.component.view.rootEl as HTMLElement // yuck
 
 
       let popover = this.renderPopover({ // will be outside of all scrollers within the view
       let popover = this.renderPopover({ // will be outside of all scrollers within the view
         parentEl: viewEl,
         parentEl: viewEl,

+ 1 - 2
packages/timegrid/src/DayTimeCols.ts

@@ -14,11 +14,10 @@ import {
   Hit,
   Hit,
   ComponentContext,
   ComponentContext,
   renderer,
   renderer,
-  DomLocation
 } from '@fullcalendar/core'
 } from '@fullcalendar/core'
 import TimeCols, { TimeColsSeg, TimeColsRenderProps } from './TimeCols'
 import TimeCols, { TimeColsSeg, TimeColsRenderProps } from './TimeCols'
 
 
-export interface DayTimeColsProps extends DomLocation {
+export interface DayTimeColsProps {
   renderProps: TimeColsRenderProps
   renderProps: TimeColsRenderProps
   dateProfile: DateProfile | null
   dateProfile: DateProfile | null
   dayTableModel: DayTableModel
   dayTableModel: DayTableModel

+ 5 - 6
packages/timegrid/src/TimeCols.ts

@@ -24,8 +24,7 @@ import {
   DateProfile,
   DateProfile,
   sortEventSegs,
   sortEventSegs,
   memoize,
   memoize,
-  renderer,
-  DomLocation,
+  renderer
 } from '@fullcalendar/core'
 } from '@fullcalendar/core'
 import { renderDayBgRowHtml } from '@fullcalendar/daygrid'
 import { renderDayBgRowHtml } from '@fullcalendar/daygrid'
 import TimeColsEvents from './TimeColsEvents'
 import TimeColsEvents from './TimeColsEvents'
@@ -172,7 +171,7 @@ export default class TimeCols extends Component<TimeColsProps, ComponentContext>
       contentSkeletonEl,
       contentSkeletonEl,
       bottomRuleEl,
       bottomRuleEl,
       slatContainerEl
       slatContainerEl
-    } = this.renderSkeleton(true)
+    } = this.renderSkeleton({})
 
 
     this.renderBgColumns({
     this.renderBgColumns({
       parentEl: rootBgContainerEl,
       parentEl: rootBgContainerEl,
@@ -298,7 +297,7 @@ export default class TimeCols extends Component<TimeColsProps, ComponentContext>
 
 
 
 
   _renderSlats(
   _renderSlats(
-    { rootEl, dateProfile }: { rootEl: HTMLElement, dateProfile: DateProfile } & DomLocation,
+    { rootEl, dateProfile }: { rootEl: HTMLElement, dateProfile: DateProfile },
     context: ComponentContext
     context: ComponentContext
   ) {
   ) {
     let tableEl = createElement(
     let tableEl = createElement(
@@ -366,7 +365,7 @@ export default class TimeCols extends Component<TimeColsProps, ComponentContext>
 
 
   // goes behind the slats
   // goes behind the slats
   _renderBgColumns(
   _renderBgColumns(
-    { rootEl, cells, dateProfile, renderProps }: { rootEl: HTMLElement, cells: TimeColsCell[], dateProfile: DateProfile, renderProps: any } & DomLocation,
+    { rootEl, cells, dateProfile, renderProps }: { rootEl: HTMLElement, cells: TimeColsCell[], dateProfile: DateProfile, renderProps: any },
     context: ComponentContext
     context: ComponentContext
   ) {
   ) {
     let { calendar, view, isRtl, theme, dateEnv } = context
     let { calendar, view, isRtl, theme, dateEnv } = context
@@ -654,7 +653,7 @@ function renderSkeleton(props: {}, context: ComponentContext) {
 
 
 // Renders the DOM that the view's content will live in
 // Renders the DOM that the view's content will live in
 // goes in front of the slats
 // goes in front of the slats
-function renderContentSkeleton({ colCnt, renderProps }: { colCnt: number, renderProps: any  } & DomLocation, context: ComponentContext) {
+function renderContentSkeleton({ colCnt, renderProps }: { colCnt: number, renderProps: any  }, context: ComponentContext) {
   let { isRtl } = context
   let { isRtl } = context
   let parts = []
   let parts = []