UNPKG

backbone-fractal

Version:

Lightweight composite views for Backbone

203 lines (174 loc) 6.07 kB
import { result, isFunction, isString, isArray, isUndefined, compact, map, each, extend, ListIterator, } from 'underscore'; import { eachRight } from './underscore-compat'; import { View, Model } from 'backbone'; export type Result<T> = T | { (): T }; export type Selector = NonNullable<string>; export type NoSelector = false; export type FallbackSelector = Selector | NoSelector; export type JQElement = JQuery<Element>; export type SafeInsertionMethod = 'append' | 'prepend'; export type RiskyInsertionMethod = 'after' | 'before' | 'replaceWith'; export type InsertionMethod = SafeInsertionMethod | RiskyInsertionMethod; export interface RootSubViewDton { view: string | Result<View>; selector?: Result<NoSelector>; method?: Result<SafeInsertionMethod>; place?: string | Result<boolean>; } export interface SelectorSubViewDton { view: string | Result<View>; selector: Result<Selector>; method?: Result<InsertionMethod>; place?: string | Result<boolean>; } export type SubViewDescription = RootSubViewDton | SelectorSubViewDton; export type SubView = string | Result<View | SubViewDescription>; export interface SubViewIterator { (view: View, selector: JQElement, method: InsertionMethod): void; } export interface IterationOptions { placeOnly?: boolean; reverse?: boolean; } const defaultIterationOptions: IterationOptions = { placeOnly: false, reverse: false, }; export type _NormalizedSubView = [View, JQElement, InsertionMethod]; interface ContainerMap { [selector: string]: JQElement; } export default class CompositeView<TModel extends Model = Model> extends View<TModel> { defaultPlacement: SafeInsertionMethod; _containers: ContainerMap; render() { this.beforeRender(); this.detachSubviews(); this._resetContainer(); this.placeSubviews(); this.afterRender(); return this; } beforeRender() { return this; } renderContainer() { return this; } afterRender() { return this; } remove(): this { this.clearSubviews(); delete this._containers; super.remove(); return this; } /** * Invariant: each subview must appear at most once in this list. */ subviews(): SubView[] { return []; } clearSubviews(): this { return this.forEachSubview(_removeSubview, { reverse: true }); } detachSubviews(): this { return this.forEachSubview(_detachSubview, { reverse: true }); } placeSubviews(): this { // No need to detach the subview first. // Each subview is unique, so jQuery moves rather than copies it. return this.forEachSubview(_placeSubview, { placeOnly: true }); } forEachSubview(iteratee: SubViewIterator, options?: IterationOptions):this { let _iteratee = args => iteratee.apply(this, args); let { placeOnly, reverse } = options || defaultIterationOptions; let iterator = reverse ? eachRight : each; let list = this._getSubviews(placeOnly); iterator(list, _iteratee); return this; } _resetContainer(): this { this._containers = {}; return this.renderContainer(); } _getSubviews(applyChecks: boolean): _NormalizedSubView[] { let normalize = this._normalizeSubview.bind(this, applyChecks); if (!this._containers) this._resetContainer(); return compact(map(result(this, 'subviews'), normalize)); } _normalizeSubview(applyChecks: boolean, sv: SubView): _NormalizedSubView | void { sv = this._resolve(sv); if (isUndefined(sv)) return null; if (sv instanceof View) return [sv, this.$el, this.defaultPlacement]; let { view, selector, place } = sv; if (applyChecks && !isUndefined(place) && !this._resolve(place)) { return null; } view = this._resolve(view); if (!(view instanceof View)) return null; if (isFunction(selector)) { selector = selector.call(this); } if (isString(selector)) { let { method } = sv; return this._normalizeSelectorSubview(view, selector, method); } else { let { method } = sv as RootSubViewDton; return this._normalizeRootSubview(view, method); } } _normalizeRootSubview( view: View, method: Result<SafeInsertionMethod>, ): _NormalizedSubView { if (isFunction(method)) { method = method.call(this) as SafeInsertionMethod; } if (method && method !== 'append' && method !== 'prepend') { throw new TypeError(`Selector empty or attempting jQuery method ` + `.${method}() on the root element of a CompositeView.`); } return [view, this.$el, method || this.defaultPlacement]; } _normalizeSelectorSubview( view: View, selector: Selector, method: Result<InsertionMethod>, ): _NormalizedSubView { if (isFunction(method)) method = method.call(this) as InsertionMethod; let container = this._containers[selector]; if (!container) { this._containers[selector] = container = this.$(selector).first(); } return [view, container, method || this.defaultPlacement]; } _resolve<T>(handle: string | Result<T>): T { if (isString(handle)) handle = result(this, handle) as Result<T>; if (isFunction(handle)) handle = handle.call(this) as T; return handle; } } extend(CompositeView.prototype, { defaultPlacement: 'append', }); export function _removeSubview<V extends View>(view: V): void { view.remove(); } export function _detachSubview<V extends View>(view: V): void { view.$el.detach(); } export function _placeSubview<V extends View>( subview: V, container: JQElement, method: InsertionMethod, ): void { const insert = (container[method]); insert.call(container, subview.el); }