Last active
January 29, 2017 23:40
-
-
Save Zaggen/d065d432503eed0e7b0ab7945a872932 to your computer and use it in GitHub Desktop.
Component classes that extend base backbone class, to have similar support to angular2 directives
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface IObjectLiteral { | |
[key: string]: any | |
} | |
// @description This type of view takes care of extra initialization | |
// and rendering of the element. It allows you to define components | |
// in a similar fashion of angular 2; You define a subComponents | |
// by adding its class to the subComponents array and add the corresponding | |
// tagName on the template, and it will be replaced at runtime. You can pass | |
// attributes to those subComponents by defining them on the empty tag element | |
export class ComponentView extends Backbone.View { | |
protected subComponents: Array<(typeof SubComponentView)> | |
protected _subComponentsInstances: SubComponentView[] | |
protected _sharedChannel: Backbone.Events | |
protected _templateVars: any | |
protected _super: any | |
public attrs: any | |
protected initialize(options: any = {}): void { | |
this._beforeInitialize.apply(this, arguments) | |
const {collection, model} = options | |
this.collection = collection || this.collection | |
this.model = model || this.model | |
this._sharedChannel = this._sharedChannel || _.extend({}, (Backbone as any).Events); | |
this.attrs = options.componentAttributes || {} | |
this._applyClassesAndIdAttributesToEl() | |
this._afterInitialize.apply(this, arguments) | |
} | |
protected _applyClassesAndIdAttributesToEl(){ | |
_.each(this.attrs, (v, attrName)=> { | |
switch(attrName){ | |
case 'class': { | |
this.$el.addClass(v) | |
break | |
} | |
case 'id': { | |
this.$el.attr(attrName, v) | |
break | |
} | |
default: | |
return | |
} | |
}) | |
} | |
protected _beforeInitialize(): void {} | |
protected _afterInitialize(): void {} | |
protected _getTemplateData(): IObjectLiteral { | |
if(this.model) | |
return this.model.toJSON() | |
else | |
return {} | |
} | |
protected _getTemplateAttributes(): IObjectLiteral { | |
return this.attrs || {} | |
} | |
protected _getSubComponentOptions(): IObjectLiteral{ | |
return _.extend({parentComponent: this}, _.pick(this, 'model', 'collection', '_sharedChannel')) | |
} | |
protected _parseComponentAttributes(componentEl: JQuery): IObjectLiteral { | |
return _.transform((componentEl[0].attributes as any),(attrs, {name, value})=> { | |
try { | |
value = JSON.parse(value) | |
} catch (e){} | |
attrs[_.camelCase(name)] = value | |
}, {}) | |
} | |
// TODO: Deprecated | |
protected _encodeAttributesData(attrs: IObjectLiteral): IObjectLiteral{ | |
return _.transform(_.keys(attrs), (memo, k)=> { | |
const val = attrs[k] | |
memo[k] = _.isObject(val) ? JSON.stringify(val) : val | |
}, {}) | |
} | |
protected _beforeRender(): void{} | |
protected _afterRender(): void{} | |
// Renders the element by compiling its template and then searches for any custom tag that matches | |
// the subComponents tags and replace those with the rendered version of those | |
public render() { | |
this._beforeRender.apply(this, arguments) | |
// const parsedAttributes = this._encodeAttributesData(this._getTemplateAttributes()) | |
const templateVars = this._templateVars || {} | |
const templateData = _.extend({attrs: this._getTemplateAttributes()}, templateVars, this._getTemplateData()) | |
// First we render the template contents and place them inside this componentView element | |
this.$el.html(this.template(templateData)) | |
// We pick a couple of properties from this componentView so we can pass them to the subComponents (child views) | |
const viewOptions = this._getSubComponentOptions() | |
this._processSubComponents(viewOptions) | |
this._afterRender.apply(this, arguments) | |
return this | |
} | |
protected _processSubComponents(viewOptions: IObjectLiteral){ | |
this._removeSubComponentsInstances() | |
_.each(this.subComponents, (SubComponentView)=> { | |
const {tagName} = SubComponentView.prototype | |
if(tagName === 'div'){ | |
const errorMsg = `SubComponent "${(SubComponentView as any).name}" has a generic tagName, | |
please add a custom one so it can be replaced on the parent` | |
throw new Error(errorMsg) | |
} | |
const dummyComponents = this.$(tagName) | |
_.each(dummyComponents, (el)=> { | |
// We instantiate each SubComponentView with the parent viewOptions, | |
// and replace the dummy element generated by the template fn | |
const $dummyComponent = $(el) | |
const componentAttributes = this._parseComponentAttributes($dummyComponent) | |
let subComponent | |
if(_.isEmpty($dummyComponent.children())){ | |
const customViewOptions = _.extend({componentAttributes}, viewOptions) | |
subComponent = new SubComponentView(customViewOptions) | |
$dummyComponent.replaceWith(subComponent.render().$el) | |
} | |
else { | |
const customViewOptions = _.extend({el: $dummyComponent[0], componentAttributes}, viewOptions) | |
subComponent = (new SubComponentView(customViewOptions)).render() | |
} | |
this._subComponentsInstances.push(subComponent) | |
}) | |
}) | |
} | |
public remove(){ | |
this._beforeRemove() | |
super.remove() | |
this._afterRemove() | |
} | |
protected _beforeRemove(){ | |
this._removeSubComponentsInstances() | |
} | |
protected _afterRemove(){} | |
protected _removeSubComponentsInstances(){ | |
_.each(this._subComponentsInstances, (c)=> { | |
c.remove() | |
}) | |
this._subComponentsInstances = [] | |
} | |
} | |
export class SubComponentView extends ComponentView { | |
private _parent: ComponentView | |
protected initialize(options: any) { | |
const {_sharedChannel, parentComponent} = options | |
this._parent = parentComponent | |
this._sharedChannel = _sharedChannel | |
super.initialize.apply(this, arguments) | |
} | |
} | |
export class SubComponentController extends SubComponentView { | |
// This type of component won't render a template, but | |
// it does call before and after render hooks | |
public render(){ | |
this._beforeRender.apply(this, arguments) | |
this._afterRender.apply(this, arguments) | |
return this | |
} | |
protected _beforeRemove(){} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment