Skip to content

Instantly share code, notes, and snippets.

@Zaggen
Last active January 29, 2017 23:40
Show Gist options
  • Save Zaggen/d065d432503eed0e7b0ab7945a872932 to your computer and use it in GitHub Desktop.
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
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