From 480a822bd6f057e50d7cdefa779f2b075a623aa3 Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Sun, 11 Dec 2016 18:45:36 -0800 Subject: [PATCH] Deprecate component lifecycle hook arguments --- .../lib/syntax/curly-component.js | 5 +- .../integration/components/life-cycle-test.js | 129 +++++++++--------- packages/ember-views/lib/index.js | 2 +- .../ember-views/lib/mixins/view_support.js | 86 +++++++++++- 4 files changed, 149 insertions(+), 73 deletions(-) diff --git a/packages/ember-glimmer/lib/syntax/curly-component.js b/packages/ember-glimmer/lib/syntax/curly-component.js index f94861bb564..13e1ba78f17 100644 --- a/packages/ember-glimmer/lib/syntax/curly-component.js +++ b/packages/ember-glimmer/lib/syntax/curly-component.js @@ -23,6 +23,7 @@ import { _instrumentStart } from 'ember-metal'; import { + dispatchLifeCycleHook, setViewElement } from 'ember-views'; import { @@ -342,8 +343,8 @@ class CurlyComponentManager { component.setProperties(props); component[IS_DISPATCHING_ATTRS] = false; - component.trigger('didUpdateAttrs', { oldAttrs, newAttrs }); - component.trigger('didReceiveAttrs', { oldAttrs, newAttrs }); + dispatchLifeCycleHook(component, 'didUpdateAttrs', oldAttrs, newAttrs); + dispatchLifeCycleHook(component, 'didReceiveAttrs', oldAttrs, newAttrs); } if (environment.isInteractive) { diff --git a/packages/ember-glimmer/tests/integration/components/life-cycle-test.js b/packages/ember-glimmer/tests/integration/components/life-cycle-test.js index 16c3fd2fca5..b505d049721 100644 --- a/packages/ember-glimmer/tests/integration/components/life-cycle-test.js +++ b/packages/ember-glimmer/tests/integration/components/life-cycle-test.js @@ -307,22 +307,22 @@ class LifeCycleHooksTest extends RenderingTest { // Sync hooks ['the-top', 'init'], - ['the-top', 'didInitAttrs', { attrs: topAttrs }], - ['the-top', 'didReceiveAttrs', { newAttrs: topAttrs }], + ['the-top', 'didInitAttrs', { attrs: topAttrs, newAttrs: topAttrs }], + ['the-top', 'didReceiveAttrs', { attrs: topAttrs, newAttrs: topAttrs }], ['the-top', 'on(init)'], ['the-top', 'willRender'], ['the-top', 'willInsertElement'], ['the-middle', 'init'], - ['the-middle', 'didInitAttrs', { attrs: middleAttrs }], - ['the-middle', 'didReceiveAttrs', { newAttrs: middleAttrs }], + ['the-middle', 'didInitAttrs', { attrs: middleAttrs, newAttrs: middleAttrs }], + ['the-middle', 'didReceiveAttrs', { attrs: middleAttrs, newAttrs: middleAttrs }], ['the-middle', 'on(init)'], ['the-middle', 'willRender'], ['the-middle', 'willInsertElement'], ['the-bottom', 'init'], - ['the-bottom', 'didInitAttrs', { attrs: bottomAttrs }], - ['the-bottom', 'didReceiveAttrs', { newAttrs: bottomAttrs }], + ['the-bottom', 'didInitAttrs', { attrs: bottomAttrs, newAttrs: bottomAttrs }], + ['the-bottom', 'didReceiveAttrs', { attrs: bottomAttrs, newAttrs: bottomAttrs }], ['the-bottom', 'on(init)'], ['the-bottom', 'willRender'], ['the-bottom', 'willInsertElement'], @@ -343,18 +343,18 @@ class LifeCycleHooksTest extends RenderingTest { nonInteractive: [ // Sync hooks ['the-top', 'init'], - ['the-top', 'didInitAttrs', { attrs: topAttrs }], - ['the-top', 'didReceiveAttrs', { newAttrs: topAttrs }], + ['the-top', 'didInitAttrs', { attrs: topAttrs, newAttrs: topAttrs }], + ['the-top', 'didReceiveAttrs', { attrs: topAttrs, newAttrs: topAttrs }], ['the-top', 'on(init)'], ['the-middle', 'init'], - ['the-middle', 'didInitAttrs', { attrs: middleAttrs }], - ['the-middle', 'didReceiveAttrs', { newAttrs: middleAttrs }], + ['the-middle', 'didInitAttrs', { attrs: middleAttrs, newAttrs: middleAttrs }], + ['the-middle', 'didReceiveAttrs', { attrs: middleAttrs, newAttrs: middleAttrs }], ['the-middle', 'on(init)'], ['the-bottom', 'init'], - ['the-bottom', 'didInitAttrs', { attrs: bottomAttrs }], - ['the-bottom', 'didReceiveAttrs', { newAttrs: bottomAttrs }], + ['the-bottom', 'didInitAttrs', { attrs: bottomAttrs, newAttrs: bottomAttrs }], + ['the-bottom', 'didReceiveAttrs', { attrs: bottomAttrs, newAttrs: bottomAttrs }], ['the-bottom', 'on(init)'] ] }); @@ -452,7 +452,7 @@ class LifeCycleHooksTest extends RenderingTest { // the new attribute to rerender itself imperatively, that would result // in lifecycle hooks being invoked for the child. - topAttrs = { oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@horsetomdale' } }; + topAttrs = { attrs: { twitter: '@horsetomdale' }, oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@horsetomdale' } }; this.assertHooks({ label: 'after update', @@ -551,30 +551,30 @@ class LifeCycleHooksTest extends RenderingTest { // Sync hooks ['the-parent', 'init'], - ['the-parent', 'didInitAttrs', { attrs: parentAttrs }], - ['the-parent', 'didReceiveAttrs', { newAttrs: parentAttrs }], + ['the-parent', 'didInitAttrs', { attrs: parentAttrs, newAttrs: parentAttrs }], + ['the-parent', 'didReceiveAttrs', { attrs: parentAttrs, newAttrs: parentAttrs }], ['the-parent', 'on(init)'], ['the-parent', 'willRender'], ['the-parent', 'willInsertElement'], ['the-first-child', 'init'], - ['the-first-child', 'didInitAttrs', { attrs: firstAttrs }], - ['the-first-child', 'didReceiveAttrs', { newAttrs: firstAttrs }], + ['the-first-child', 'didInitAttrs', { attrs: firstAttrs, newAttrs: firstAttrs }], + ['the-first-child', 'didReceiveAttrs', { attrs: firstAttrs, newAttrs: firstAttrs }], ['the-first-child', 'on(init)'], ['the-first-child', 'willRender'], ['the-first-child', 'willInsertElement'], ['the-second-child', 'init'], - ['the-second-child', 'didInitAttrs', { attrs: secondAttrs }], - ['the-second-child', 'didReceiveAttrs', { newAttrs: secondAttrs }], + ['the-second-child', 'didInitAttrs', { attrs: secondAttrs, newAttrs: secondAttrs }], + ['the-second-child', 'didReceiveAttrs', { attrs: secondAttrs, newAttrs: secondAttrs }], ['the-second-child', 'on(init)'], ['the-second-child', 'willRender'], ['the-second-child', 'willInsertElement'], ['the-last-child', 'init'], - ['the-last-child', 'didInitAttrs', { attrs: lastAttrs }], - ['the-last-child', 'didReceiveAttrs', { newAttrs: lastAttrs }], + ['the-last-child', 'didInitAttrs', { attrs: lastAttrs, newAttrs: lastAttrs }], + ['the-last-child', 'didReceiveAttrs', { attrs: lastAttrs, newAttrs: lastAttrs }], ['the-last-child', 'on(init)'], ['the-last-child', 'willRender'], ['the-last-child', 'willInsertElement'], @@ -598,23 +598,23 @@ class LifeCycleHooksTest extends RenderingTest { // Sync hooks ['the-parent', 'init'], - ['the-parent', 'didInitAttrs', { attrs: parentAttrs }], - ['the-parent', 'didReceiveAttrs', { newAttrs: parentAttrs }], + ['the-parent', 'didInitAttrs', { attrs: parentAttrs, newAttrs: parentAttrs }], + ['the-parent', 'didReceiveAttrs', { attrs: parentAttrs, newAttrs: parentAttrs }], ['the-parent', 'on(init)'], ['the-first-child', 'init'], - ['the-first-child', 'didInitAttrs', { attrs: firstAttrs }], - ['the-first-child', 'didReceiveAttrs', { newAttrs: firstAttrs }], + ['the-first-child', 'didInitAttrs', { attrs: firstAttrs, newAttrs: firstAttrs }], + ['the-first-child', 'didReceiveAttrs', { attrs: firstAttrs, newAttrs: firstAttrs }], ['the-first-child', 'on(init)'], ['the-second-child', 'init'], - ['the-second-child', 'didInitAttrs', { attrs: secondAttrs }], - ['the-second-child', 'didReceiveAttrs', { newAttrs: secondAttrs }], + ['the-second-child', 'didInitAttrs', { attrs: secondAttrs, newAttrs: secondAttrs }], + ['the-second-child', 'didReceiveAttrs', { attrs: secondAttrs, newAttrs: secondAttrs }], ['the-second-child', 'on(init)'], ['the-last-child', 'init'], - ['the-last-child', 'didInitAttrs', { attrs: lastAttrs }], - ['the-last-child', 'didReceiveAttrs', { newAttrs: lastAttrs }], + ['the-last-child', 'didInitAttrs', { attrs: lastAttrs, newAttrs: lastAttrs }], + ['the-last-child', 'didReceiveAttrs', { attrs: lastAttrs, newAttrs: lastAttrs }], ['the-last-child', 'on(init)'] ] }); @@ -734,12 +734,13 @@ class LifeCycleHooksTest extends RenderingTest { this.assertText('Twitter: @horsetomdale|Name: Horse Tom Dale|Website: horsetomdale.net'); parentAttrs = { + attrs: { twitter: '@horsetomdale', name: 'Horse Tom Dale', website: 'horsetomdale.net' }, oldAttrs: { twitter: '@tomdale', name: 'Tom Dale', website: 'tomdale.net' }, newAttrs: { twitter: '@horsetomdale', name: 'Horse Tom Dale', website: 'horsetomdale.net' } }; - firstAttrs = { oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@horsetomdale' } }; - secondAttrs = { oldAttrs: { name: 'Tom Dale' }, newAttrs: { name: 'Horse Tom Dale' } }; - lastAttrs = { oldAttrs: { website: 'tomdale.net' }, newAttrs: { website: 'horsetomdale.net' } }; + firstAttrs = { attrs: { twitter: '@horsetomdale' }, oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@horsetomdale' } }; + secondAttrs = { attrs: { name: 'Horse Tom Dale' }, oldAttrs: { name: 'Tom Dale' }, newAttrs: { name: 'Horse Tom Dale' } }; + lastAttrs = { attrs: { website: 'horsetomdale.net' }, oldAttrs: { website: 'tomdale.net' }, newAttrs: { website: 'horsetomdale.net' } }; this.assertHooks({ label: 'after update', @@ -877,22 +878,22 @@ class LifeCycleHooksTest extends RenderingTest { // Sync hooks ['the-top', 'init'], - ['the-top', 'didInitAttrs', { attrs: topAttrs }], - ['the-top', 'didReceiveAttrs', { newAttrs: topAttrs }], + ['the-top', 'didInitAttrs', { attrs: topAttrs, newAttrs: topAttrs }], + ['the-top', 'didReceiveAttrs', { attrs: topAttrs, newAttrs: topAttrs }], ['the-top', 'on(init)'], ['the-top', 'willRender'], ['the-top', 'willInsertElement'], ['the-middle', 'init'], - ['the-middle', 'didInitAttrs', { attrs: middleAttrs }], - ['the-middle', 'didReceiveAttrs', { newAttrs: middleAttrs }], + ['the-middle', 'didInitAttrs', { attrs: middleAttrs, newAttrs: middleAttrs }], + ['the-middle', 'didReceiveAttrs', { attrs: middleAttrs, newAttrs: middleAttrs }], ['the-middle', 'on(init)'], ['the-middle', 'willRender'], ['the-middle', 'willInsertElement'], ['the-bottom', 'init'], - ['the-bottom', 'didInitAttrs', { attrs: bottomAttrs }], - ['the-bottom', 'didReceiveAttrs', { newAttrs: bottomAttrs }], + ['the-bottom', 'didInitAttrs', { attrs: bottomAttrs, newAttrs: bottomAttrs }], + ['the-bottom', 'didReceiveAttrs', { attrs: bottomAttrs, newAttrs: bottomAttrs }], ['the-bottom', 'on(init)'], ['the-bottom', 'willRender'], ['the-bottom', 'willInsertElement'], @@ -913,18 +914,18 @@ class LifeCycleHooksTest extends RenderingTest { // Sync hooks ['the-top', 'init'], - ['the-top', 'didInitAttrs', { attrs: topAttrs }], - ['the-top', 'didReceiveAttrs', { newAttrs: topAttrs }], + ['the-top', 'didInitAttrs', { attrs: topAttrs, newAttrs: topAttrs }], + ['the-top', 'didReceiveAttrs', { attrs: topAttrs, newAttrs: topAttrs }], ['the-top', 'on(init)'], ['the-middle', 'init'], - ['the-middle', 'didInitAttrs', { attrs: middleAttrs }], - ['the-middle', 'didReceiveAttrs', { newAttrs: middleAttrs }], + ['the-middle', 'didInitAttrs', { attrs: middleAttrs, newAttrs: middleAttrs }], + ['the-middle', 'didReceiveAttrs', { attrs: middleAttrs, newAttrs: middleAttrs }], ['the-middle', 'on(init)'], ['the-bottom', 'init'], - ['the-bottom', 'didInitAttrs', { attrs: bottomAttrs }], - ['the-bottom', 'didReceiveAttrs', { newAttrs: bottomAttrs }], + ['the-bottom', 'didInitAttrs', { attrs: bottomAttrs, newAttrs: bottomAttrs }], + ['the-bottom', 'didReceiveAttrs', { attrs: bottomAttrs, newAttrs: bottomAttrs }], ['the-bottom', 'on(init)'] ] }); @@ -936,9 +937,9 @@ class LifeCycleHooksTest extends RenderingTest { // Because the `twitter` attr is used by the all of the components, // the lifecycle hooks are invoked for all components. - topAttrs = { oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@horsetomdale' } }; - middleAttrs = { oldAttrs: { twitterTop: '@tomdale' }, newAttrs: { twitterTop: '@horsetomdale' } }; - bottomAttrs = { oldAttrs: { twitterMiddle: '@tomdale' }, newAttrs: { twitterMiddle: '@horsetomdale' } }; + topAttrs = { attrs: { twitter: '@horsetomdale' }, oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@horsetomdale' } }; + middleAttrs = { attrs: { twitterTop: '@horsetomdale' }, oldAttrs: { twitterTop: '@tomdale' }, newAttrs: { twitterTop: '@horsetomdale' } }; + bottomAttrs = { attrs: { twitterMiddle: '@horsetomdale' }, oldAttrs: { twitterMiddle: '@tomdale' }, newAttrs: { twitterMiddle: '@horsetomdale' } }; this.assertHooks({ label: 'after updating (root)', @@ -996,9 +997,9 @@ class LifeCycleHooksTest extends RenderingTest { // In this case, because the attrs are passed down, all child components are invoked. - topAttrs = { oldAttrs: { twitter: '@horsetomdale' }, newAttrs: { twitter: '@horsetomdale' } }; - middleAttrs = { oldAttrs: { twitterTop: '@horsetomdale' }, newAttrs: { twitterTop: '@horsetomdale' } }; - bottomAttrs = { oldAttrs: { twitterMiddle: '@horsetomdale' }, newAttrs: { twitterMiddle: '@horsetomdale' } }; + topAttrs = { attrs: { twitter: '@horsetomdale' }, oldAttrs: { twitter: '@horsetomdale' }, newAttrs: { twitter: '@horsetomdale' } }; + middleAttrs = { attrs: { twitterTop: '@horsetomdale' }, oldAttrs: { twitterTop: '@horsetomdale' }, newAttrs: { twitterTop: '@horsetomdale' } }; + bottomAttrs = { attrs: { twitterMiddle: '@horsetomdale' }, oldAttrs: { twitterMiddle: '@horsetomdale' }, newAttrs: { twitterMiddle: '@horsetomdale' } }; this.assertHooks({ label: 'after no-op rernder (root)', @@ -1065,8 +1066,8 @@ class LifeCycleHooksTest extends RenderingTest { let initialHooks = (count) => { let ret = [ ['an-item', 'init'], - ['an-item', 'didInitAttrs', { attrs: { count } }], - ['an-item', 'didReceiveAttrs', { newAttrs: { count } }], + ['an-item', 'didInitAttrs', { attrs: { count }, newAttrs: { count } }], + ['an-item', 'didReceiveAttrs', { attrs: { count }, newAttrs: { count } }], ['an-item', 'on(init)'] ]; if (this.isInteractive) { @@ -1077,8 +1078,8 @@ class LifeCycleHooksTest extends RenderingTest { } ret.push( ['nested-item', 'init'], - ['nested-item', 'didInitAttrs', { attrs: { } }], - ['nested-item', 'didReceiveAttrs', { newAttrs: { } }], + ['nested-item', 'didInitAttrs', { attrs: { }, newAttrs: { } }], + ['nested-item', 'didReceiveAttrs', { attrs: { }, newAttrs: { } }], ['nested-item', 'on(init)'] ); if (this.isInteractive) { @@ -1179,16 +1180,16 @@ class LifeCycleHooksTest extends RenderingTest { ['nested-item', 'willClearRender'], ['no-items', 'init'], - ['no-items', 'didInitAttrs', { attrs: { } }], - ['no-items', 'didReceiveAttrs', { newAttrs: { } }], + ['no-items', 'didInitAttrs', { attrs: { }, newAttrs: { } }], + ['no-items', 'didReceiveAttrs', { attrs: { }, newAttrs: { } }], ['no-items', 'on(init)'], ['no-items', 'willRender'], ['no-items', 'willInsertElement'], ['nested-item', 'init'], - ['nested-item', 'didInitAttrs', { attrs: { } }], - ['nested-item', 'didReceiveAttrs', { newAttrs: { } }], + ['nested-item', 'didInitAttrs', { attrs: { }, newAttrs: { } }], + ['nested-item', 'didReceiveAttrs', { attrs: { }, newAttrs: { } }], ['nested-item', 'on(init)'], ['nested-item', 'willRender'], ['nested-item', 'willInsertElement'], @@ -1223,13 +1224,13 @@ class LifeCycleHooksTest extends RenderingTest { nonInteractive: [ ['no-items', 'init'], - ['no-items', 'didInitAttrs', { attrs: { } }], - ['no-items', 'didReceiveAttrs', { newAttrs: { } }], + ['no-items', 'didInitAttrs', { attrs: { }, newAttrs: { } }], + ['no-items', 'didReceiveAttrs', { attrs: { }, newAttrs: { } }], ['no-items', 'on(init)'], ['nested-item', 'init'], - ['nested-item', 'didInitAttrs', { attrs: { } }], - ['nested-item', 'didReceiveAttrs', { newAttrs: { } }], + ['nested-item', 'didInitAttrs', { attrs: { }, newAttrs: { } }], + ['nested-item', 'didReceiveAttrs', { attrs: { }, newAttrs: { } }], ['nested-item', 'on(init)'], ['an-item', 'willDestroy'], @@ -1624,8 +1625,8 @@ function expr(value) { return { isExpr: true, value }; } -function hook(name, hook, args) { - return { name, hook, args }; +function hook(name, hook, { attrs, oldAttrs, newAttrs } = {}) { + return { name, hook, args: { attrs, oldAttrs, newAttrs } }; } function json(serializable) { diff --git a/packages/ember-views/lib/index.js b/packages/ember-views/lib/index.js index 23338400275..dbbde0ac53f 100644 --- a/packages/ember-views/lib/index.js +++ b/packages/ember-views/lib/index.js @@ -23,7 +23,7 @@ export { default as CoreView } from './views/core_view'; export { default as ClassNamesSupport } from './mixins/class_names_support'; export { default as ChildViewsSupport } from './mixins/child_views_support'; export { default as ViewStateSupport } from './mixins/view_state_support'; -export { default as ViewMixin } from './mixins/view_support'; +export { default as ViewMixin, dispatchLifeCycleHook } from './mixins/view_support'; export { default as ActionSupport } from './mixins/action_support'; export { MUTABLE_CELL diff --git a/packages/ember-views/lib/mixins/view_support.js b/packages/ember-views/lib/mixins/view_support.js index 0ae781657a8..02f318750ab 100644 --- a/packages/ember-views/lib/mixins/view_support.js +++ b/packages/ember-views/lib/mixins/view_support.js @@ -1,14 +1,59 @@ import { guidFor, getOwner } from 'ember-utils'; -import { assert, deprecate, descriptor, Mixin } from 'ember-metal'; +import { assert, deprecate, descriptor, runInDebug, Mixin } from 'ember-metal'; import { environment } from 'ember-environment'; import { matches } from '../system/utils'; import { POST_INIT } from 'ember-runtime/system/core_object'; - - import jQuery from '../system/jquery'; function K() { return this; } +export let dispatchLifeCycleHook = function(component, hook, oldAttrs, newAttrs) { + component.trigger(hook, { attrs: newAttrs, oldAttrs, newAttrs }); +}; + +runInDebug(() => { + class Attrs { + constructor(oldAttrs, newAttrs, message) { + this._oldAttrs = oldAttrs; + this._newAttrs = newAttrs; + this._message = message; + } + + get attrs() { + return this.newAttrs; + } + + get oldAttrs() { + deprecate(this._message, false, { + id: 'ember-views.lifecycle-hook-arguments', + until: '2.13.0', + url: 'TODO' + }); + + return this._oldAttrs; + } + + get newAttrs() { + deprecate(this._message, false, { + id: 'ember-views.lifecycle-hook-arguments', + until: '2.13.0', + url: 'TODO' + }); + + return this._newAttrs; + } + } + + dispatchLifeCycleHook = function(component, hook, oldAttrs, newAttrs) { + if (typeof component[hook] === 'function' && component[hook].length !== 0) { + // Already warned in init + component.trigger(hook, { attrs: newAttrs, oldAttrs, newAttrs }); + } else { + component.trigger(hook, new Attrs(oldAttrs, newAttrs, `[DEPRECATED] Ember will stop passing arguments to component lifecycle hooks. Please change \`${component.toString()}#${hook}\` to stop taking arguments.`)); + } + }; +}); + /** @class ViewMixin @namespace Ember @@ -17,11 +62,10 @@ function K() { return this; } export default Mixin.create({ concatenatedProperties: ['attributeBindings'], [POST_INIT]() { - this.trigger('didInitAttrs', { attrs: this.attrs }); - this.trigger('didReceiveAttrs', { newAttrs: this.attrs }); + dispatchLifeCycleHook(this, 'didInitAttrs', undefined, this.attrs); + dispatchLifeCycleHook(this, 'didReceiveAttrs', undefined, this.attrs); }, - // .......................................................... // TEMPLATE SUPPORT // @@ -463,6 +507,36 @@ export default Mixin.create({ } ); + deprecate( + `[DEPRECATED] Ember will stop passing arguments to component lifecycle hooks. Please change \`${this.toString()}#didInitAttrs\` to stop taking arguments.`, + typeof this.didInitAttrs !== 'function' || this.didInitAttrs.length === 0, + { + id: 'ember-views.lifecycle-hook-arguments', + until: '2.13.0', + url: 'TODO' + } + ); + + deprecate( + `[DEPRECATED] Ember will stop passing arguments to component lifecycle hooks. Please change \`${this.toString()}#didReceiveAttrs\` to stop taking arguments.`, + typeof this.didReceiveAttrs !== 'function' || this.didReceiveAttrs.length === 0, + { + id: 'ember-views.lifecycle-hook-arguments', + until: '2.13.0', + url: 'TODO' + } + ); + + deprecate( + `[DEPRECATED] Ember will stop passing arguments to component lifecycle hooks. Please change \`${this.toString()}#didUpdateAttrs\` to stop taking arguments.`, + typeof this.didUpdateAttrs !== 'function' || this.didUpdateAttrs.length === 0, + { + id: 'ember-views.lifecycle-hook-arguments', + until: '2.13.0', + url: 'TODO' + } + ); + assert( 'Using a custom `.render` function is no longer supported.', !this.render