diff --git a/src/Ractive.js b/src/Ractive.js
index caf8e72bf0..83d8740a96 100644
--- a/src/Ractive.js
+++ b/src/Ractive.js
@@ -11,6 +11,7 @@ import interpolators from './Ractive/static/interpolators';
import { svg, win } from './config/environment';
import proto from './Ractive/prototype';
import { extend, extendWith } from './extend/_extend';
+import { proxy } from './extend/_proxy';
import parse from './parse/_parse';
import getContext, { getNodeInfo } from './Ractive/static/getContext';
import isInstance from './Ractive/static/isInstance';
@@ -76,6 +77,7 @@ defineProperties( Ractive, {
joinKeys: { value: joinKeys },
normaliseKeypath: { value: normalise },
parse: { value: parse },
+ proxy: { value: proxy },
splitKeypath: { value: splitKeypath },
// sharedSet and styleSet are in _extend because circular refs
unescapeKey: { value: unescapeKey },
@@ -96,6 +98,7 @@ defineProperties( Ractive, {
extensions: { value: [] },
interpolators: { writable: true, value: interpolators },
partials: { writable: true, value: {} },
+ proxies: { writable: true, value: {} },
transitions: { writable: true, value: {} },
// CSS variables
diff --git a/src/Ractive/config/custom/css/css.js b/src/Ractive/config/custom/css/css.js
index 8c0efdef18..1a1cc910fa 100755
--- a/src/Ractive/config/custom/css/css.js
+++ b/src/Ractive/config/custom/css/css.js
@@ -25,28 +25,7 @@ export default {
value: new CSSModel( Child )
});
- if ( !options.css ) return;
-
- let css = typeof options.css === 'string' && !hasCurly.test( options.css ) ?
- ( getElement( options.css ) || options.css ) :
- options.css;
-
- const id = options.cssId || uuid();
-
- if ( typeof css === 'object' ) {
- css = 'textContent' in css ? css.textContent : css.innerHTML;
- } else if ( typeof css === 'function' ) {
- Child._css = options.css;
- css = evalCSS( Child, css );
- }
-
- const def = Child._cssDef = { transform: !options.noCssTransform };
-
- def.styles = def.transform ? transformCss( css, id ) : css;
- def.id = proto.cssId = id;
- Child._cssIds.push( id );
-
- addCSS( Child._cssDef );
+ if ( options.css ) initCSS( options, Child, proto );
},
// Called when creating a new component instance
@@ -90,3 +69,26 @@ export function evalCSS ( component, css ) {
const result = css.call( component, data );
return typeof result === 'string' ? result : '';
}
+
+export function initCSS ( options, target, proto ) {
+ let css = typeof options.css === 'string' && !hasCurly.test( options.css ) ?
+ ( getElement( options.css ) || options.css ) :
+ options.css;
+
+ const id = options.cssId || uuid();
+
+ if ( typeof css === 'object' ) {
+ css = 'textContent' in css ? css.textContent : css.innerHTML;
+ } else if ( typeof css === 'function' ) {
+ target._css = options.css;
+ css = evalCSS( target, css );
+ }
+
+ const def = target._cssDef = { transform: !options.noCssTransform };
+
+ def.styles = def.transform ? transformCss( css, id ) : css;
+ def.id = proto.cssId = id;
+ target._cssIds.push( id );
+
+ addCSS( target._cssDef );
+}
diff --git a/src/Ractive/config/defaults.js b/src/Ractive/config/defaults.js
index 95a3a3c244..364aad3739 100644
--- a/src/Ractive/config/defaults.js
+++ b/src/Ractive/config/defaults.js
@@ -19,7 +19,6 @@ export default {
sanitize: false,
stripComments: true,
contextLines: 0,
- parserTransforms: [],
// data & binding:
data: {},
diff --git a/src/Ractive/config/registries.js b/src/Ractive/config/registries.js
index 079d4aac6e..efe3f4f38c 100644
--- a/src/Ractive/config/registries.js
+++ b/src/Ractive/config/registries.js
@@ -9,6 +9,7 @@ const registryNames = [
'events',
'interpolators',
'partials',
+ 'proxies',
'transitions'
];
diff --git a/src/Ractive/config/runtime-parser.js b/src/Ractive/config/runtime-parser.js
index 54a6e6990b..c89a492fe2 100755
--- a/src/Ractive/config/runtime-parser.js
+++ b/src/Ractive/config/runtime-parser.js
@@ -15,7 +15,6 @@ const parseOptions = [
'sanitize',
'stripComments',
'contextLines',
- 'parserTransforms',
'allowExpressions',
'attributes'
];
diff --git a/src/Ractive/construct.js b/src/Ractive/construct.js
index 43ef262ff6..c9db1f3431 100755
--- a/src/Ractive/construct.js
+++ b/src/Ractive/construct.js
@@ -21,6 +21,7 @@ const registryNames = [
'events',
'interpolators',
'partials',
+ 'proxies',
'transitions'
];
diff --git a/src/config/types.js b/src/config/types.js
index 516a605698..502de4f1d8 100644
--- a/src/config/types.js
+++ b/src/config/types.js
@@ -12,6 +12,7 @@ export const ANCHOR = 11;
export const ATTRIBUTE = 13;
export const CLOSING_TAG = 14;
export const COMPONENT = 15;
+export const PROXY = 49;
export const YIELDER = 16;
export const INLINE_PARTIAL = 17;
export const DOCTYPE = 18;
@@ -52,3 +53,4 @@ export const DECORATOR = 71;
export const TRANSITION = 72;
export const BINDING_FLAG = 73;
export const DELEGATE_FLAG = 74;
+export const PROXY_FLAG = 75;
diff --git a/src/extend/_proxy.js b/src/extend/_proxy.js
new file mode 100644
index 0000000000..3530bdc067
--- /dev/null
+++ b/src/extend/_proxy.js
@@ -0,0 +1,25 @@
+import styleSet from '../Ractive/static/styleSet';
+import CSSModel from 'src/model/specials/CSSModel';
+import { assign, create, defineProperties, defineProperty } from 'utils/object';
+
+import { initCSS } from 'src/Ractive/config/custom/css/css';
+
+export function proxy ( fn, opts ) {
+ if ( typeof fn !== 'function' ) throw new Error( `The proxy must be a function` );
+
+ assign( fn, opts );
+
+ defineProperties( fn, {
+ extensions: { value: [] },
+ _cssIds: { value: [] },
+ cssData: { value: assign( create( this.cssData ), fn.cssData || {} ) },
+
+ styleSet: { value: styleSet.bind( fn ) }
+ });
+
+ defineProperty( fn, '_cssModel', { value: new CSSModel( fn ) } );
+
+ if ( fn.css ) initCSS( fn, fn, fn );
+
+ return fn;
+}
diff --git a/src/model/specials/CSSModel.js b/src/model/specials/CSSModel.js
index 5713ac55ec..a671557b9e 100644
--- a/src/model/specials/CSSModel.js
+++ b/src/model/specials/CSSModel.js
@@ -11,7 +11,6 @@ export default class CSSModel extends SharedModel {
if ( this.locked ) return;
const component = this.component;
-
component.extensions.forEach( e => {
const model = e._cssModel;
model.mark();
diff --git a/src/parse/_parse.js b/src/parse/_parse.js
index fb14cb8bcb..e18c6e96f0 100755
--- a/src/parse/_parse.js
+++ b/src/parse/_parse.js
@@ -1,5 +1,4 @@
import { TEMPLATE_VERSION } from 'config/template';
-import { ELEMENT } from 'config/types';
import Parser from './Parser';
import readMustache from './converters/readMustache';
import readTriple from './converters/mustache/readTriple';
@@ -17,7 +16,6 @@ import cleanup from './utils/cleanup';
import insertExpressions from './utils/insertExpressions';
import shared from '../Ractive/shared';
import { create, keys } from 'utils/object';
-import { isArray } from 'utils/is';
// See https://github.com/ractivejs/template-spec for information
// about the Ractive template specification
@@ -75,13 +73,6 @@ const StandardParser = Parser.extend({
this.allowExpressions = options.allowExpressions;
if ( options.attributes ) this.inTag = true;
-
- this.transforms = options.transforms || options.parserTransforms;
- if ( this.transforms ) {
- this.transforms = this.transforms.concat( shared.defaults.parserTransforms );
- } else {
- this.transforms = shared.defaults.parserTransforms;
- }
},
postProcess ( result ) {
@@ -96,55 +87,6 @@ const StandardParser = Parser.extend({
cleanup( result[0].t, this.stripComments, this.preserveWhitespace, !this.preserveWhitespace, !this.preserveWhitespace );
- const transforms = this.transforms;
- if ( transforms.length ) {
- const tlen = transforms.length;
- const walk = function ( fragment ) {
- let len = fragment.length;
-
- for ( let i = 0; i < len; i++ ) {
- let node = fragment[i];
-
- if ( node.t === ELEMENT ) {
- for ( let j = 0; j < tlen; j++ ) {
- const res = transforms[j].call( shared.Ractive, node );
- if ( !res ) {
- continue;
- } else if ( res.remove ) {
- fragment.splice( i--, 1 );
- len--;
- break;
- } else if ( res.replace ) {
- if ( isArray( res.replace ) ) {
- fragment.splice( i--, 1, ...res.replace );
- len += res.replace.length - 1;
- } else {
- fragment[i--] = node = res.replace;
- }
-
- break;
- }
- }
-
- // watch for partials
- if ( node.p && !isArray( node.p ) ) {
- for ( const k in node.p ) walk( node.p[k] );
- }
- }
-
- if ( node.f ) walk( node.f );
- }
- };
-
- // process the root fragment
- walk( result[0].t );
-
- // watch for root partials
- if ( result[0].p && !isArray( result[0].p ) ) {
- for ( const k in result[0].p ) walk( result[0].p[k] );
- }
- }
-
if ( this.csp !== false ) {
const expr = {};
insertExpressions( result[0].t, expr );
diff --git a/src/parse/converters/element/readAttribute.js b/src/parse/converters/element/readAttribute.js
index 263acd8fe7..1da760b87a 100755
--- a/src/parse/converters/element/readAttribute.js
+++ b/src/parse/converters/element/readAttribute.js
@@ -1,4 +1,4 @@
-import { ARRAY_LITERAL, ATTRIBUTE, DECORATOR, DELEGATE_FLAG, BINDING_FLAG, INTERPOLATOR, TRANSITION, EVENT } from '../../../config/types';
+import { ARRAY_LITERAL, ATTRIBUTE, DECORATOR, DELEGATE_FLAG, BINDING_FLAG, INTERPOLATOR, PROXY_FLAG, TRANSITION, EVENT } from '../../../config/types';
import getLowestIndex from '../utils/getLowestIndex';
import readMustache from '../readMustache';
import { decodeCharacterReferences } from 'src/utils/html';
@@ -17,7 +17,8 @@ const boundPattern = /^((bind|class)-(([-a-zA-Z0-9_])+))$/;
const directives = {
lazy: { t: BINDING_FLAG, v: 'l' },
twoway: { t: BINDING_FLAG, v: 't' },
- 'no-delegation': { t: DELEGATE_FLAG }
+ 'no-delegation': { t: DELEGATE_FLAG },
+ 'no-proxy': { t: PROXY_FLAG }
};
const unquotedAttributeValueTextPattern = /^[^\s"'=<>\/`]+/;
const proxyEvent = /^[^\s"'=<>@\[\]()]*/;
diff --git a/src/view/items/Component.js b/src/view/items/Component.js
index c5446795e7..8bbcad314e 100755
--- a/src/view/items/Component.js
+++ b/src/view/items/Component.js
@@ -5,7 +5,7 @@ import { teardown } from 'src/Ractive/prototype/teardown';
import getRactiveContext from 'shared/getRactiveContext';
import { warnIfDebug } from 'utils/log';
import { createDocumentFragment } from 'utils/dom';
-import { ANCHOR, ATTRIBUTE, BINDING_FLAG, COMPONENT, DECORATOR, EVENT, TRANSITION, YIELDER } from 'config/types';
+import { ANCHOR, ATTRIBUTE, BINDING_FLAG, COMPONENT, DECORATOR, EVENT, PROXY_FLAG, TRANSITION, YIELDER } from 'config/types';
import construct from 'src/Ractive/construct';
import initialise from 'src/Ractive/initialise';
import render from 'src/Ractive/render';
@@ -96,9 +96,10 @@ export default class Component extends Item {
}) );
break;
- case TRANSITION:
case BINDING_FLAG:
case DECORATOR:
+ case PROXY_FLAG:
+ case TRANSITION:
break;
default:
diff --git a/src/view/items/Element.js b/src/view/items/Element.js
index 2882cec0a6..fb2ca1ecec 100755
--- a/src/view/items/Element.js
+++ b/src/view/items/Element.js
@@ -1,4 +1,4 @@
-import { ATTRIBUTE, BINDING_FLAG, DECORATOR, DELEGATE_FLAG, EVENT, TRANSITION } from 'config/types';
+import { ATTRIBUTE, BINDING_FLAG, DECORATOR, DELEGATE_FLAG, EVENT, PROXY_FLAG, TRANSITION } from 'config/types';
import { win } from 'config/environment';
import { html, svg } from 'config/namespaces';
import { toArray, addToArray, removeFromArray } from 'utils/array';
@@ -70,6 +70,8 @@ export default class Element extends ContainerItem {
this.delegate = false;
break;
+ case PROXY_FLAG: break;
+
default:
( leftovers || ( leftovers = [] ) ).push( template );
break;
diff --git a/src/view/items/Partial.js b/src/view/items/Partial.js
index bcfb55988e..aaafc3ba12 100755
--- a/src/view/items/Partial.js
+++ b/src/view/items/Partial.js
@@ -195,7 +195,7 @@ export default class Partial extends MustacheContainer {
}
}
-function parsePartial( name, partial, ractive ) {
+export function parsePartial( name, partial, ractive ) {
let parsed;
try {
diff --git a/src/view/items/Proxy.js b/src/view/items/Proxy.js
new file mode 100644
index 0000000000..a84b317cde
--- /dev/null
+++ b/src/view/items/Proxy.js
@@ -0,0 +1,190 @@
+import { ContainerItem } from './shared/Item';
+import Fragment from '../Fragment';
+import { attach } from './Section';
+import getPartialTemplate from './partial/getPartialTemplate';
+import { parsePartial } from './Partial';
+import runloop from 'src/global/runloop';
+import { PROXY } from 'config/types';
+import { applyCSS } from 'src/global/css';
+import { isArray } from 'utils/is';
+import { assign, create, hasOwn, keys } from 'utils/object';
+import noop from 'utils/noop';
+
+// thingy that can supply css and template
+export default function Proxy ( options, fn ) {
+ ContainerItem.call( this, options );
+
+ // make a defensive shallow copy of the template
+ const template = this.template = assign( {}, this.template );
+
+ this.fn = fn;
+ this.type = PROXY;
+ this.name = template.e;
+
+ const handle = this.handle = this.up.getContext();
+ handle.template = template;
+ handle.proxy = this;
+ handle.refresh = refresh;
+ handle.name = template.e;
+ handle.attrs = {};
+ this.dirtyAttrs = false;
+
+ if ( !template.p ) template.p = {};
+ handle.partials = assign( {}, template.p );
+ if ( !hasOwn( template.p, 'content' ) ) template.p.content = template.f || [];
+
+ if ( isArray( fn.attributes ) ) {
+ this._attrs = {};
+
+ const me = this;
+ const invalidate = function () {
+ this.dirty = true;
+ me.dirtyAttrs = true;
+ me.bubble();
+ };
+
+ if ( isArray( template.m ) ) {
+ const attrs = template.m;
+ template.m = attrs.filter( a => !~fn.attributes.indexOf( a.n ) );
+ template.p['extra-attributes'] = template.m;
+ attrs.filter( a => ~fn.attributes.indexOf( a.n ) ).forEach( a => {
+ const fragment = new Fragment({
+ template: a.f,
+ owner: this
+ });
+ fragment.bubble = invalidate;
+ fragment.findFirstNode = noop;
+ this._attrs[ a.n ] = fragment;
+ });
+ }
+ } else {
+ template.p['extra-attributes'] = template.m;
+ }
+}
+
+const proto = Proxy.prototype = create( ContainerItem.prototype );
+
+assign( proto, {
+ constructor: Proxy,
+
+ bind () {
+ if ( !this.bound ) {
+ this.bound = true;
+
+ // bind attributes
+ if ( this._attrs ) {
+ keys( this._attrs ).forEach( k => {
+ this._attrs[k].bind();
+ });
+ this.refreshAttrs();
+ }
+
+ this.proxy = this.fn( this.handle, this.handle.attrs );
+
+ this.redo();
+
+ if ( this.fragment ) this.fragment.bind();
+ }
+ },
+
+ redo () {
+ let fragment = this.fragment;
+ const wasBound = fragment && fragment.bound;
+ const wasRendered = fragment && fragment.rendered;
+
+ if ( wasBound ) fragment.unbind();
+ if ( wasRendered ) fragment.unrender( true );
+
+ let tpl = this.proxy.template;
+ if ( !isArray( tpl ) ) {
+ if ( typeof tpl === 'string' ) {
+ tpl = getPartialTemplate( this.ractive, tpl, this.up );
+ if ( !tpl ) {
+ tpl = this.proxy.template;
+ tpl = parsePartial( tpl, tpl, this.ractive ).t;
+ }
+ } else if ( tpl && ( typeof tpl.template === 'string' || typeof isArray( tpl.t ) ) ) {
+ if ( typeof tpl.template === 'string' ) {
+ tpl = parsePartial( tpl.template, tpl.template, this.ractive ).t;
+ } else {
+ tpl = tpl.t;
+ }
+ } else if ( !isArray( tpl ) ) {
+ tpl = [];
+ }
+ }
+
+ this.fragment = fragment = new Fragment({
+ owner: this,
+ template: tpl,
+ cssIds: this.fn._cssIds
+ });
+
+ if ( wasBound ) fragment.bind();
+ if ( wasRendered ) attach( this, fragment );
+ },
+
+ refreshAttrs () {
+ keys( this._attrs ).forEach( k => {
+ this._attrs[k].update();
+ this.handle.attrs[k] = this._attrs[k].valueOf();
+ });
+ },
+
+ render ( target, occupants ) {
+ if ( this.fn._cssDef && !this.fn._cssDef.applied ) applyCSS();
+
+ this.rendered = true;
+ this.fragment.render ( target, occupants );
+ },
+
+ unbind () {
+ if ( !this.bound ) return;
+
+ this.bound = false;
+ if ( this.fragment ) this.fragment.unbind();
+
+ if ( this._attrs ) {
+ keys( this._attrs ).forEach( k => {
+ this._attrs[k].unbind();
+ });
+ }
+ },
+
+ unrender ( shouldDestroy ) {
+ if ( !this.rendered ) return;
+
+ if ( shouldDestroy && typeof this.proxy.teardown === 'function' ) this.proxy.teardown();
+
+ this.rendered = false;
+ this.fragment.unrender( shouldDestroy );
+ },
+
+ update () {
+ if ( this.dirty ) {
+ this.dirty = false;
+
+ if ( this.dirtyAttrs ) {
+ this.refreshAttrs();
+ if ( typeof this.proxy.update === 'function' ) this.proxy.update( this.handle.attrs );
+ }
+ if ( typeof this.proxy.invalidate === 'function' ) this.proxy.invalidate();
+ if ( this.fragment ) this.fragment.update();
+ }
+ }
+});
+
+function refresh () {
+ if ( this.updating ) {
+ this.proxy.redo();
+ return Promise.resolve();
+ } else {
+ const promise = runloop.start();
+
+ this.proxy.redo();
+
+ runloop.end();
+
+ return promise;
+ }
+}
diff --git a/src/view/items/Section.js b/src/view/items/Section.js
index 10f9d55eac..422401f49d 100755
--- a/src/view/items/Section.js
+++ b/src/view/items/Section.js
@@ -184,7 +184,7 @@ export default class Section extends MustacheContainer {
}
}
-function attach ( section, fragment ) {
+export function attach ( section, fragment ) {
const anchor = section.up.findNextNode( section );
if ( anchor ) {
diff --git a/src/view/items/component/getComponentConstructor.js b/src/view/items/component/getComponentConstructor.js
index aac0b65e94..104556a0a9 100644
--- a/src/view/items/component/getComponentConstructor.js
+++ b/src/view/items/component/getComponentConstructor.js
@@ -11,8 +11,8 @@ export default function getComponentConstructor ( ractive, name ) {
if ( instance ) {
Component = instance.components[ name ];
- // best test we have for not Ractive.extend
- if ( Component && !Component.Parent ) {
+ // if not from Ractive.extend, it's a function that will return a constructor
+ if ( Component && !Component.isInstance ) {
// function option, execute and store for reset
const fn = Component.bind( instance );
fn.isOwner = hasOwn( instance.components, name );
diff --git a/src/view/items/createItem.js b/src/view/items/createItem.js
index b4005d7365..aea7778717 100755
--- a/src/view/items/createItem.js
+++ b/src/view/items/createItem.js
@@ -1,5 +1,5 @@
import { ALIAS, ANCHOR, COMMENT, COMPONENT, DOCTYPE, ELEMENT, INTERPOLATOR, PARTIAL, SECTION, TRIPLE, YIELDER } from 'config/types';
-import { ATTRIBUTE, BINDING_FLAG, DECORATOR, EVENT, TRANSITION } from 'config/types';
+import { ATTRIBUTE, BINDING_FLAG, DECORATOR, EVENT, PROXY_FLAG, TRANSITION } from 'config/types';
import Alias from './Alias';
import Attribute from './element/Attribute';
import BindingFlag from './element/BindingFlag';
@@ -15,6 +15,7 @@ import Input from './element/specials/Input';
import Mapping from './component/Mapping';
import Option from './element/specials/Option';
import Partial from './Partial';
+import Proxy from './Proxy';
import Section from './Section';
import Select from './element/specials/Select';
import Textarea from './element/specials/Textarea';
@@ -24,6 +25,8 @@ import Triple from './Triple';
import getComponentConstructor from './component/getComponentConstructor';
import findElement from './shared/findElement';
+import { findInstance } from 'shared/registry';
+
const constructors = {};
constructors[ ALIAS ] = Alias;
constructors[ ANCHOR ] = Component;
@@ -51,21 +54,28 @@ const specialElements = {
};
export default function createItem ( options ) {
- if ( typeof options.template === 'string' ) {
+ const tpl = options.template;
+ const parent = options.up;
+
+ if ( typeof tpl === 'string' ) {
return new Text( options );
}
- if ( options.template.t === ELEMENT ) {
- // could be component or element
- const ComponentConstructor = getComponentConstructor( options.up.ractive, options.template.e );
- if ( ComponentConstructor ) {
- return new Component( options, ComponentConstructor );
+ if ( tpl.t === ELEMENT ) {
+ // could be proxy or component or element
+ let ctor = getComponentConstructor( parent.ractive, tpl.e );
+ if ( ctor ) {
+ return new Component( options, ctor );
+ }
+
+ ctor = getProxyConstructor( parent.ractive, tpl.e );
+ if ( ctor && ( !tpl.m || !tpl.m.find( a => a.t === PROXY_FLAG ) ) ) {
+ return new Proxy( options, ctor );
}
- const tagName = options.template.e.toLowerCase();
+ ctor = specialElements[ options.template.e.toLowerCase() ] || Element;
- const ElementConstructor = specialElements[ tagName ] || Element;
- return new ElementConstructor( options );
+ return new ctor( options );
}
let Item;
@@ -87,3 +97,8 @@ export default function createItem ( options ) {
return new Item( options );
}
+
+function getProxyConstructor ( ractive, name ) {
+ const instance = findInstance( 'proxies', ractive, name );
+ if ( instance ) return instance.proxies[ name ];
+}
diff --git a/tests/browser/init/config.js b/tests/browser/init/config.js
index 723b7f6756..2d1add92ea 100644
--- a/tests/browser/init/config.js
+++ b/tests/browser/init/config.js
@@ -20,7 +20,6 @@ export default function() {
'sanitize',
'stripComments',
'contextLines',
- 'parserTransforms',
'data',
'computed',
'syncComputedChildren',
@@ -64,7 +63,6 @@ export default function() {
'noCssTransform',
'noIntro',
'noOutro',
- 'parserTransforms',
'preserveWhitespace',
'resolveInstanceMembers',
'sanitize',
@@ -86,6 +84,7 @@ export default function() {
'events',
'interpolators',
'partials',
+ 'proxies',
'transitions'
];
diff --git a/tests/browser/parser.js b/tests/browser/parser.js
index 51b4eae685..3460452ab8 100644
--- a/tests/browser/parser.js
+++ b/tests/browser/parser.js
@@ -12,114 +12,6 @@ export default function () {
Ractive.defaults.delimiters = delimiters;
});
- test( `parser transforms can drop elements`, t => {
- const parsed = Ractive.parse( `