Skip to content

Commit

Permalink
Adds Ember.Debug.RenderDebug as an api for render debugging.
Browse files Browse the repository at this point in the history
Currently exposes `Ember.Debug.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 Oct 6, 2015
1 parent 23258c1 commit 4e3a106
Show file tree
Hide file tree
Showing 4 changed files with 408 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 @@ -6,6 +6,9 @@
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.Debug = Ember.Debug || {};
Ember.Debug.RenderDebug = { getTopLevelNode };
281 changes: 281 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,281 @@
import Component from 'ember-views/views/component';
import { get } from 'ember-metal/property_get';
import EmberAttrMorph from 'ember-htmlbars/morphs/attr-morph';
const { keys } = Object;

/**
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.
@public
@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}
@public
*/
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}
@public
*/
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 do 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.
@public
*/
buildChildren() {
let children = this._getChildRenderNodes() || [];
return children.map(renderNode => new this.constructor(renderNode, this))
.filter(inspectedNode => inspectedNode._isInspectable());
},

/**
Skip non-inspectable render nodes such as
attr morphs.
@private
@method _isInspectable
@return {Boolean}
*/
_isInspectable() {
return !(this._renderNode instanceof EmberAttrMorph);
},

/**
Gather the children assigned to the render node.
@private
@method _getChildRenderNodes
@return {Array} children renderNodes
@public
*/
_getChildRenderNodes() {
if (this._renderNode.morphMap) {
// each helper
return keys(this._renderNode.morphMap).map(key => this._renderNode.morphMap[key]);
} else {
return this._renderNode.childNodes;
}
},


/**
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}
@public
*/
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}
@public
*/
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}
@public
*/
hasComponentInstance() {
return !!this.getComponentInstance();
},

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

/**
Returns the node's controller.
@method getController
@return {Ember.Controller}
@public
*/
getController() {
return this._renderNode.lastResult && 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
@public
*/
getTemplateName() {
let template = this._renderNode.lastResult && 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}
@public
*/
isEmberComponent() {
let 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
@public
*/
getModel() {
let controller = this.getController();
if (controller) {
return get(controller, 'model');
}
},

/**
The name of the component/view instance.
@return {String}
@public
*/
getComponentInstanceName() {
let name;
let 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
@public
*/
getName() {
let name;
let 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
let 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}
@public
*/
getBoundingClientRect() {
let range = document.createRange();
range.setStartBefore(this._renderNode.firstNode);
range.setEndAfter(this._renderNode.lastNode);
return range.getBoundingClientRect();
}
};

export default InspectedNode;
19 changes: 19 additions & 0 deletions packages/ember-extension-support/lib/render_debug.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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`.
@public
@param {Ember.Application} application
@return {InspectedNode} The top level node
*/
export function getTopLevelNode(application) {
let topViewId = $(application.rootElement).find('> .ember-view').attr('id');
let rootView = application.__container__.lookup('-view-registry:main')[topViewId];
if (rootView) {
return new InspectedNode(rootView._renderNode);
}
}
Loading

0 comments on commit 4e3a106

Please sign in to comment.