Skip to content

Commit

Permalink
Adds Ember.Debugging.RenderDebug as an api for render debugging.
Browse files Browse the repository at this point in the history
Currently exposes `Ember.Debugging.RenderDebug.getTopLevelNode` which
returns the top level inspected node. `InspectedNode` is a wrapper class for
the htmlbars render node. It provides public debugging methods
so that external tools like the Ember Inspector can use stable and
tested apis.
  • Loading branch information
teddyzeenny committed May 24, 2015
1 parent d9889a2 commit 1274406
Show file tree
Hide file tree
Showing 4 changed files with 368 additions and 0 deletions.
3 changes: 3 additions & 0 deletions packages/ember-extension-support/lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ Ember Extension Support
import Ember from "ember-metal/core";
import DataAdapter from "ember-extension-support/data_adapter";
import ContainerDebugAdapter from "ember-extension-support/container_debug_adapter";
import { getTopLevelNode } from "ember-extension-support/render_debug";

Ember.DataAdapter = DataAdapter;
Ember.ContainerDebugAdapter = ContainerDebugAdapter;

Ember.Debugging = { RenderDebug: { getTopLevelNode } };
234 changes: 234 additions & 0 deletions packages/ember-extension-support/lib/models/inspected_node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import Component from "ember-views/views/component";
import { A as emberA } from "ember-runtime/system/native_array";
import { get } from "ember-metal/property_get";

/**
* Wraps a render node to provide debugging API for external services
* such as the Ember Inspector.
*
* Ideally public methods on this class should completely hide
* the internal implementations of render nodes.
*
* @class InspectedNode
* @param {Object} renderNode The Htmlbars render node
* @param {Object} parent The parent InspectedNode instance
*/
function InspectedNode(renderNode, parent) {
this.renderNode = renderNode;
this.parent = parent;
}

