Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creates SoyComponent for better integration between Component and soy templates #54

Merged
merged 1 commit into from
Feb 3, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ var mainFiles = [
'src/net/XhrTransport.js',
'src/net/WebSocketTransport.js',
'src/webchannel/WebChannel.js',
'src/component/Component.js'
'src/component/Component.js',
'src/component/SoyComponent.js'
];

gulp.task('build', ['clean'], function() {
Expand Down
48 changes: 26 additions & 22 deletions src/component/Component.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
* CustomComponent.prototype.detached = function() {};
* </code>
*
* @param {!Object} opt_config An object with the initial values for this component's
* attributes.
* @constructor
*/
lfr.Component = function(opt_config) {
Expand Down Expand Up @@ -280,12 +282,13 @@
};

/**
* Clears the surface content cache.
* @param {string} surfaceId The surface id to be removed from the cache.
* Clears the surfaces content cache.
* @protected
*/
lfr.Component.prototype.clearSurfaceCache_ = function(surfaceId) {
this.getSurface(surfaceId).cacheState = lfr.Component.Cache.NOT_INITIALIZED;
lfr.Component.prototype.clearSurfacesCache_ = function() {
for (var surfaceId in this.surfaces_) {
this.getSurface(surfaceId).cacheState = lfr.Component.Cache.NOT_INITIALIZED;
}
};

/**
Expand Down Expand Up @@ -409,7 +412,7 @@

this.decorateInternal();
this.computeSurfacesCacheStateFromDom_(); // TODO(edu): This optimization seems worth it, analyze it.
this.renderSurfacesContentIfModified_(this.surfaces_); // TODO(edu): Sync surfaces on decorate?
this.renderSurfacesContent_(this.surfaces_); // TODO(edu): Sync surfaces on decorate?

this.fireAttrsChanges_(this.constructor.ATTRS_SYNC_MERGED);

Expand Down Expand Up @@ -500,6 +503,19 @@
};

/**
* Gets the content for the requested surface. By default this just calls
* `getSurfaceContent`, but can be overriden to add more behavior (check
* `lfr.SoyComponent` for an example).
* @param {string} surfaceId The surface id.
* @return {Object|string} The content to be rendered.
* @protected
*/
lfr.Component.prototype.getSurfaceContent_ = function(surfaceId) {
return this.getSurfaceContent(surfaceId);
};

/**
* Gets the content for the requested surface. Should be implemented by subclasses.
* @param {string} surfaceId The surface id.
* @return {Object|string} The content to be rendered.
*/
Expand Down Expand Up @@ -544,9 +560,7 @@
*/
lfr.Component.prototype.handleAttributesChanges_ = function(event) {
if (this.inDocument) {
this.renderSurfacesContentIfModified_(
this.getModifiedSurfacesFromChanges_(event.changes)
);
this.renderSurfacesContent_(this.getModifiedSurfacesFromChanges_(event.changes));
}
this.fireAttrsChanges_(event.changes);
};
Expand Down Expand Up @@ -649,7 +663,8 @@
}

this.renderInternal();
this.renderSurfacesContent_();
this.clearSurfacesCache_();
this.renderSurfacesContent_(this.surfaces_);

this.fireAttrsChanges_(this.constructor.ATTRS_SYNC_MERGED);

Expand Down Expand Up @@ -710,24 +725,13 @@

/**
* Renders all surfaces contents ignoring the cache.
* @protected
*/
lfr.Component.prototype.renderSurfacesContent_ = function() {
for (var surfaceId in this.surfaces_) {
this.clearSurfaceCache_(surfaceId);
this.renderSurfaceContent(surfaceId, this.getSurfaceContent(surfaceId));
}
};

/**
* Renders surfaces contents if they differ from current state.
* @param {Object.<string, Object=>} surfaces Object map where the key is
* the surface id and value the optional surface configuration.
* @protected
*/
lfr.Component.prototype.renderSurfacesContentIfModified_ = function(surfaces) {
lfr.Component.prototype.renderSurfacesContent_ = function(surfaces) {
for (var surfaceId in surfaces) {
this.renderSurfaceContent(surfaceId, this.getSurfaceContent(surfaceId));
this.renderSurfaceContent(surfaceId, this.getSurfaceContent_(surfaceId));
}
};

Expand Down
66 changes: 66 additions & 0 deletions src/component/SoyComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
(function() {
'use strict';

/**
* Special Component class that handles a better integration between soy templates
* and the components. It allows for automatic rendering of surfaces that have soy
* templates defined with their names, skipping the call to `getSurfaceContent`.
* @param {Object} opt_config An object with the initial values for this component's
* attributes.
* @constructor
*/
lfr.SoyComponent = function(opt_config) {
lfr.SoyComponent.base(this, 'constructor', opt_config);
lfr.mergeSuperClassesProperty(this.constructor, 'TEMPLATES', this.mergeTemplates_);
};
lfr.inherits(lfr.SoyComponent, lfr.Component);

/**
* The soy templates for this component. Templates that have the same
* name of a registered surface will be used for automatically rendering
* it.
* @type {Object<string, !function(Object):Object>}
* @protected
* @static
*/
lfr.SoyComponent.TEMPLATES = {};

/**
* Overrides the default behavior so that this can automatically render
* the appropriate soy template when one exists.
* @param {string} surfaceId The surface id.
* @return {Object|string} The content to be rendered.
* @protected
* @override
*/
lfr.SoyComponent.prototype.getSurfaceContent_ = function(surfaceId) {
var surfaceTemplate = this.constructor.TEMPLATES_MERGED[surfaceId];
if (lfr.isFunction(surfaceTemplate)) {
return surfaceTemplate(this).content;
} else {
return lfr.SoyComponent.base(this, 'getSurfaceContent_', surfaceId);
}
};

/**
* Merges an array of values for the `TEMPLATES` property into a single object.
* @param {!Array} values The values to be merged.
* @return {!Object} The merged value.
* @protected
*/
lfr.SoyComponent.prototype.mergeTemplates_ = function(values) {
return lfr.object.mixin.apply(null, [{}].concat(values.reverse()));
};

/**
* Overrides the behavior of this method to automatically render the element
* template if it's defined.
* @override
*/
lfr.SoyComponent.prototype.renderInternal = function() {
var elementTemplate = this.constructor.TEMPLATES_MERGED.element;
if (lfr.isFunction(elementTemplate)) {
lfr.dom.append(this.element, elementTemplate(this).content);
}
};
}());
2 changes: 1 addition & 1 deletion test/component/Component.js
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,7 @@ describe('Component', function() {

sinon.spy(lfr.dom, 'append');

custom.renderSurfacesContentIfModified_({
custom.renderSurfacesContent_({
header: true,
body: true,
bottom: true
Expand Down
113 changes: 113 additions & 0 deletions test/component/SoyComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
'use strict';

var assert = require('assert');
var jsdom = require('mocha-jsdom');
var sinon = require('sinon');
require('../fixture/sandbox.js');

describe('SoyComponent', function() {

jsdom();

beforeEach(function() {
Element.prototype.classList = {
add: sinon.stub(),
remove: sinon.stub()
};
});

afterEach(function() {
document.body.innerHTML = '';
});

it('should render element content automatically when template is defined', function() {
var CustomComponent = createCustomComponentClass();
CustomComponent.TEMPLATES = {
element: function() {
return {
content: '<div class="myContent">Hello World</div>'
};
}
};

var custom = new CustomComponent();
custom.render();

assert.strictEqual(1, custom.element.children.length);
assert.strictEqual('myContent', custom.element.children[0].className);
});

it('should create surfaces from element template', function() {
var CustomComponent = createCustomComponentClass();
CustomComponent.SURFACES = {
header: {}
};
CustomComponent.TEMPLATES = {
element: function(data) {
return {
content: '<div id="' + data.id + '-header">Header Surface</div>'
};
}
};

var custom = new CustomComponent();
custom.render();

assert.strictEqual('Header Surface', custom.getSurfaceElement('header').innerHTML);
});

it('should not throw error if element template is not defined', function() {
var CustomComponent = createCustomComponentClass();
var custom = new CustomComponent();

assert.doesNotThrow(function() {
custom.render();
});
});

it('should render surface automatically from template', function(done) {
var CustomComponent = createCustomComponentClass();
CustomComponent.ATTRS = {
headerContent: {
value: 'Hello World'
}
};
CustomComponent.SURFACES = {
header: {
renderAttrs: ['headerContent']
}
};
CustomComponent.TEMPLATES = {
element: function(data) {
return {
content: '<div id="' + data.id + '-header"></div>'
};
},
header: function(data) {
return {
content: '<p>' + data.headerContent + '</p>'
};
}
};

var custom = new CustomComponent();
custom.render();

var surfaceElement = custom.getSurfaceElement('header');
assert.strictEqual('<p>Hello World</p>', surfaceElement.innerHTML);

custom.headerContent = 'Hello World 2';
lfr.async.nextTick(function() {
assert.strictEqual('<p>Hello World 2</p>', surfaceElement.innerHTML);
done();
});
});
});

function createCustomComponentClass() {
function CustomComponent(opt_config) {
CustomComponent.base(this, 'constructor', opt_config);
}
lfr.inherits(CustomComponent, lfr.SoyComponent);
return CustomComponent;
}
1 change: 1 addition & 0 deletions test/fixture/sandbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ require('../../src/net/XhrTransport.js');
require('../../src/net/WebSocketTransport.js');
require('../../src/webchannel/WebChannel.js');
require('../../src/component/Component.js');
require('../../src/component/SoyComponent.js');

global.window = null;
global.Event = null;