From ef82b66e95324805e3d3d982d2052c52eee4eca7 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 2 Aug 2013 13:17:28 -0400 Subject: [PATCH] Easier to group bindings. Perf improvements. --- .../admin/templates/email_logs.js.handlebars | 32 ++-- app/assets/javascripts/application.js.erb | 2 - .../discourse/helpers/grouped_each.js | 111 +++++++++++++ .../list/basic_topic_list.js.handlebars | 20 +-- .../list/topic_list_item.js.handlebars | 146 +++++++++--------- .../templates/list/topics.js.handlebars | 4 +- .../discourse/templates/post.js.handlebars | 2 +- .../discourse/views/embedded_post_view.js | 7 +- .../views/list/topic_list_item_view.js | 9 +- .../views/modal/option_boolean_view.js | 9 +- .../javascripts/discourse/views/post_view.js | 10 +- .../discourse/views/raw_div_view.js | 11 -- .../views/search/search_results_type_view.js | 10 +- .../discourse/views/user/user_stream_view.js | 8 +- .../javascripts/discourse/views/view.js | 14 ++ .../javascripts/external_development/ember.js | 96 ++++++------ .../external_development/group-helper.js | 23 --- .../external_production/group-helper.js | 23 --- test/javascripts/test_helper.js | 1 - 19 files changed, 284 insertions(+), 254 deletions(-) create mode 100644 app/assets/javascripts/discourse/helpers/grouped_each.js delete mode 100644 app/assets/javascripts/discourse/views/raw_div_view.js delete mode 100644 app/assets/javascripts/external_development/group-helper.js delete mode 100644 app/assets/javascripts/external_production/group-helper.js diff --git a/app/assets/javascripts/admin/templates/email_logs.js.handlebars b/app/assets/javascripts/admin/templates/email_logs.js.handlebars index b61e4f6fc88..f937ee6bc53 100644 --- a/app/assets/javascripts/admin/templates/email_logs.js.handlebars +++ b/app/assets/javascripts/admin/templates/email_logs.js.handlebars @@ -10,22 +10,22 @@ {{#if model.length}} - {{#group}} - {{#collection contentBinding="model" tagName="tbody" itemTagName="tr"}} - {{date created_at}} - - {{#if user}} - {{#linkTo 'adminUser' user}}{{avatar user imageSize="tiny"}}{{/linkTo}} - {{#linkTo 'adminUser' user}}{{user.username}}{{/linkTo}} - {{else}} - — - {{/if}} - - {{to_address}} - {{email_type}} - {{reply_key}} - {{/collection}} - {{/group}} + {{#groupedEach model}} + + {{date created_at}} + + {{#if user}} + {{#linkTo 'adminUser' user}}{{avatar user imageSize="tiny"}}{{/linkTo}} + {{#linkTo 'adminUser' user}}{{user.username}}{{/linkTo}} + {{else}} + — + {{/if}} + + {{to_address}} + {{email_type}} + {{reply_key}} + + {{/groupedEach}} {{/if}} diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index dc1dadfea83..35acdbfeefb 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -23,10 +23,8 @@ require_asset ("./external/handlebars.js") if Rails.env.development? require_asset ("./external_development/ember.js") - require_asset ("./external_development/group-helper.js") else require_asset ("./external_production/ember.js") - require_asset ("./external_production/group-helper.js") end require_asset ("./main_include.js") diff --git a/app/assets/javascripts/discourse/helpers/grouped_each.js b/app/assets/javascripts/discourse/helpers/grouped_each.js new file mode 100644 index 00000000000..d59409117e4 --- /dev/null +++ b/app/assets/javascripts/discourse/helpers/grouped_each.js @@ -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(); +}); \ No newline at end of file diff --git a/app/assets/javascripts/discourse/templates/list/basic_topic_list.js.handlebars b/app/assets/javascripts/discourse/templates/list/basic_topic_list.js.handlebars index 60b0846a800..9672cffa5a8 100644 --- a/app/assets/javascripts/discourse/templates/list/basic_topic_list.js.handlebars +++ b/app/assets/javascripts/discourse/templates/list/basic_topic_list.js.handlebars @@ -10,8 +10,8 @@ {{i18n activity}} - {{#group}} - {{#collection contentBinding="view.topics" tagName="tbody" itemTagName="tr"}} + {{#groupedEach view.topics}} + {{{unbound fancy_title}}} {{#if unread}} @@ -21,35 +21,35 @@ {{unbound new_posts}} {{/if}} {{#if unseen}} - + {{/if}} {{categoryLink category}} - {{number posts_count numberKey="posts_long"}} + {{number posts_count numberKey="posts_long"}} {{#if like_count}} - {{like_count}} + {{unbound like_count}} {{/if}} {{number views numberKey="views_long"}} {{#if bumped}} - {{unboundAge created_at}} + {{unboundAge created_at}} - {{unboundAge bumped_at}} + {{unboundAge bumped_at}} {{else}} - {{unboundAge created_at}} + {{unboundAge created_at}} {{/if}} - {{/collection}} - {{/group}} + + {{/groupedEach}} \ No newline at end of file diff --git a/app/assets/javascripts/discourse/templates/list/topic_list_item.js.handlebars b/app/assets/javascripts/discourse/templates/list/topic_list_item.js.handlebars index 0925d81e044..ccf18fcb2ed 100644 --- a/app/assets/javascripts/discourse/templates/list/topic_list_item.js.handlebars +++ b/app/assets/javascripts/discourse/templates/list/topic_list_item.js.handlebars @@ -1,76 +1,76 @@ - {{#if controller.currentUser.id}} - - - +{{#if controller.currentUser.id}} + + + +{{/if}} + + + + {{#if controller.rankDetailsVisible}} +
+

{{rank_details.hot_topic_type}}

+

+ ({{float rank_details.random_bias}} * {{float rank_details.random_multiplier}}) + ({{float rank_details.days_ago_bias}} * {{float rank_details.days_ago_multiplier}}) = {{float rank_details.ranking_score}} + +

+
{{/if}} - - - {{#if controller.rankDetailsVisible}} -
-

{{rank_details.hot_topic_type}}

-

- ({{float rank_details.random_bias}} * {{float rank_details.random_multiplier}}) + ({{float rank_details.days_ago_bias}} * {{float rank_details.days_ago_multiplier}}) = {{float rank_details.ranking_score}} - -

-
- {{/if}} - - {{topicStatus topic=this}} - {{{topicLink this}}} - {{#if unread}} - {{unread}} - {{/if}} - {{#if displayNewPosts}} - {{displayNewPosts}} - {{/if}} - {{#if unseen}} - - {{/if}} - - {{#if hasExcerpt}} -
- {{excerpt}} - {{#if excerptTruncated}} - {{#unless canClearPin}}{{i18n read_more}}{{/unless}} - {{/if}} - {{#if canClearPin}} - {{i18n topic.clear_pin.title}} - {{/if}} -
- {{/if}} - - - - {{categoryLink category}} - - - - {{#each posters}} - {{avatar this usernamePath="user.username" imageSize="small"}} - {{/each}} - - - {{number posts_count numberKey="posts_long"}} - - - {{#if like_count}} - {{number like_count numberKey="likes_long"}} - {{/if}} - - - {{number views numberKey="views_long"}} - - {{#if bumped}} - - {{unboundAge created_at}} - - - {{unboundAge bumped_at}} - - {{else}} - - {{unboundAge created_at}} - - + {{topicStatus topic=this}} + {{{topicLink this}}} + {{#if unread}} + {{unread}} {{/if}} + {{#if displayNewPosts}} + {{displayNewPosts}} + {{/if}} + {{#if unseen}} + + {{/if}} + + {{#if hasExcerpt}} +
+ {{excerpt}} + {{#if excerptTruncated}} + {{#unless canClearPin}}{{i18n read_more}}{{/unless}} + {{/if}} + {{#if canClearPin}} + {{i18n topic.clear_pin.title}} + {{/if}} +
+ {{/if}} + + + + {{categoryLink category}} + + + + {{#each posters}} + {{avatar this usernamePath="user.username" imageSize="small"}} + {{/each}} + + +{{number posts_count numberKey="posts_long"}} + + + {{#if like_count}} + {{number like_count numberKey="likes_long"}} + {{/if}} + + +{{number views numberKey="views_long"}} + +{{#if bumped}} + + {{unboundAge created_at}} + + + {{unboundAge bumped_at}} + +{{else}} + + {{unboundAge created_at}} + + +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/list/topics.js.handlebars b/app/assets/javascripts/discourse/templates/list/topics.js.handlebars index 23c6b850b6a..e9a25630774 100644 --- a/app/assets/javascripts/discourse/templates/list/topics.js.handlebars +++ b/app/assets/javascripts/discourse/templates/list/topics.js.handlebars @@ -41,9 +41,7 @@ {{/if}} - {{#group}} - {{collection contentBinding="topics" tagName="tbody" itemViewClass="Discourse.TopicListItemView"}} - {{/group}} + {{collection contentBinding="topics" tagName="tbody" itemViewClass="Discourse.TopicListItemView"}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/post.js.handlebars b/app/assets/javascripts/discourse/templates/post.js.handlebars index cd719a2f3d9..3c3cbab395a 100644 --- a/app/assets/javascripts/discourse/templates/post.js.handlebars +++ b/app/assets/javascripts/discourse/templates/post.js.handlebars @@ -47,7 +47,7 @@ {{/unless}} - {{view Discourse.RawDivView class="cooked" contentBinding="cooked"}} +
{{{cooked}}}
{{view Discourse.PostMenuView postBinding="this" postViewBinding="view"}} {{view Discourse.RepliesView contentBinding="replies" postViewBinding="view"}} diff --git a/app/assets/javascripts/discourse/views/embedded_post_view.js b/app/assets/javascripts/discourse/views/embedded_post_view.js index aff7e9e2cb4..2a9c0082d04 100644 --- a/app/assets/javascripts/discourse/views/embedded_post_view.js +++ b/app/assets/javascripts/discourse/views/embedded_post_view.js @@ -6,15 +6,10 @@ @namespace Discourse @module Discourse **/ -Discourse.EmbeddedPostView = Discourse.View.extend({ +Discourse.EmbeddedPostView = Discourse.GroupedView.extend({ templateName: 'embedded_post', classNames: ['reply'], - init: function() { - this._super(); - this.set('context', this.get('content')); - }, - didInsertElement: function() { Discourse.ScreenTrack.instance().track(this.get('elementId'), this.get('post.post_number')); }, diff --git a/app/assets/javascripts/discourse/views/list/topic_list_item_view.js b/app/assets/javascripts/discourse/views/list/topic_list_item_view.js index 5a69bd02281..2d1f6d71456 100644 --- a/app/assets/javascripts/discourse/views/list/topic_list_item_view.js +++ b/app/assets/javascripts/discourse/views/list/topic_list_item_view.js @@ -2,11 +2,11 @@ This view handles the rendering of a topic in a list @class TopicListItemView - @extends Discourse.View + @extends Discourse.GroupedView @namespace Discourse @module Discourse **/ -Discourse.TopicListItemView = Discourse.View.extend({ +Discourse.TopicListItemView = Discourse.GroupedView.extend({ tagName: 'tr', templateName: 'list/topic_list_item', 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'), - init: function() { - this._super(); - this.set('context', this.get('content')); - }, - highlight: function() { var $topic = this.$(); var originalCol = $topic.css('backgroundColor'); diff --git a/app/assets/javascripts/discourse/views/modal/option_boolean_view.js b/app/assets/javascripts/discourse/views/modal/option_boolean_view.js index 51b5b8e6eb7..5536a204f4e 100644 --- a/app/assets/javascripts/discourse/views/modal/option_boolean_view.js +++ b/app/assets/javascripts/discourse/views/modal/option_boolean_view.js @@ -6,7 +6,7 @@ @namespace Discourse @module Discourse **/ -Discourse.OptionBooleanView = Discourse.View.extend({ +Discourse.OptionBooleanView = Discourse.GroupedView.extend({ classNames: ['archetype-option'], composerControllerBinding: 'Discourse.router.composerController', templateName: "modal/option_boolean", @@ -16,12 +16,7 @@ Discourse.OptionBooleanView = Discourse.View.extend({ metaData = this.get('parentView.metaData'); metaData.set(this.get('content.key'), this.get('checked') ? 'true' : 'false'); return this.get('controller.controllers.composer').saveDraft(); - }).observes('checked'), - - init: function() { - this._super(); - return this.set('context', this.get('content')); - } + }).observes('checked') }); diff --git a/app/assets/javascripts/discourse/views/post_view.js b/app/assets/javascripts/discourse/views/post_view.js index 961d5fad116..f0dc293dc52 100644 --- a/app/assets/javascripts/discourse/views/post_view.js +++ b/app/assets/javascripts/discourse/views/post_view.js @@ -2,11 +2,11 @@ This view renders a post. @class PostView - @extends Discourse.View + @extends Discourse.GroupedView @namespace Discourse @module Discourse **/ -Discourse.PostView = Discourse.View.extend({ +Discourse.PostView = Discourse.GroupedView.extend({ classNames: ['topic-post', 'clearfix'], templateName: 'post', classNameBindings: ['postTypeClass', @@ -28,18 +28,12 @@ Discourse.PostView = Discourse.View.extend({ }); }.observes('post.cooked'), - init: function() { - this._super(); - this.set('context', this.get('content')); - }, - mouseUp: function(e) { if (this.get('controller.multiSelect') && (e.metaKey || e.ctrlKey)) { this.get('controller').selectPost(this.get('post')); } }, - selected: function() { var selectedPosts = this.get('controller.selectedPosts'); if (!selectedPosts) return false; diff --git a/app/assets/javascripts/discourse/views/raw_div_view.js b/app/assets/javascripts/discourse/views/raw_div_view.js deleted file mode 100644 index 28ef39e8ff7..00000000000 --- a/app/assets/javascripts/discourse/views/raw_div_view.js +++ /dev/null @@ -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')); - } - -}); diff --git a/app/assets/javascripts/discourse/views/search/search_results_type_view.js b/app/assets/javascripts/discourse/views/search/search_results_type_view.js index 2a5248a142f..2c63dd328d4 100644 --- a/app/assets/javascripts/discourse/views/search/search_results_type_view.js +++ b/app/assets/javascripts/discourse/views/search/search_results_type_view.js @@ -8,17 +8,11 @@ **/ Discourse.SearchResultsTypeView = Ember.CollectionView.extend({ tagName: 'ul', - itemViewClass: Ember.View.extend({ + itemViewClass: Discourse.GroupedView.extend({ tagName: 'li', classNameBindings: ['selected'], templateName: Discourse.computed.fmt('parentView.type', "search/%@_result"), - selected: Discourse.computed.propertyEqual('content.index', 'controller.selectedIndex'), - - init: function() { - this._super(); - this.set('context', this.get('content')); - } - + selected: Discourse.computed.propertyEqual('content.index', 'controller.selectedIndex') }) }); diff --git a/app/assets/javascripts/discourse/views/user/user_stream_view.js b/app/assets/javascripts/discourse/views/user/user_stream_view.js index 60de6246b4f..c04271f1fe7 100644 --- a/app/assets/javascripts/discourse/views/user/user_stream_view.js +++ b/app/assets/javascripts/discourse/views/user/user_stream_view.js @@ -13,13 +13,7 @@ Discourse.UserStreamView = Ember.CollectionView.extend(Discourse.LoadMore, { eyelineSelector: '#user-activity .user-stream .item', classNames: ['user-stream'], - itemViewClass: Ember.View.extend({ - templateName: 'user/stream_item', - init: function() { - this._super(); - this.set('context', this.get('content')); - } - }), + itemViewClass: Discourse.GroupedView.extend({ templateName: 'user/stream_item' }), loadMore: function() { var userStreamView = this; diff --git a/app/assets/javascripts/discourse/views/view.js b/app/assets/javascripts/discourse/views/view.js index 0be210a50a5..44f8ba5f14a 100644 --- a/app/assets/javascripts/discourse/views/view.js +++ b/app/assets/javascripts/discourse/views/view.js @@ -9,6 +9,20 @@ **/ 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({ /** diff --git a/app/assets/javascripts/external_development/ember.js b/app/assets/javascripts/external_development/ember.js index eb28f82d2cc..3acfb3ef40d 100755 --- a/app/assets/javascripts/external_development/ember.js +++ b/app/assets/javascripts/external_development/ember.js @@ -1999,7 +1999,7 @@ function suspendListener(obj, eventName, target, method, callback) { Suspends multiple listeners during a callback. - + @method suspendListeners @for Ember @param obj @@ -2066,7 +2066,7 @@ function watchedEvents(obj) { is skipped, and once listeners are removed. A listener without a target is executed on the passed object. If an array of actions is not passed, the actions stored on the passed object are invoked. - + @method sendEvent @for Ember @param obj @@ -2794,14 +2794,14 @@ Map.create = function() { Map.prototype = { /** This property will change as the number of objects in the map changes. - + @property length @type number @default 0 */ length: 0, - - + + /** Retrieve the value associated with a given key. @@ -4395,7 +4395,7 @@ Ember.computed.alias = function(dependentKey) { @return {Ember.ComputedProperty} computed property which creates an one way computed property to the original value for property. - Where `computed.alias` aliases `get` and `set`, and allows for bidirectional + Where `computed.alias` aliases `get` and `set`, and allows for bidirectional data flow, `computed.oneWay` only provides an aliased `get`. The `set` will not mutate the upstream property, rather causes the current property to become the value set. This causes the downstream property to permentantly @@ -23878,7 +23878,7 @@ helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {}; var buffer = '', stack1, hashTypes, hashContexts, escapeExpression=this.escapeExpression, self=this; function program1(depth0,data) { - + var buffer = '', hashTypes, hashContexts; data.buffer.push("