InspectedNode.prototype = {
/**
* Useful for subclassing.
*
* @property constructor
* @type {Function}
*/
constructor: InspectedNode,

/**
* The render node we are representing and adding
* debugging functionality to.
*
* This property is set upon creation of the instance.
*
* @property renderNode
* @type {Object}
* @private
*/
renderNode: null,

/**
* The parent inspected node. This property is set
* when building the a node's children. This allows
* us to walk upward in the render hierarchy.
*
* @property parent
* @type {InspectedNode}
*/
parent: null,

/**
* Fetches the render node's children and returns
* an array of InspectedNode instances (an instance is created
* for each child).
*
* Consecutive calls to this method does *not* return the same
* instances (each call creates new instances). This is why
* the method name is `buildChildren` as opposed to `getChildren`.
*
* @method buildChildren
* @return {Array} array of inspected nodes wrapping the render node's children.
*/
buildChildren() {
var childNodes = emberA(this.renderNode.childNodes);
return childNodes.map(renderNode => new this.constructor(renderNode, this));
},

/**
* Not all nodes are actually views/components.
* Nodes can be attributes for example. This method allows
* us to identify component nodes.
*
* Not to be confused with `isEmberComponent`. `Ember.View` instances
* also return `true`.
*
* @method isComponentNode
* @return {Boolean}
*/
isComponentNode() {
return !!this.renderNode.state.manager;
},

/**
* Check if a node has its own controller (as opposed to sharing
* its parent's controller).
*
* Useful to identify route views from other views. Views that have
* their own controller are shown by default in the Ember Inspector
* view tree.
*
* @method hasOwnController
* @return {Boolean}
*/
hasOwnController() {
return !this.parent || (this.getController() !== this.parent.getController());
},

/**
* Check if the node has a view or component instance.
* Virtual nodes don't have a view/component instance.
*
* @method hasComponentInstance
* @return {Boolean}
*/
hasComponentInstance() {
return !!this.getComponentInstance();
},

/**
* Return a node's view/component instance.
*
* @method getComponentInstance
* @return {Ember.View|Ember.Component}
*/
getComponentInstance() {
return this.renderNode.emberView;
},

/**
* Returns the node's controller.
*
* @method getController
* @return {Ember.Controller}
*/
getController() {
return this.renderNode.lastResult.scope.locals.controller.value();
},

/**
* Get the node's template name. Relies on an htmlbars
* feature that adds the module name as a meta property
* to compiled templates.
*
* When compiling a template, pass the template's name
* as a the `moduleName` property on the options argument.
* This should be done automatically by tools that pre-compile
* templates (ex: ember-cli does this in version >= 0.2.4)
*
* @method getTemplateName
* @return {String} The template name
*/
getTemplateName() {
var template = this.renderNode.lastResult.template;
if (template && template.meta && template.meta.moduleName) {
return template.meta.moduleName.replace(/\.hbs$/, '');
}
},

/**
* Returns whether the node is an Ember.Component or not.
* *Not* to be confused with `isComponent` which returns true
* for both views and components.
*
* @return {Boolean}
*/
isEmberComponent() {
var componentInstance = this.getComponentInstance(this.renderNode);
return !!(componentInstance && (componentInstance instanceof Component));
},

/**
* The node's model. If the view has a controller,
* it will be the controller's `model` property.
*
* @return {Object} the model
*/
getModel() {
var controller = this.getController();
if (controller) {
return get(controller, 'model');
}
},

/**
* The name of the component/view instance.
*
* @return {String}
*/
getComponentInstanceName() {
var name;
var componentInstance = this.getComponentInstance();
if (componentInstance) {
// Has a component instance - take the component's name
name = get(componentInstance, '_debugContainerKey');
if (name) {
name = name.replace(/.*(view|component):/, '').replace(/:$/, '');
}
return name;
}
},

/**
* The node's name. Should be anything that the user
* can use to identity what node we are talking about.
*
* Usually either the view instance name, or the template name.
*
* @return {String} The node's name
*/
getName() {
var name;
var componentInstanceName = this.getComponentInstanceName();
if (componentInstanceName) {
name = componentInstanceName;
// If application view was not defined, it uses a `toplevel` view
if (name === 'toplevel') {
name = 'application';
}
} else {
// Virtual - no component/view instance
var templateName = this.getTemplateName();
if (templateName) {
name = templateName.replace(/^.*templates\//, '').replace(/\//g, '.');
}
}
return name;
},

/**
* Get the node's bounding client rect.
* Can be used to get the node's position.
*
* @return {DOMRect}
*/
getBoundingClientRect() {
var range = document.createRange();
range.setStartBefore(this.renderNode.firstNode);
range.setEndAfter(this.renderNode.lastNode);
return range.getBoundingClientRect();
}
};

export default InspectedNode;
18 changes: 18 additions & 0 deletions packages/ember-extension-support/lib/render_debug.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import InspectedNode from 'ember-extension-support/models/inspected_node';
import $ from "ember-views/system/jquery";

/**
* Returns the top level node in an application.
* The returned object will be an `InspectedNode` instance
* which will wrap the htmlbars `renderNode`.
*
* @param {Ember.Application} application
* @return {InspectedNode} The top level node
*/
export function getTopLevelNode(application) {
var topViewId = $(application.rootElement).find('> .ember-view').attr('id');
var rootView = application.__container__.lookup('-view-registry:main')[topViewId];
if (rootView) {
return new InspectedNode(rootView._renderNode);
}
}
113 changes: 113 additions & 0 deletions packages/ember-extension-support/tests/render_debug_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import EmberApplication from "ember-application/system/application";
import run from "ember-metal/run_loop";
import { getTopLevelNode } from "ember-extension-support/render_debug";
import compile from "ember-template-compiler/system/compile";
import Route from "ember-routing/system/route";
import Component from "ember-views/views/component";
import View from "ember-views/views/view";

var app;

function lookup(name) {
return app.__container__.lookup(name);
}

function flattenComponentNodes(node) {
var array = [];
if (node.isComponentNode()) {
array.push(node);
}
node.buildChildren().forEach(child => {
array = array.concat(flattenComponentNodes(child));
});
return array;
}

QUnit.module("Debugging - Render Debug", {
setup() {
run(() => {
app = EmberApplication.create();
app.deferReadiness();
});
},

teardown() {
run(app, 'destroy');
app = null;
}
});

QUnit.test('Simple render tree', function() {
let model = {};
app.registry.register('template:application', compile('{{outlet}}', { moduleName: 'application' }));
app.registry.register('template:index', compile('<h1>Index Page</h1>', { moduleName: 'index' }));
app.registry.register('route:application', Route.extend({
model() {
return model;
}
}));

run(app, 'advanceReadiness');

var nodes = flattenComponentNodes(getTopLevelNode(app));
var node = nodes[0];
equal(node.isComponentNode(), true, 'correctly identified as a component node');
equal(node.hasOwnController(), true, 'top level always has its own controller');
equal(node.hasComponentInstance(), true, 'Top level always has its own View instance');
equal(node.getController(), lookup('controller:application'), 'returns the correct controller');
equal(node.getTemplateName(), 'application', 'Gets the correct template name');
equal(node.isEmberComponent(), false, 'top level is a view not a component');
equal(node.getModel(), model, 'returns the correct model');
equal(node.getComponentInstanceName(), 'toplevel', 'returns the correct view instance name');
equal(node.getName(), 'application', 'returns the correct name');
ok(node.getBoundingClientRect());

node = nodes[1];
equal(node.isComponentNode(), true, 'index correctly identified as a component node');
equal(node.hasOwnController(), true, 'route nodes always have their own controller');
equal(node.hasComponentInstance(), false, 'index is virtual - no view instance');
equal(node.getController(), lookup('controller:index'), 'returns the correct controller');
equal(node.getTemplateName(), 'index', 'index gets the correct template name');
equal(node.isEmberComponent(), false, 'index is a view not a component');
equal(node.getModel(), model, "index has the same model as application");
equal(node.getName(), 'index', 'returns the correct name');
ok(node.getBoundingClientRect());
});

QUnit.test('Ember component node', function() {
app.registry.register('template:application', compile('{{foo-bar}}', { moduleName: 'application' }));
app.registry.register('template:components/foo-bar', compile('Foo bar', { moduleName: 'foo-bar' }));
app.registry.register('component:foo-bar', Component);

run(app, 'advanceReadiness');

var nodes = flattenComponentNodes(getTopLevelNode(app));
var component = nodes[1];

equal(component.isComponentNode(), true, 'correctly identified as a component node');
equal(component.hasComponentInstance(), true, 'Ember component instance detected');
equal(component.getTemplateName(), 'foo-bar', 'has the correct template name');
equal(component.isEmberComponent(), true, 'correctly identified as an Ember Component');
equal(component.getName(), 'foo-bar', 'returns the correct name');

});

QUnit.test('Inline Ember View', function() {
app.registry.register('template:application', compile('{{view "foo"}}', { moduleName: 'application' }));
app.registry.register('template:foo', compile('Foo View', { moduleName: 'foo' }));
app.registry.register('view:foo', View.extend({
templateName: 'foo'
}));

run(app, 'advanceReadiness');

var nodes = flattenComponentNodes(getTopLevelNode(app));
var view = nodes[1];

equal(view.isComponentNode(), true, 'correctly identified as a component node');
equal(view.hasComponentInstance(), true, 'Ember view instance detected');
equal(view.getTemplateName(), 'foo', 'has the correct template name');
equal(view.isEmberComponent(), false, 'not an Ember Component');
equal(view.getName(), 'foo', 'returns the correct name');

});

0 comments on commit 1274406

Please sign in to comment.