2019-06-16 15:12:54 +02:00
import { setAttribute , text , isChildren , classNames , TAG _NAMES } from "./html.js" ;
2020-04-29 10:00:51 +02:00
import { errorToDOM } from "./error.js" ;
2019-06-14 23:08:41 +02:00
2019-06-16 15:12:54 +02:00
function objHasFns ( obj ) {
for ( const value of Object . values ( obj ) ) {
if ( typeof value === "function" ) {
return true ;
2019-06-14 23:08:41 +02:00
}
2019-06-16 15:12:54 +02:00
}
return false ;
2019-06-14 23:08:41 +02:00
}
2019-06-14 23:46:31 +02:00
/ * *
Bindable template . Renders once , and allows bindings for given nodes . If you need
to change the structure on a condition , use a subtemplate ( if )
2019-06-13 00:41:45 +02:00
supports
- event handlers ( attribute fn value with name that starts with on )
- one way binding of attributes ( other attribute fn value )
- one way binding of text values ( child fn value )
- refs to get dom nodes
- className binding returning object with className => enabled map
2020-04-29 10:10:33 +02:00
- add subviews inside the template
2019-06-13 00:41:45 +02:00
* /
2020-04-29 10:04:40 +02:00
export class TemplateView {
2020-04-29 10:00:51 +02:00
constructor ( value , render = undefined ) {
2019-06-13 00:41:45 +02:00
this . _value = value ;
2020-04-29 10:00:51 +02:00
this . _render = render ;
2019-06-14 22:43:31 +02:00
this . _eventListeners = null ;
this . _bindings = null ;
2020-04-29 10:00:51 +02:00
// this should become _subViews and also include templates.
// How do we know which ones we should update though?
// Wrapper class?
this . _subViews = null ;
this . _root = null ;
this . _boundUpdateFromValue = null ;
2019-06-13 00:41:45 +02:00
}
2020-04-29 10:00:51 +02:00
_subscribe ( ) {
this . _boundUpdateFromValue = this . _updateFromValue . bind ( this ) ;
if ( typeof this . _value . on === "function" ) {
this . _value . on ( "change" , this . _boundUpdateFromValue ) ;
}
else if ( typeof this . _value . subscribe === "function" ) {
this . _value . subscribe ( this . _boundUpdateFromValue ) ;
}
2019-06-13 00:41:45 +02:00
}
2020-04-29 10:00:51 +02:00
_unsubscribe ( ) {
if ( this . _boundUpdateFromValue ) {
if ( typeof this . _value . off === "function" ) {
this . _value . off ( "change" , this . _boundUpdateFromValue ) ;
}
else if ( typeof this . _value . unsubscribe === "function" ) {
this . _value . unsubscribe ( this . _boundUpdateFromValue ) ;
2019-06-14 22:43:31 +02:00
}
2020-04-29 10:00:51 +02:00
this . _boundUpdateFromValue = null ;
2019-06-13 00:41:45 +02:00
}
2020-04-29 10:00:51 +02:00
}
_attach ( ) {
if ( this . _eventListeners ) {
for ( let { node , name , fn } of this . _eventListeners ) {
node . addEventListener ( name , fn ) ;
2019-06-14 22:43:31 +02:00
}
2019-06-13 00:41:45 +02:00
}
}
2020-04-29 10:00:51 +02:00
_detach ( ) {
2019-06-14 22:43:31 +02:00
if ( this . _eventListeners ) {
for ( let { node , name , fn } of this . _eventListeners ) {
node . removeEventListener ( name , fn ) ;
}
}
2020-04-29 10:00:51 +02:00
}
mount ( options ) {
if ( this . _render ) {
this . _root = this . _render ( this , this . _value ) ;
} else if ( this . render ) { // overriden in subclass
this . _root = this . render ( this , this . _value ) ;
2019-06-13 00:41:45 +02:00
}
2020-04-29 10:00:51 +02:00
const parentProvidesUpdates = options && options . parentProvidesUpdates ;
if ( ! parentProvidesUpdates ) {
this . _subscribe ( ) ;
}
this . _attach ( ) ;
return this . _root ;
2019-06-13 00:41:45 +02:00
}
2020-04-29 10:00:51 +02:00
unmount ( ) {
this . _detach ( ) ;
this . _unsubscribe ( ) ;
for ( const v of this . _subViews ) {
v . unmount ( ) ;
}
}
root ( ) {
return this . _root ;
}
_updateFromValue ( ) {
this . update ( this . _value ) ;
}
update ( value ) {
this . _value = value ;
if ( this . _bindings ) {
for ( const binding of this . _bindings ) {
binding ( ) ;
2019-06-14 22:43:31 +02:00
}
}
2019-06-13 00:41:45 +02:00
}
_addEventListener ( node , name , fn ) {
2019-06-14 22:43:31 +02:00
if ( ! this . _eventListeners ) {
this . _eventListeners = [ ] ;
}
2019-06-13 00:41:45 +02:00
this . _eventListeners . push ( { node , name , fn } ) ;
}
2019-06-14 22:43:31 +02:00
_addBinding ( bindingFn ) {
if ( ! this . _bindings ) {
this . _bindings = [ ] ;
}
this . _bindings . push ( bindingFn ) ;
}
2020-04-29 10:00:51 +02:00
_addSubView ( view ) {
if ( ! this . _subViews ) {
this . _subViews = [ ] ;
2019-06-14 22:43:31 +02:00
}
2020-04-29 10:00:51 +02:00
this . _subViews . push ( view ) ;
2019-06-14 22:43:31 +02:00
}
_addAttributeBinding ( node , name , fn ) {
2019-06-13 00:41:45 +02:00
let prevValue = undefined ;
const binding = ( ) => {
const newValue = fn ( this . _value ) ;
if ( prevValue !== newValue ) {
prevValue = newValue ;
setAttribute ( node , name , newValue ) ;
}
} ;
2019-06-14 22:43:31 +02:00
this . _addBinding ( binding ) ;
2019-06-13 00:41:45 +02:00
binding ( ) ;
}
2019-06-14 23:08:41 +02:00
_addClassNamesBinding ( node , obj ) {
this . _addAttributeBinding ( node , "className" , value => classNames ( obj , value ) ) ;
}
2019-06-13 00:41:45 +02:00
_addTextBinding ( fn ) {
const initialValue = fn ( this . _value ) ;
const node = text ( initialValue ) ;
let prevValue = initialValue ;
const binding = ( ) => {
const newValue = fn ( this . _value ) ;
if ( prevValue !== newValue ) {
prevValue = newValue ;
node . textContent = newValue + "" ;
}
} ;
2019-06-14 22:43:31 +02:00
this . _addBinding ( binding ) ;
2019-06-13 00:41:45 +02:00
return node ;
}
el ( name , attributes , children ) {
2019-06-15 17:50:54 +02:00
if ( attributes && isChildren ( attributes ) ) {
children = attributes ;
attributes = null ;
2019-06-13 00:41:45 +02:00
}
const node = document . createElement ( name ) ;
2019-06-14 23:46:31 +02:00
2019-06-13 00:41:45 +02:00
if ( attributes ) {
2019-06-14 23:46:31 +02:00
this . _setNodeAttributes ( node , attributes ) ;
2019-06-13 00:41:45 +02:00
}
if ( children ) {
2019-06-14 23:46:31 +02:00
this . _setNodeChildren ( node , children ) ;
2019-06-13 00:41:45 +02:00
}
return node ;
}
2019-06-14 22:43:31 +02:00
2019-06-14 23:46:31 +02:00
_setNodeAttributes ( node , attributes ) {
for ( let [ key , value ] of Object . entries ( attributes ) ) {
const isFn = typeof value === "function" ;
// binding for className as object of className => enabled
if ( key === "className" && typeof value === "object" && value !== null ) {
2019-06-16 15:12:54 +02:00
if ( objHasFns ( value ) ) {
this . _addClassNamesBinding ( node , value ) ;
} else {
setAttribute ( node , key , classNames ( value ) ) ;
}
2019-06-14 23:46:31 +02:00
} else if ( key . startsWith ( "on" ) && key . length > 2 && isFn ) {
const eventName = key . substr ( 2 , 1 ) . toLowerCase ( ) + key . substr ( 3 ) ;
const handler = value ;
this . _addEventListener ( node , eventName , handler ) ;
} else if ( isFn ) {
this . _addAttributeBinding ( node , key , value ) ;
} else {
setAttribute ( node , key , value ) ;
}
}
}
_setNodeChildren ( node , children ) {
if ( ! Array . isArray ( children ) ) {
children = [ children ] ;
}
for ( let child of children ) {
if ( typeof child === "function" ) {
child = this . _addTextBinding ( child ) ;
} else if ( ! child . nodeType ) {
// not a DOM node, turn into text
child = text ( child ) ;
}
node . appendChild ( child ) ;
}
}
2019-06-14 22:43:31 +02:00
_addReplaceNodeBinding ( fn , renderNode ) {
let prevValue = fn ( this . _value ) ;
let node = renderNode ( null ) ;
const binding = ( ) => {
const newValue = fn ( this . _value ) ;
if ( prevValue !== newValue ) {
prevValue = newValue ;
const newNode = renderNode ( node ) ;
if ( node . parentElement ) {
node . parentElement . replaceChild ( newNode , node ) ;
}
node = newNode ;
}
} ;
this . _addBinding ( binding ) ;
return node ;
}
2020-04-29 10:00:51 +02:00
// this insert a view, and is not a view factory for `if`, so returns the root element to insert in the template
// you should not call t.view() and not use the result (e.g. attach the result to the template DOM tree).
view ( view ) {
let root ;
try {
root = view . mount ( ) ;
} catch ( err ) {
return errorToDOM ( err ) ;
}
this . _addSubView ( view ) ;
return root ;
}
// sugar
createTemplate ( render ) {
2020-04-29 10:04:40 +02:00
return vm => new TemplateView ( vm , render ) ;
2020-04-29 10:00:51 +02:00
}
2019-06-14 22:43:31 +02:00
// creates a conditional subtemplate
2020-04-29 10:00:51 +02:00
if ( fn , viewCreator ) {
2019-06-14 22:43:31 +02:00
const boolFn = value => ! ! fn ( value ) ;
return this . _addReplaceNodeBinding ( boolFn , ( prevNode ) => {
if ( prevNode && prevNode . nodeType !== Node . COMMENT _NODE ) {
2020-04-29 10:00:51 +02:00
const viewIdx = this . _subViews . findIndex ( v => v . root ( ) === prevNode ) ;
if ( viewIdx !== - 1 ) {
const [ view ] = this . _subViews . splice ( viewIdx , 1 ) ;
view . unmount ( ) ;
}
2019-06-14 22:43:31 +02:00
}
if ( boolFn ( this . _value ) ) {
2020-04-29 10:00:51 +02:00
const view = viewCreator ( this . _value ) ;
return this . view ( view ) ;
2019-06-14 22:43:31 +02:00
} else {
return document . createComment ( "if placeholder" ) ;
}
} ) ;
}
2019-06-13 00:41:45 +02:00
}
for ( const tag of TAG _NAMES ) {
2020-04-29 10:04:40 +02:00
TemplateView . prototype [ tag ] = function ( attributes , children ) {
2019-06-14 23:46:31 +02:00
return this . el ( tag , attributes , children ) ;
2019-06-13 00:41:45 +02:00
} ;
}
2020-04-29 10:04:40 +02:00
// TODO: should we an instance of something else than the view itself into the render method? That way you can't call template functions outside of the render method.
2020-04-29 10:10:33 +02:00
// methods that should be on the Template:
// el & all the tag names
// view
// if
// createTemplate
//
// all the binding stuff goes on this class, we just set the bindings on the members of the view.