Easier to group bindings. Perf improvements.

This commit is contained in:
Robin Ward 2013-08-02 13:17:28 -04:00
parent 1f53d5dcac
commit ef82b66e95
19 changed files with 284 additions and 254 deletions

View File

@ -10,8 +10,8 @@
</thead> </thead>
{{#if model.length}} {{#if model.length}}
{{#group}} {{#groupedEach model}}
{{#collection contentBinding="model" tagName="tbody" itemTagName="tr"}} <tr>
<td>{{date created_at}}</td> <td>{{date created_at}}</td>
<td> <td>
{{#if user}} {{#if user}}
@ -24,8 +24,8 @@
<td><a href='mailto:{{unbound to_address}}'>{{to_address}}</a></td> <td><a href='mailto:{{unbound to_address}}'>{{to_address}}</a></td>
<td>{{email_type}}</td> <td>{{email_type}}</td>
<td>{{reply_key}}</td> <td>{{reply_key}}</td>
{{/collection}} </tr>
{{/group}} {{/groupedEach}}
{{/if}} {{/if}}
</table> </table>

View File

@ -23,10 +23,8 @@ require_asset ("./external/handlebars.js")
if Rails.env.development? if Rails.env.development?
require_asset ("./external_development/ember.js") require_asset ("./external_development/ember.js")
require_asset ("./external_development/group-helper.js")
else else
require_asset ("./external_production/ember.js") require_asset ("./external_production/ember.js")
require_asset ("./external_production/group-helper.js")
end end
require_asset ("./main_include.js") require_asset ("./main_include.js")

View File

@ -0,0 +1,111 @@
var DiscourseGroupedEach = function(context, path, options) {
var self = this,
normalized = Ember.Handlebars.normalizePath(context, path, options.data);
this.context = context;
this.path = path;
this.options = options;
this.template = options.fn;
this.containingView = options.data.view;
this.normalizedRoot = normalized.root;
this.normalizedPath = normalized.path;
this.content = this.lookupContent();
this.addContentObservers();
this.addArrayObservers();
this.containingView.on('willClearRender', function() {
self.destroy();
});
};
DiscourseGroupedEach.prototype = {
contentWillChange: function() {
this.removeArrayObservers();
},
contentDidChange: function() {
this.content = this.lookupContent();
this.addArrayObservers();
this.rerenderContainingView();
},
contentArrayWillChange: Ember.K,
contentArrayDidChange: function() {
this.rerenderContainingView();
},
lookupContent: function() {
return Ember.Handlebars.get(this.normalizedRoot, this.normalizedPath, this.options);
},
addArrayObservers: function() {
this.content.addArrayObserver(this, {
willChange: 'contentArrayWillChange',
didChange: 'contentArrayDidChange'
});
},
removeArrayObservers: function() {
this.content.removeArrayObserver(this, {
willChange: 'contentArrayWillChange',
didChange: 'contentArrayDidChange'
});
},
addContentObservers: function() {
Ember.addBeforeObserver(this.normalizedRoot, this.normalizedPath, this, this.contentWillChange);
Ember.addObserver(this.normalizedRoot, this.normalizedPath, this, this.contentDidChange);
},
removeContentObservers: function() {
Ember.removeBeforeObserver(this.normalizedRoot, this.normalizedPath, this.contentWillChange);
Ember.removeObserver(this.normalizedRoot, this.normalizedPath, this.contentDidChange);
},
render: function() {
var content = this.content,
contentLength = Em.get(content, 'length'),
data = this.options.data,
template = this.template;
data.insideEach = true;
data.insideGroup = true;
for (var i = 0; i < contentLength; i++) {
template(content.objectAt(i), { data: data });
}
},
rerenderContainingView: function() {
Ember.run.scheduleOnce('render', this.containingView, 'rerender');
},
destroy: function() {
this.removeContentObservers();
this.removeArrayObservers();
}
};
Ember.Handlebars.registerHelper('groupedEach', function(path, options) {
if (arguments.length === 4) {
Ember.assert("If you pass more than one argument to the groupedEach helper, it must be in the form #groupedEach foo in bar", arguments[1] === "in");
var keywordName = arguments[0];
options = arguments[3];
path = arguments[2];
if (path === '') { path = "this"; }
options.hash.keyword = keywordName;
}
if (arguments.length === 1) {
options = path;
path = 'this';
}
options.hash.dataSourceBinding = path;
new DiscourseGroupedEach(this, path, options).render();
});

View File

@ -10,8 +10,8 @@
<th class='num activity' colspan='2'>{{i18n activity}}</th> <th class='num activity' colspan='2'>{{i18n activity}}</th>
</tr> </tr>
{{#group}} {{#groupedEach view.topics}}
{{#collection contentBinding="view.topics" tagName="tbody" itemTagName="tr"}} <tr>
<td class='main-link'> <td class='main-link'>
<a class='title' href="{{unbound lastReadUrl}}">{{{unbound fancy_title}}}</a> <a class='title' href="{{unbound lastReadUrl}}">{{{unbound fancy_title}}}</a>
{{#if unread}} {{#if unread}}
@ -21,35 +21,35 @@
<a href="{{unbound lastReadUrl}}" class='badge new-posts badge-notification' title='{{i18n topic.new_posts count="new_posts"}}'>{{unbound new_posts}}</a> <a href="{{unbound lastReadUrl}}" class='badge new-posts badge-notification' title='{{i18n topic.new_posts count="new_posts"}}'>{{unbound new_posts}}</a>
{{/if}} {{/if}}
{{#if unseen}} {{#if unseen}}
<a href="{{lastReadUrl}}" class='badge new-posts badge-notification' title='{{i18n topic.new}}'><i class='icon icon-asterisk'></i></a> <a href="{{unbound lastReadUrl}}" class='badge new-posts badge-notification' title='{{i18n topic.new}}'><i class='icon icon-asterisk'></i></a>
{{/if}} {{/if}}
</td> </td>
<td class='category'> <td class='category'>
{{categoryLink category}} {{categoryLink category}}
</td> </td>
<td class='num posts'><a href="{{lastReadUrl}}" class='badge-posts'>{{number posts_count numberKey="posts_long"}}</a></td> <td class='num posts'><a href="{{unbound lastReadUrl}}" class='badge-posts'>{{number posts_count numberKey="posts_long"}}</a></td>
<td class='num likes'> <td class='num likes'>
{{#if like_count}} {{#if like_count}}
<a href='{{url}}{{#if has_best_of}}?filter=best_of{{/if}}'>{{like_count}} <i class='icon-heart'></i></a> <a href='{{unbound url}}{{#if has_best_of}}?filter=best_of{{/if}}'>{{unbound like_count}} <i class='icon-heart'></i></a>
{{/if}} {{/if}}
</td> </td>
<td {{bindAttr class=":num :views viewsHeat"}}>{{number views numberKey="views_long"}}</td> <td {{bindAttr class=":num :views viewsHeat"}}>{{number views numberKey="views_long"}}</td>
{{#if bumped}} {{#if bumped}}
<td class='num activity'> <td class='num activity'>
<a href="{{url}}" {{{bindAttr class=":age ageCold"}}} title='{{i18n first_post}}: {{{unboundDate created_at}}}' >{{unboundAge created_at}}</a> <a href="{{unbound url}}" {{{bindAttr class=":age ageCold"}}} title='{{i18n first_post}}: {{{unboundDate created_at}}}' >{{unboundAge created_at}}</a>
</td> </td>
<td class='num activity last'> <td class='num activity last'>
<a href="{{lastPostUrl}}" class='age' title='{{i18n last_post}}: {{{unboundDate bumped_at}}}'>{{unboundAge bumped_at}}</a> <a href="{{unbound lastPostUrl}}" class='age' title='{{i18n last_post}}: {{{unboundDate bumped_at}}}'>{{unboundAge bumped_at}}</a>
</td> </td>
{{else}} {{else}}
<td class='num activity'> <td class='num activity'>
<a href="{{url}}" class='age' title='{{i18n first_post}}: {{{unboundDate created_at}}}'>{{unboundAge created_at}}</a> <a href="{{unbound url}}" class='age' title='{{i18n first_post}}: {{{unboundDate created_at}}}'>{{unboundAge created_at}}</a>
</td> </td>
<td class="activity"></td> <td class="activity"></td>
{{/if}} {{/if}}
{{/collection}} </tr>
{{/group}} {{/groupedEach}}
</table> </table>

View File

@ -1,10 +1,10 @@
{{#if controller.currentUser.id}} {{#if controller.currentUser.id}}
<td class='star'> <td class='star'>
<a {{bindAttr class=":star :icon-star starred:starred"}} {{action toggleStar this}} href='#' {{bindAttr title="favoriteTooltip"}}></a> <a {{bindAttr class=":star :icon-star starred:starred"}} {{action toggleStar this}} href='#' {{bindAttr title="favoriteTooltip"}}></a>
</td> </td>
{{/if}} {{/if}}
<td class='main-link clearfix'> <td class='main-link clearfix'>
{{#if controller.rankDetailsVisible}} {{#if controller.rankDetailsVisible}}
<div class='rank-details'> <div class='rank-details'>
@ -35,42 +35,42 @@
{{#unless canClearPin}}<a href="{{lastReadUrl}}">{{i18n read_more}}</a>{{/unless}} {{#unless canClearPin}}<a href="{{lastReadUrl}}">{{i18n read_more}}</a>{{/unless}}
{{/if}} {{/if}}
{{#if canClearPin}} {{#if canClearPin}}
<a href="#" {{action clearPin this}} title="{{unbound i18n topic.clear_pin.help}}">{{i18n topic.clear_pin.title}}</a> <a href="#" {{action clearPin this}} title="{{i18n topic.clear_pin.help}}">{{i18n topic.clear_pin.title}}</a>
{{/if}} {{/if}}
</div> </div>
{{/if}} {{/if}}
</td> </td>
<td class='category'> <td class='category'>
{{categoryLink category}} {{categoryLink category}}
</td> </td>
<td class='posters'> <td class='posters'>
{{#each posters}} {{#each posters}}
<a href="{{user.path}}" class="{{unbound extras}}">{{avatar this usernamePath="user.username" imageSize="small"}}</a> <a href="{{user.path}}" class="{{extras}}">{{avatar this usernamePath="user.username" imageSize="small"}}</a>
{{/each}} {{/each}}
</td> </td>
<td class='num posts'><a href="{{lastReadUrl}}" class='badge-posts'>{{number posts_count numberKey="posts_long"}}</a></td> <td class='num posts'><a href="{{lastReadUrl}}" class='badge-posts'>{{number posts_count numberKey="posts_long"}}</a></td>
<td class='num likes'> <td class='num likes'>
{{#if like_count}} {{#if like_count}}
<a href='{{url}}{{#if has_best_of}}?filter=best_of{{/if}}' title='{{i18n topic.likes count="like_count"}}'>{{number like_count numberKey="likes_long"}} <i class='icon-heart'></i></a> <a href='{{url}}{{#if has_best_of}}?filter=best_of{{/if}}' title='{{i18n topic.likes count="like_count"}}'>{{number like_count numberKey="likes_long"}} <i class='icon-heart'></i></a>
{{/if}} {{/if}}
</td> </td>
<td {{bindAttr class=":num :views viewsHeat"}}>{{number views numberKey="views_long"}}</td> <td {{bindAttr class=":num :views viewsHeat"}}>{{number views numberKey="views_long"}}</td>
{{#if bumped}} {{#if bumped}}
<td class='num activity'> <td class='num activity'>
<a href="{{url}}" {{{bindAttr class=":age ageCold"}}} title='{{i18n first_post}}: {{{unboundDate created_at}}}' >{{unboundAge created_at}}</a> <a href="{{url}}" {{{bindAttr class=":age ageCold"}}} title='{{i18n first_post}}: {{{unboundDate created_at}}}' >{{unboundAge created_at}}</a>
</td> </td>
<td class='num activity last'> <td class='num activity last'>
<a href="{{lastPostUrl}}" class='age' title='{{i18n last_post}}: {{{unboundDate bumped_at}}}'>{{unboundAge bumped_at}}</a> <a href="{{lastPostUrl}}" class='age' title='{{i18n last_post}}: {{{unboundDate bumped_at}}}'>{{unboundAge bumped_at}}</a>
</td> </td>
{{else}} {{else}}
<td class='num activity'> <td class='num activity'>
<a href="{{url}}" class='age' title='{{i18n first_post}}: {{{unboundDate created_at}}}'>{{unboundAge created_at}}</a> <a href="{{url}}" class='age' title='{{i18n first_post}}: {{{unboundDate created_at}}}'>{{unboundAge created_at}}</a>
</td> </td>
<td class='activity'></td> <td class='activity'></td>
{{/if}} {{/if}}

View File

@ -41,9 +41,7 @@
</tbody> </tbody>
{{/if}} {{/if}}
{{#group}}
{{collection contentBinding="topics" tagName="tbody" itemViewClass="Discourse.TopicListItemView"}} {{collection contentBinding="topics" tagName="tbody" itemViewClass="Discourse.TopicListItemView"}}
{{/group}}
</table> </table>
{{/if}} {{/if}}

View File

@ -47,7 +47,7 @@
</div> </div>
</div> </div>
{{/unless}} {{/unless}}
{{view Discourse.RawDivView class="cooked" contentBinding="cooked"}} <div class='cooked'>{{{cooked}}}</div>
{{view Discourse.PostMenuView postBinding="this" postViewBinding="view"}} {{view Discourse.PostMenuView postBinding="this" postViewBinding="view"}}
</div> </div>
{{view Discourse.RepliesView contentBinding="replies" postViewBinding="view"}} {{view Discourse.RepliesView contentBinding="replies" postViewBinding="view"}}

View File

@ -6,15 +6,10 @@
@namespace Discourse @namespace Discourse
@module Discourse @module Discourse
**/ **/
Discourse.EmbeddedPostView = Discourse.View.extend({ Discourse.EmbeddedPostView = Discourse.GroupedView.extend({
templateName: 'embedded_post', templateName: 'embedded_post',
classNames: ['reply'], classNames: ['reply'],
init: function() {
this._super();
this.set('context', this.get('content'));
},
didInsertElement: function() { didInsertElement: function() {
Discourse.ScreenTrack.instance().track(this.get('elementId'), this.get('post.post_number')); Discourse.ScreenTrack.instance().track(this.get('elementId'), this.get('post.post_number'));
}, },

View File

@ -2,11 +2,11 @@
This view handles the rendering of a topic in a list This view handles the rendering of a topic in a list
@class TopicListItemView @class TopicListItemView
@extends Discourse.View @extends Discourse.GroupedView
@namespace Discourse @namespace Discourse
@module Discourse @module Discourse
**/ **/
Discourse.TopicListItemView = Discourse.View.extend({ Discourse.TopicListItemView = Discourse.GroupedView.extend({
tagName: 'tr', tagName: 'tr',
templateName: 'list/topic_list_item', templateName: 'list/topic_list_item',
classNameBindings: ['content.archived', ':topic-list-item', 'content.hasExcerpt:has-excerpt'], classNameBindings: ['content.archived', ':topic-list-item', 'content.hasExcerpt:has-excerpt'],
@ -14,11 +14,6 @@ Discourse.TopicListItemView = Discourse.View.extend({
'data-topic-id': Em.computed.alias('content.id'), 'data-topic-id': Em.computed.alias('content.id'),
init: function() {
this._super();
this.set('context', this.get('content'));
},
highlight: function() { highlight: function() {
var $topic = this.$(); var $topic = this.$();
var originalCol = $topic.css('backgroundColor'); var originalCol = $topic.css('backgroundColor');

View File

@ -6,7 +6,7 @@
@namespace Discourse @namespace Discourse
@module Discourse @module Discourse
**/ **/
Discourse.OptionBooleanView = Discourse.View.extend({ Discourse.OptionBooleanView = Discourse.GroupedView.extend({
classNames: ['archetype-option'], classNames: ['archetype-option'],
composerControllerBinding: 'Discourse.router.composerController', composerControllerBinding: 'Discourse.router.composerController',
templateName: "modal/option_boolean", templateName: "modal/option_boolean",
@ -16,12 +16,7 @@ Discourse.OptionBooleanView = Discourse.View.extend({
metaData = this.get('parentView.metaData'); metaData = this.get('parentView.metaData');
metaData.set(this.get('content.key'), this.get('checked') ? 'true' : 'false'); metaData.set(this.get('content.key'), this.get('checked') ? 'true' : 'false');
return this.get('controller.controllers.composer').saveDraft(); return this.get('controller.controllers.composer').saveDraft();
}).observes('checked'), }).observes('checked')
init: function() {
this._super();
return this.set('context', this.get('content'));
}
}); });

View File

@ -2,11 +2,11 @@
This view renders a post. This view renders a post.
@class PostView @class PostView
@extends Discourse.View @extends Discourse.GroupedView
@namespace Discourse @namespace Discourse
@module Discourse @module Discourse
**/ **/
Discourse.PostView = Discourse.View.extend({ Discourse.PostView = Discourse.GroupedView.extend({
classNames: ['topic-post', 'clearfix'], classNames: ['topic-post', 'clearfix'],
templateName: 'post', templateName: 'post',
classNameBindings: ['postTypeClass', classNameBindings: ['postTypeClass',
@ -28,18 +28,12 @@ Discourse.PostView = Discourse.View.extend({
}); });
}.observes('post.cooked'), }.observes('post.cooked'),
init: function() {
this._super();
this.set('context', this.get('content'));
},
mouseUp: function(e) { mouseUp: function(e) {
if (this.get('controller.multiSelect') && (e.metaKey || e.ctrlKey)) { if (this.get('controller.multiSelect') && (e.metaKey || e.ctrlKey)) {
this.get('controller').selectPost(this.get('post')); this.get('controller').selectPost(this.get('post'));
} }
}, },
selected: function() { selected: function() {
var selectedPosts = this.get('controller.selectedPosts'); var selectedPosts = this.get('controller.selectedPosts');
if (!selectedPosts) return false; if (!selectedPosts) return false;

View File

@ -1,11 +0,0 @@
// used to render a div with unescaped contents
Discourse.RawDivView = Ember.View.extend({
shouldRerender: Discourse.View.renderIfChanged('content'),
render: function(buffer) {
buffer.push(this.get('content'));
}
});

View File

@ -8,17 +8,11 @@
**/ **/
Discourse.SearchResultsTypeView = Ember.CollectionView.extend({ Discourse.SearchResultsTypeView = Ember.CollectionView.extend({
tagName: 'ul', tagName: 'ul',
itemViewClass: Ember.View.extend({ itemViewClass: Discourse.GroupedView.extend({
tagName: 'li', tagName: 'li',
classNameBindings: ['selected'], classNameBindings: ['selected'],
templateName: Discourse.computed.fmt('parentView.type', "search/%@_result"), templateName: Discourse.computed.fmt('parentView.type', "search/%@_result"),
selected: Discourse.computed.propertyEqual('content.index', 'controller.selectedIndex'), selected: Discourse.computed.propertyEqual('content.index', 'controller.selectedIndex')
init: function() {
this._super();
this.set('context', this.get('content'));
}
}) })
}); });

View File

@ -13,13 +13,7 @@ Discourse.UserStreamView = Ember.CollectionView.extend(Discourse.LoadMore, {
eyelineSelector: '#user-activity .user-stream .item', eyelineSelector: '#user-activity .user-stream .item',
classNames: ['user-stream'], classNames: ['user-stream'],
itemViewClass: Ember.View.extend({ itemViewClass: Discourse.GroupedView.extend({ templateName: 'user/stream_item' }),
templateName: 'user/stream_item',
init: function() {
this._super();
this.set('context', this.get('content'));
}
}),
loadMore: function() { loadMore: function() {
var userStreamView = this; var userStreamView = this;

View File

@ -9,6 +9,20 @@
**/ **/
Discourse.View = Ember.View.extend(Discourse.Presence, {}); Discourse.View = Ember.View.extend(Discourse.Presence, {});
Discourse.GroupedView = Ember.View.extend(Discourse.Presence, {
init: function() {
this._super();
this.set('context', this.get('content'));
var templateData = this.get('templateData');
if (templateData) {
this.set('templateData.insideGroup', true);
}
}
});
Discourse.View.reopenClass({ Discourse.View.reopenClass({
/** /**

View File

@ -1,23 +0,0 @@
(function() {
var get = Ember.get, set = Ember.set, EmberHandlebars = Ember.Handlebars;
EmberHandlebars.registerHelper('group', function(options) {
var data = options.data,
fn = options.fn,
view = data.view,
childView;
childView = view.createChildView(Ember._MetamorphView, {
context: get(view, 'context'),
template: function(context, options) {
options.data.insideGroup = true;
return fn(context, options);
}
});
view.appendChild(childView);
});
})();

View File

@ -1,23 +0,0 @@
(function() {
var get = Ember.get, set = Ember.set, EmberHandlebars = Ember.Handlebars;
EmberHandlebars.registerHelper('group', function(options) {
var data = options.data,
fn = options.fn,
view = data.view,
childView;
childView = view.createChildView(Ember._MetamorphView, {
context: get(view, 'context'),
template: function(context, options) {
options.data.insideGroup = true;
return fn(context, options);
}
});
view.appendChild(childView);
});
})();

View File

@ -13,7 +13,6 @@
//= require ../../app/assets/javascripts/external/jquery.ui.widget.js //= require ../../app/assets/javascripts/external/jquery.ui.widget.js
//= require ../../app/assets/javascripts/external/handlebars.js //= require ../../app/assets/javascripts/external/handlebars.js
//= require ../../app/assets/javascripts/external_development/ember.js //= require ../../app/assets/javascripts/external_development/ember.js
//= require ../../app/assets/javascripts/external_development/group-helper.js
//= require ../../app/assets/javascripts/locales/i18n //= require ../../app/assets/javascripts/locales/i18n
//= require ../../app/assets/javascripts/discourse/helpers/i18n_helpers //= require ../../app/assets/javascripts/discourse/helpers/i18n_helpers