@webqit/oohtml
Version:
A suite of new DOM features that brings language support for modern UI development paradigms: a component-based architecture, data binding, and reactivity.
166 lines (158 loc) • 7.7 kB
JavaScript
/**
* @imports
*/
import DOMBindingsContext from './DOMBindingsContext.js';
import { _, _init, _splitOuter } from '../util.js';
/**
* @init
*
* @param Object $config
*/
export default function init( $config = {} ) {
const { config, window } = _init.call( this, 'bindings-api', $config, {
attr: { bindingsreflection: 'bindings' },
api: { bind: 'bind', bindings: 'bindings', },
} );
window.webqit.DOMBindingsContext = DOMBindingsContext;
exposeAPIs.call( window, config );
realtime.call(window, config);
}
/**
* @Defs
*
* The internal bindings object
* within elements and the document object.
*/
function getBindings( config, node ) {
const window = this, { webqit: { Observer, oohtml: { configs: { CONTEXT_API: ctxConfig } } } } = window;
if ( !_( node ).has( 'bindings' ) ) {
const bindingsObj = Object.create( null );
_( node ).set( 'bindings', bindingsObj );
Observer.observe( bindingsObj, mutations => {
if ( node instanceof window.Element ) {
const bindingsParse = parseBindingsAttr( node.getAttribute( config.attr.bindingsreflection ) || '' );
const bindingsParseBefore = new Map(bindingsParse);
for ( const m of mutations ) {
if ( m.detail?.publish !== false ) {
if ( m.type === 'delete' ) bindingsParse.delete( m.key );
else bindingsParse.set( m.key, undefined );
}
}
if ( bindingsParse.size && bindingsParse.size !== bindingsParseBefore.size ) {
node.setAttribute( config.attr.bindingsreflection, `{ ${ [ ...bindingsParse.entries() ].map(([ key, value ]) => value === undefined ? key : `${ key }: ${ value }` ).join( ', ' ) } }` );
} else if ( !bindingsParse.size ) node.toggleAttribute( config.attr.bindingsreflection, false );
} else {
const contextsApi = node[ ctxConfig.api.contexts ];
for ( const m of mutations ) {
if ( m.type === 'delete' ) {
const ctx = contextsApi.find( DOMBindingsContext.kind, m.key );
if ( ctx ) contextsApi.detach( ctx );
} else if ( !contextsApi.find( DOMBindingsContext.kind, m.key ) ) {
contextsApi.attach( new DOMBindingsContext( m.key ) );
}
}
}
} );
}
return _( node ).get( 'bindings' );
}
/**
* Exposes Bindings with native APIs.
*
* @param Object config
*
* @return Void
*/
function exposeAPIs( config ) {
const window = this, { webqit: { Observer } } = window;
// The Bindings APIs
[ window.Document.prototype, window.Element.prototype, window.ShadowRoot.prototype ].forEach( prototype => {
// No-conflict assertions
const type = prototype === window.Document.prototype ? 'Document' : ( prototype === window.ShadowRoot.prototype ? 'ShadowRoot' : 'Element' );
if ( config.api.bind in prototype ) { throw new Error( `The ${ type } prototype already has a "${ config.api.bind }" API!` ); }
if ( config.api.bindings in prototype ) { throw new Error( `The ${ type } prototype already has a "${ config.api.bindings }" API!` ); }
// Definitions
Object.defineProperty( prototype, config.api.bind, { value: function( bindings, options = {} ) {
return applyBindings.call( window, config, this, bindings, options );
} });
Object.defineProperty( prototype, config.api.bindings, { get: function() {
return Observer.proxy( getBindings.call( window, config, this ) );
} } );
} );
}
/**
* Exposes Bindings with native APIs.
*
* @param Object config
* @param document|Element target
* @param Object bindings
* @param Object params
*
* @return Void
*/
function applyBindings( config, target, bindings, { merge, diff, publish, namespace } = {} ) {
const window = this, { webqit: { Observer } } = window;
const bindingsObj = getBindings.call( this, config, target );
const $params = { diff, namespace, detail: { publish } };
const exitingKeys = merge ? [] : Observer.ownKeys( bindingsObj, $params ).filter( key => !( key in bindings ) );
return Observer.batch( bindingsObj, () => {
if ( exitingKeys.length ) { Observer.deleteProperties( bindingsObj, exitingKeys, $params ); }
return Observer.set( bindingsObj, bindings, $params );
}, $params );
}
/**
* Performs realtime capture of elements and their attributes
* and their module query results; then resolves the respective import elements.
*
* @param Object config
*
* @return Void
*/
function realtime(config) {
const window = this, { webqit: { realdom, Observer, oohtml: { configs } } } = window;
// ------------
const attachBindingsContext = (host, key) => {
const contextsApi = host[configs.CONTEXT_API.api.contexts];
if ( !contextsApi.find( DOMBindingsContext.kind, key ) ) {
contextsApi.attach( new DOMBindingsContext( key ) );
}
};
const detachBindingsContext = (host, key) => {
let ctx, contextsApi = host[configs.CONTEXT_API.api.contexts];
while( ctx = contextsApi.find( DOMBindingsContext.kind, key ) ) contextsApi.detach(ctx);
};
// ------------
realdom.realtime(window.document).query( `[${window.CSS.escape(config.attr.bindingsreflection)}]`, record => {
record.exits.forEach( entry => detachBindingsContext( entry ) );
record.entrants.forEach(entry => {
const bindingsParse = parseBindingsAttr( entry.getAttribute( config.attr.bindingsreflection ) || '' );
const newData = [ ...bindingsParse.entries() ].filter(([ k, v ]) => v !== undefined );
if ( newData.length ) entry[ config.api.bind ]( Object.fromEntries( newData ), { merge: true, publish: false } );
for ( const [ key ] of bindingsParse ) {
attachBindingsContext( entry, key );
}
} );
}, { id: 'bindings:dom', live: true, subtree: 'cross-roots', timing: 'sync', eventDetails: true });
realdom.realtime( window.document, 'attr' ).observe( config.attr.bindingsreflection, record => {
const bindingsObj = getBindings.call( window, config, record.target );
const bindingsParse = parseBindingsAttr( record.value || '' );
const oldBindings = parseBindingsAttr( record.oldValue || '' );
for ( const key of new Set([ ...bindingsParse.keys(), ...oldBindings.keys() ]) ) {
if ( !oldBindings.has( key ) ) {
if ( bindingsParse.get( key ) !== undefined ) Observer.set( bindingsObj, key, bindingsParse.get( key ), { detail: { publish: false } } );
attachBindingsContext( record.target, key );
} else if ( !bindingsParse.has( key ) ) {
if ( oldBindings.get( key ) !== undefined ) Observer.deleteProperty( bindingsObj, key, { detail: { publish: false } } );
detachBindingsContext( record.target, key );
} else if ( bindingsParse.get( key ) !== oldBindings.get( key ) ) {
Observer.set( bindingsObj, key, bindingsParse.get( key ), { detail: { publish: false } } );
}
}
}, { id: 'bindings:attr', subtree: 'cross-roots', timing: 'sync', newValue: true, oldValue: true } );
}
const parseBindingsAttr = str => {
str = str.trim();
return new Map(_splitOuter( str.slice(1, -1), ',' ).filter( s => s.trim() ).map( _str => {
return _splitOuter( _str, ':' ).map( s => s.trim() );
}));
};