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( `
`, { - transforms: [ - n => n.e === 'foo' ? { remove: true } : undefined - ] - }); - - t.deepEqual( parsed, { v: 4, t: [{ t: 7, e: 'div' }] } ); - }); - - test( `parser transforms can modify an element in place`, t => { - const parsed = Ractive.parse( `
`, { - transforms: [ - n => n.e === 'div' ? ( n.m || ( n.m = [] ) ).push({ t: 13, f: 'bar', n: 'class' }) && n : undefined - ] - }); - - t.deepEqual( parsed, { v: 4, t: [{ t: 7, e: 'div', m: [{ t: 13, f: 'bar', n: 'class' }] }] } ); - }); - - test( `parser transforms can replace an element`, t => { - const parsed = Ractive.parse( ``, { - transforms: [ - function ( n ) { return n.e === 'bar' ? { replace: this.parse( `
yep
` ).t[0] } : undefined; } - ] - }); - - t.deepEqual( parsed, { v: 4, t: [{ t: 7, e: 'div', m: [{ t: 13, f: 'replaced', n: 'id' }], f: [ 'yep' ] }] } ); - }); - - test( `parser transforms can replace an element with multiple elements`, t => { - const parsed = Ractive.parse( ``, { - transforms: [ - function ( n ) { return n.e === 'bar' ? { replace: this.parse( `
yep
sure` ).t } : undefined; } - ] - }); - - t.deepEqual( parsed, { v: 4, t: [{ t: 7, e: 'div', m: [{ t: 13, f: 'replaced', n: 'id' }], f: [ 'yep' ] }, { t: 7, e: 'span', f: [ 'sure' ] }] } ); - }); - - test( `parser transforms can replace an element with multiple elements, which can also be replaced`, t => { - const parsed = Ractive.parse( ``, { - transforms: [ - function ( n ) { return n.e === 'bar' ? { replace: this.parse( `
yep
sure` ).t } : undefined; }, - function ( n ) { if ( n.e === 'baz' ) n.e = 'span'; } - ] - }); - - t.deepEqual( parsed, { v: 4, t: [{ t: 7, e: 'div', m: [{ t: 13, f: 'replaced', n: 'id' }], f: [ 'yep' ] }, { t: 7, e: 'span', f: [ 'sure' ] }] } ); - }); - - test( `parser transforms contribute to the parsed expression map`, t => { - const parsed = Ractive.parse( ``, { - transforms: [ - function ( n ) { return n.e === 'bar' ? { replace: this.parse( `
yep
` ).t[0] } : undefined; } - ] - }); - - t.deepEqual( parsed.t, [{ t: 7, e: 'div', m: [{ t: 13, f: [{ t: 2, x: { s: `""+_0`, r: ['replaced'] } }], n: 'id' }], f: [ 'yep' ] }] ); - t.ok( typeof parsed.e[`""+_0`] === 'function' ); - }); - - test( `parser transforms may be global`, t => { - Ractive.defaults.parserTransforms.push( - function ( n ) { return n.e === 'bar' ? { replace: this.parse( `
yep
` ).t[0] } : undefined; } - ); - - const parsed = Ractive.parse( `` ); - t.deepEqual( parsed, { v: 4, t: [{ t: 7, e: 'div', m: [{ t: 13, f: 'replaced', n: 'id' }], f: [ 'yep' ] }] } ); - - Ractive.defaults.parserTransforms.pop(); - }); - - test( `parser transforms include local and global options`, t => { - Ractive.defaults.parserTransforms.push( - function ( n ) { return n.e === 'bar' ? { replace: this.parse( `yep` ).t[0] } : undefined; } - ); - - const parsed = Ractive.parse( ``, { - transforms: [ - function ( n ) { if ( n.e === 'baz' ) n.e = 'div'; } - ] - }); - t.deepEqual( parsed, { v: 4, t: [{ t: 7, e: 'div', m: [{ t: 13, f: 'replaced', n: 'id' }], f: [ 'yep' ] }] } ); - - Ractive.defaults.parserTransforms.pop(); - }); - - test( `parser transforms also apply to inline partials`, t => { - const parsed = Ractive.parse( `
{{#partial foo}}{{/partial}}
`, { - transforms: [ - function ( n ) { return n.e === 'bar' ? { replace: this.parse( `
yep
` ).t[0] } : undefined; } - ] - }); - - t.deepEqual( parsed, { v: 4, t: [{ t: 7, e: 'div', p: { foo: [{ t: 7, e: 'div', m: [{ t: 13, f: 'replaced', n: 'id' }], f: [ 'yep' ] }] } }] } ); - }); - - test( `parser transforms also apply to root inline partials`, t => { - const parsed = Ractive.parse( `{{#partial foo}}{{/partial}}`, { - transforms: [ - function ( n ) { return n.e === 'bar' ? { replace: this.parse( `
yep
` ).t[0] } : undefined; } - ] - }); - - t.deepEqual( parsed, { v: 4, t: [], p: { foo: [{ t: 7, e: 'div', m: [{ t: 13, f: 'replaced', n: 'id' }], f: [ 'yep' ] }] } } ); - }); - test( `block sections with only a reference warn about non-matching closing tags (#2925)`, t => { t.expect( 1 ); diff --git a/tests/browser/proxies.js b/tests/browser/proxies.js new file mode 100644 index 0000000000..2ee5adf645 --- /dev/null +++ b/tests/browser/proxies.js @@ -0,0 +1,406 @@ +import { initModule } from '../helpers/test-config'; +import { test } from 'qunit'; +import { fire } from 'simulant'; + +export default function() { + initModule( 'proxies.js' ); + + test( `basic proxy`, t => { + new Ractive({ + target: fixture, + template: '', + proxies: { + proxy: Ractive.proxy( () => ({ template: [ 'proxy' ] }) ) + } + }); + + t.htmlEqual( fixture.innerHTML, 'proxy' ); + }); + + test( `proxies and sections`, t => { + const r = new Ractive({ + target: fixture, + template: `ab{{#if foo}}cd{{else}}e{{/if}}{{#each list}}{{/each}}{{#each list}}g{{/each}}{{#each list}}h{{/each}}`, + data: { + list: [ 0 ], + foo: true + }, + proxies: { + proxy: Ractive.proxy( () => ({ template: [ 'z' ] }) ) + } + }); + + t.htmlEqual( fixture.innerHTML, 'azbczdzzgzzh' ); + + r.toggle( 'foo' ); + + t.htmlEqual( fixture.innerHTML, 'azbzezzzgzzh' ); + }); + + test( `proxy claimed attributes`, t => { + const r = new Ractive({ + target: fixture, + template: '', + proxies: { + proxy: Ractive.proxy( + ( handle, attrs ) => { + t.equal( Object.keys( attrs ).length, 1 ); + t.equal( attrs.name, 'joe' ); + t.equal( handle.template.m.length, 1 ); + t.equal( handle.template.m[0].n, 'value' ); + + return { + template: [{ t: 7, e: 'input', m: handle.template.m }] + }; + }, + { + attributes: [ 'name' ] + } + ) + } + }); + + t.htmlEqual( fixture.innerHTML, '' ); + + const input = r.find( 'input' ); + input.value = 'larry'; + fire( input, 'change' ); + + t.equal( r.get( 'foo' ), 'larry' ); + }); + + test( `updating claimed proxy attributes`, t => { + const obj = { hello: 'world' }; + + const r = new Ractive({ + target: fixture, + template: ``, + data: { + foo: 10, + bar: obj + }, + proxies: { + proxy: Ractive.proxy( + ( handle, attrs ) => { + t.equal( Object.keys( attrs ).length, 2 ); + t.strictEqual( attrs.name, 10 ); + t.strictEqual( attrs.value, obj ); + return { + template: [{ t: 7, e: 'button', m: handle.template.m }], + update ( attrs ) { + t.equal( JSON.stringify( attrs.name ), JSON.stringify( [ 'test' ] ) ); + t.strictEqual( attrs.value, '42' ); + } + }; + }, + { + attributes: [ 'name', 'value' ] + } + ) + } + }); + + t.htmlEqual( fixture.innerHTML, '' ); + + r.set({ + foo: [ 'test' ], + bar: '42' + }); + }); + + test( `shuffling claimed proxy attributes`, t => { + t.expect( 0 ); + + const r = new Ractive({ + target: fixture, + template: `{{#each list}}{{/each}}`, + data: { + list: [ 0 ] + }, + proxies: { + proxy: Ractive.proxy( + () => { + return { + template: [], + update () { + t.ok( false, 'should not update on shuffle' ); + } + }; + }, + { + attributes: [ 'val' ] + } + ) + } + }); + + r.unshift( 'list', 1 ); + }); + + test( `proxy content partial`, t => { + new Ractive({ + target: fixture, + template: 'proxy1nope{{#partial content}}proxy2{{/partial}}', + proxies: { + proxy: Ractive.proxy( + () => ({ template: '{{>content}}' }) + ) + } + }); + + t.htmlEqual( fixture.innerHTML, 'proxy1proxy2' ); + }); + + test( `proxy unclaimed attributes partial`, t => { + new Ractive({ + target: fixture, + template: ``, + proxies: { + proxy1: Ractive.proxy( + handle => { + t.equal( handle.template.m.length, 1 ); + return { template: '
extra-attributes}} />' }; + }, + { attributes: [ 'name' ] } + ), + proxy2: Ractive.proxy( + handle => { + t.equal( handle.template.m.length, 2 ); + return { template: 'extra-attributes}} />' }; + } + ) + } + }); + + t.htmlEqual( fixture.innerHTML, '
' ); + }); + + test( `optional proxy invalidate callback`, t => { + t.expect( 1 ); + + const r = new Ractive({ + target: fixture, + template: `{{#if bar}}...{{/if}}{{#if foo}}...{{/if}}`, + data: { + foo: true, + bar: true + }, + proxies: { + proxy: Ractive.proxy( + () => { + return { + template: '{{>content}}', + invalidate () { + t.ok( true ); + } + }; + } + ) + } + }); + + r.toggle( 'foo' ); + r.toggle( 'bar' ); + }); + + test( `proxy refresh`, t => { + let handle; + const proxy = {}; + + new Ractive({ + target: fixture, + template: '', + proxies: { + proxy: Ractive.proxy( h => { + handle = h; + return proxy; + }) + } + }); + + const script = document.createElement( 'script' ); + script.setAttribute( 'type', 'text/html' ); + script.setAttribute( 'id', 'proxy-template' ); + script.textContent = 'hello'; + document.body.appendChild( script ); + + t.htmlEqual( fixture.innerHTML, '' ); + + proxy.template = '#proxy-template'; + handle.refresh(); + t.htmlEqual( fixture.innerHTML, 'hello' ); + + proxy.template = 'testing'; + handle.refresh(); + t.htmlEqual( fixture.innerHTML, 'testing' ); + + proxy.template = { template: 'partial style obj' }; + handle.refresh(); + t.htmlEqual( fixture.innerHTML, 'partial style obj' ); + + proxy.template = { t: [ 'other partial' ] }; + handle.refresh(); + t.htmlEqual( fixture.innerHTML, 'other partial' ); + + proxy.template = [ 'direct template' ]; + handle.refresh(); + t.htmlEqual( fixture.innerHTML, 'direct template' ); + + document.body.removeChild( script ); + }); + + test( `proxy progressive enhancement`, t => { + fixture.innerHTML = `
hello
`; + const div = fixture.childNodes[0]; + + new Ractive({ + target: fixture, + enhance: true, + template: '', + proxies: { + proxy: Ractive.proxy( () => ({ template: '
hello
' }) ) + } + }); + + t.ok( fixture.childNodes.length === 1 ); + t.ok( fixture.childNodes[0] === div ); + }); + + test( `proxy kept set`, t => { + const r = new Ractive({ + target: fixture, + template: '{{#if foo}}{{/if}}', + data: { + foo: true + }, + proxies: { + proxy: Ractive.proxy( () => ({ template: '
hello
' }) ) + } + }); + + const div = fixture.childNodes[0]; + + r.toggle( 'foo', { keep: true } ); + r.toggle( 'foo' ); + + t.ok( fixture.childNodes.length === 1 ); + t.ok( fixture.childNodes[0] === div ); + }); + + test( `proxy in component`, t => { + new Ractive({ + target: fixture, + template: '
', + components: { + cmp: Ractive.extend({ + template: '
', + proxies: { + div: Ractive.proxy( () => ({ template: '' }) ) + } + }) + } + }); + + t.htmlEqual( fixture.innerHTML, '
' ); + }); + + test( `proxy teardown callback`, t => { + let up = 0; + let down = 0; + + const r = new Ractive({ + target: fixture, + template: `{{#if foo}}{{/if}}`, + proxies: { + proxy: Ractive.proxy( + () => ++up && { + template: 'yep', + teardown () { down++; } + } + ) + } + }); + + r.toggle( 'foo' ); + + t.equal( up, 1 ); + t.equal( down, 0 ); + + r.toggle( 'foo' ); + + t.equal( up, 1 ); + t.equal( down, 1 ); + + r.toggle( 'foo' ); + + t.equal( up, 2 ); + t.equal( down, 1 ); + + r.toggle( 'foo' ); + + t.equal( up, 2 ); + t.equal( down, 2 ); + }); + + test( `proxy css`, t => { + const r = new Ractive({ + target: fixture, + template: ``, + proxies: { + proxy: Ractive.proxy( + () => ({ template: '
' }), + { + css: 'div { width: 123px; }' + } + ) + } + }); + + t.equal( r.find( 'div' ).clientWidth, 123 ); + }); + + test( `proxy css no transform`, t => { + const r = new Ractive({ + target: fixture, + template: `
`, + proxies: { + proxy: Ractive.proxy( + () => ({ template: [ 'yep' ] }), + { + css: '.proxy-css-no-transforms { width: 123px; }', + noCssTransform: true + } + ) + } + }); + + t.equal( r.find( 'div' ).clientWidth, 123 ); + }); + + test( `proxy css fn`, t => { + const proxy = Ractive.proxy( + () => ({ template: '
' }), + { + css ( data ) { + return `div { width: ${data('width')}; }`; + } + } + ); + + proxy.styleSet( 'width', '123px' ); + + const r = new Ractive({ + target: fixture, + template: ``, + proxies: { + proxy + } + }); + + t.equal( r.find( 'div' ).clientWidth, 123 ); + + proxy.styleSet( 'width', '124px' ); + + t.equal( r.find( 'div' ).clientWidth, 124 ); + }); +}