FIX: Fix rerendering issues on some components.

This commit is contained in:
Robin Ward 2014-12-08 12:59:04 -05:00
parent 7069ee16e2
commit 5fd3f2547c
18 changed files with 96 additions and 162 deletions

View file

@ -1,23 +1,16 @@
/**
This component handles rendering of what actions have been taken on a post. It uses
buffer rendering for performance rather than a template.
import StringBuffer from 'discourse/mixins/string-buffer';
@class ActionsHistoryComponent
@extends Em.Component
@namespace Discourse
@module Discourse
**/
export default Em.Component.extend({
export default Em.Component.extend(StringBuffer, {
tagName: 'section',
classNameBindings: [':post-actions', 'hidden'],
actionsHistory: Em.computed.alias('post.actionsHistory'),
emptyHistory: Em.computed.empty('actionsHistory'),
hidden: Em.computed.and('emptyHistory', 'post.notDeleted'),
shouldRerender: Discourse.View.renderIfChanged('actionsHistory.@each', 'actionsHistory.users.length', 'post.deleted'),
rerenderTriggers: ['actionsHistory.@each', 'actionsHistory.users.length', 'post.deleted'],
// This was creating way too many bound ifs and subviews in the handlebars version.
render: function(buffer) {
renderString: function(buffer) {
if (!this.get('emptyHistory')) {
this.get('actionsHistory').forEach(function(c) {
buffer.push("<div class='post-action'>");

View file

@ -1,8 +1,9 @@
export default Ember.Component.extend({
import StringBuffer from 'discourse/mixins/string-buffer';
shouldRerender: Discourse.View.renderIfChanged("site.isReadOnly"),
export default Ember.Component.extend(StringBuffer, {
rerenderTriggers: ['site.isReadOnly'],
render: function(buffer) {
renderString: function(buffer) {
var notices = [];
if (this.site.get("isReadOnly")) {

View file

@ -1,17 +1,11 @@
/**
This view handles rendering of a navigation item
import StringBuffer from 'discourse/mixins/string-buffer';
@class NavigationItemComponent
@extends Ember.Component
@namespace Discourse
@module Discourse
**/
export default Ember.Component.extend({
export default Ember.Component.extend(StringBuffer, {
tagName: 'li',
classNameBindings: ['active', 'content.hasIcon:has-icon'],
attributeBindings: ['title'],
hidden: Em.computed.not('content.visible'),
shouldRerender: Discourse.View.renderIfChanged('content.count'),
rerenderTriggers: ['content.count'],
title: function() {
var categoryName = this.get('content.categoryName'),
@ -42,7 +36,7 @@ export default Ember.Component.extend({
return I18n.t("filters." + name + ".title", extra);
}.property('content.count'),
render: function(buffer) {
renderString: function(buffer) {
var content = this.get('content');
buffer.push("<a href='" + content.get('href') + "'>");
if (content.get('hasIcon')) {

View file

@ -1,8 +1,12 @@
var MAX_SHOWN = 5;
export default Em.Component.extend({
import StringBuffer from 'discourse/mixins/string-buffer';
export default Em.Component.extend(StringBuffer, {
classNameBindings: [':gutter'],
rerenderTriggers: ['expanded'],
// Roll up links to avoid duplicates
collapsed: function() {
var seen = {},
@ -21,7 +25,7 @@ export default Em.Component.extend({
return result;
}.property('links'),
render: function(buffer) {
renderString: function(buffer) {
var links = this.get('collapsed'),
toRender = links,
collapsed = !this.get('expanded');
@ -62,10 +66,6 @@ export default Em.Component.extend({
}
},
_rerenderIfNeeded: function() {
this.rerender();
}.observes('expanded'),
click: function(e) {
var $target = $(e.target);
if ($target.hasClass('toggle-more')) {

View file

@ -1,8 +1,10 @@
export default Ember.Component.extend({
tagName: 'h2',
import StringBuffer from 'discourse/mixins/string-buffer';
_shouldRerender: Discourse.View.renderIfChanged('period.title'),
render: function(buffer) {
export default Ember.Component.extend(StringBuffer, {
tagName: 'h2',
rerenderTriggers: ['period.title'],
renderString: function(buffer) {
buffer.push("<i class='fa fa-calendar-o'></i> " + this.get('period.title'));
}
});

View file

@ -1,3 +1,4 @@
import StringBuffer from 'discourse/mixins/string-buffer';
// Creates a link
function link(buffer, prop, url, cssClass, i18nKey, text) {
@ -7,12 +8,12 @@ function link(buffer, prop, url, cssClass, i18nKey, text) {
buffer.push("<a href='" + url + "' class='badge " + cssClass + " badge-notification' title='" + title + "'>" + (text || prop) + "</a>\n");
}
export default Ember.Component.extend({
export default Ember.Component.extend(StringBuffer, {
tagName: 'span',
classNameBindings: [':topic-post-badges'],
_shouldRerender: Discourse.View.renderIfChanged('url', 'unread', 'newPosts', 'unseen'),
rerenderTriggers: ['url', 'unread', 'newPosts', 'unseen'],
render: function(buffer) {
renderString: function(buffer) {
var url = this.get('url');
link(buffer, this.get('unread'), url, 'unread', 'unread_posts');

View file

@ -1,18 +1,12 @@
/**
This view is for rendering an icon representing the status of a topic
import StringBuffer from 'discourse/mixins/string-buffer';
@class TopicStatusComponent
@extends Ember.Component
@namespace Discourse
@module Discourse
**/
export default Ember.Component.extend({
export default Ember.Component.extend(StringBuffer, {
classNames: ['topic-statuses'],
hasDisplayableStatus: Em.computed.or('topic.archived','topic.closed', 'topic.pinned', 'topic.unpinned', 'topic.invisible', 'topic.archetypeObject.notDefault', 'topic.is_warning'),
shouldRerender: Discourse.View.renderIfChanged('topic.archived', 'topic.closed', 'topic.pinned', 'topic.visible', 'topic.unpinned', 'topic.is_warning'),
rerenderTriggers: ['topic.archived', 'topic.closed', 'topic.pinned', 'topic.visible', 'topic.unpinned', 'topic.is_warning'],
didInsertElement: function(){
watchClick: function(){
var self = this;
this.$('a').click(function(){
@ -27,13 +21,13 @@ export default Ember.Component.extend({
return false;
});
},
}.on('didInsertElement'),
canAct: function() {
return Discourse.User.current() && !this.get('disableActions');
}.property('disableActions'),
render: function(buffer) {
renderString: function(buffer) {
if (!this.get('hasDisplayableStatus')) { return; }
var self = this;
@ -41,7 +35,7 @@ export default Ember.Component.extend({
var renderIconIf = function(conditionProp, name, key, actionable) {
if (!self.get(conditionProp)) { return; }
var title = Handlebars.Utils.escapeExpression(I18n.t("topic_statuses." + key + ".help"));
var startTag = actionable ? "a href='#'" : "span";
var startTag = actionable ? "a href" : "span";
var endTag = actionable ? "a" : "span";
buffer.push("<" + startTag + " title='" + title + "' class='topic-status'><i class='fa fa-" + name + "'></i></" + endTag + ">");

View file

@ -49,14 +49,13 @@ Ember.Handlebars.registerBoundHelper("boundI18n", function(property, options) {
@for Handlebars
**/
Ember.Handlebars.registerHelper('countI18n', function(key, options) {
var view = Discourse.View.extend({
var view = Discourse.View.extend(Discourse.StringBuffer, {
tagName: 'span',
shouldRerender: Discourse.View.renderIfChanged('count', 'suffix'),
rerenderTriggers: ['count', 'suffix'],
render: function(buffer) {
renderString: function(buffer) {
buffer.push(I18n.t(key + (this.get('suffix') || ''), { count: this.get('count') }));
}
});
return Ember.Handlebars.helpers.view.call(this, view, options);
});

View file

@ -1,8 +1,10 @@
export default Ember.Component.extend({
import StringBuffer from 'discourse/mixins/string-buffer';
export default Ember.Component.extend(StringBuffer, {
tagName: 'li',
classNameBindings: ['active', 'noGlyph'],
shouldRerender: Discourse.View.renderIfChanged('content.count', 'count'),
rerenderTriggers: ['content.count', 'count'],
noGlyph: Em.computed.empty('icon'),
active: function() {
@ -37,7 +39,7 @@ export default Ember.Component.extend({
return this.get('content.description') || I18n.t("user.filters.all");
}.property('content.description'),
render: function(buffer) {
renderString: function(buffer) {
buffer.push("<a href='" + this.get('url') + "'>");
var icon = this.get('icon');
if (icon) {

View file

@ -1,4 +1,6 @@
export default Discourse.View.extend({
import StringBuffer from 'discourse/mixins/string-buffer';
export default Discourse.View.extend(StringBuffer, {
tagName: 'button',
classNameBindings: [':btn', ':standard', 'dropDownToggle'],
attributeBindings: ['title', 'data-toggle', 'data-share-url'],
@ -12,7 +14,7 @@ export default Discourse.View.extend({
return I18n.t(this.get('textKey'));
}.property('textKey'),
render: function(buffer) {
renderString: function(buffer) {
if (this.renderIcon) {
this.renderIcon(buffer);
}

View file

@ -1,41 +1,45 @@
export default Discourse.View.extend({
classNameBindings: [':btn-group', 'hidden'],
shouldRerender: Discourse.View.renderIfChanged('text', 'longDescription'),
import StringBuffer from 'discourse/mixins/string-buffer';
didInsertElement: function() {
var self = this;
export default Discourse.View.extend(StringBuffer, {
classNameBindings: [':btn-group', 'hidden'],
rerenderTriggers: ['text', 'longDescription'],
_bindClick: function() {
// If there's a click handler, call it
if (self.clicked) {
self.$('ul li').on('click.dropdown-button', function(e) {
if (this.clicked) {
var self = this;
this.$().on('click.dropdown-button', 'ul li', function(e) {
e.preventDefault();
if ($(e.currentTarget).data('id') !== self.get('activeItem'))
if ($(e.currentTarget).data('id') !== self.get('activeItem')) {
self.clicked($(e.currentTarget).data('id'));
}
self.$('.dropdown-toggle').dropdown('toggle');
return false;
});
}
},
}.on('didInsertElement'),
willDestroyElement: function() {
this.$('ul li').off('click.dropdown-button');
},
_unbindClick: function() {
this.$().off('click.dropdown-button', 'ul li');
}.on('willDestroyElement'),
render: function(buffer) {
var self = this;
renderString: function(buffer) {
buffer.push("<h4 class='title'>" + self.get('title') + "</h4>");
buffer.push("<h4 class='title'>" + this.get('title') + "</h4>");
buffer.push("<button class='btn standard dropdown-toggle' data-toggle='dropdown'>");
buffer.push(self.get('text'));
buffer.push(this.get('text'));
buffer.push("</button>");
buffer.push("<ul class='dropdown-menu'>");
_.each(self.get('dropDownContent'), function(row) {
var self = this;
this.get('dropDownContent').forEach(function(row) {
var id = row.id,
title = row.title,
iconClass = row.styleClasses,
description = row.description,
className = (self.get('activeItem') === id? 'disabled': '');
className = (self.get('activeItem') === id ? 'disabled': '');
buffer.push("<li data-id=\"" + id + "\" class=\"" + className + "\"><a href='#'>");
buffer.push("<li data-id=\"" + id + "\" class=\"" + className + "\"><a href>");
buffer.push("<span class='icon " + iconClass + "'></span>");
buffer.push("<div><span class='title'>" + title + "</span>");
buffer.push("<span>" + description + "</span></div>");
@ -44,7 +48,7 @@ export default Discourse.View.extend({
buffer.push("</ul>");
var desc = self.get('longDescription');
var desc = this.get('longDescription');
if (desc) {
buffer.push("<p>");
buffer.push(desc);

View file

@ -1,19 +1,13 @@
/**
This view handles rendering a tip when a field on a form is invalid
import StringBuffer from 'discourse/mixins/string-buffer';
@class InputTipView
@extends Discourse.View
@namespace Discourse
@module Discourse
**/
export default Discourse.View.extend({
export default Discourse.View.extend(StringBuffer, {
classNameBindings: [':tip', 'good', 'bad'],
rerenderTriggers: ['validation'],
shouldRerender: Discourse.View.renderIfChanged('validation'),
bad: Em.computed.alias('validation.failed'),
good: Em.computed.not('bad'),
render: function(buffer) {
renderString: function(buffer) {
var reason = this.get('validation.reason');
if (reason) {
var icon = this.get('good') ? 'fa-check' : 'fa-times';

View file

@ -1,3 +1,5 @@
import StringBuffer from 'discourse/mixins/string-buffer';
// Helper class for rendering a button
export var Button = function(action, label, icon, opts) {
this.action = action;
@ -28,11 +30,11 @@ Button.prototype.render = function(buffer) {
var hiddenButtons;
export default Discourse.View.extend({
export default Discourse.View.extend(StringBuffer, {
tagName: 'section',
classNames: ['post-menu-area', 'clearfix'],
shouldRerender: Discourse.View.renderIfChanged(
rerenderTriggers: [
'post.deleted_at',
'post.flagsAvailable.@each',
'post.reply_count',
@ -43,13 +45,13 @@ export default Discourse.View.extend({
'post.topic.deleted_at',
'post.replies.length',
'post.wiki',
'collapsed'),
'collapsed'],
_collapsedByDefault: function() {
this.set('collapsed', true);
}.on('init'),
render: function(buffer) {
renderString: function(buffer) {
var post = this.get('post');
buffer.push("<nav class='post-controls'>");

View file

@ -6,7 +6,7 @@ export default ButtonView.extend({
helpKeyBinding: 'controller.starTooltipKey',
attributeBindings: ['disabled'],
shouldRerender: Discourse.View.renderIfChanged('controller.starred'),
rerenderTriggers: ['controller.starred'],
click: function() {
this.get('controller').send('toggleStar');

View file

@ -1,19 +1,15 @@
/**
This view is used for rendering the notification that a topic will
automatically close.
import StringBuffer from 'discourse/mixins/string-buffer';
@class TopicClosingView
@extends Discourse.View
@namespace Discourse
@module Discourse
**/
export default Discourse.View.extend({
export default Discourse.View.extend(StringBuffer, {
elementId: 'topic-closing-info',
delayedRerender: null,
shouldRerender: Discourse.View.renderIfChanged('topic.closed', 'topic.details.{auto_close_at,auto_close_based_on_last_post,auto_close_hours}'),
rerenderTriggers: ['topic.closed',
'topic.details.auto_close_at',
'topic.details.auto_close_based_on_last_post',
'topic.details.auto_close_hours'],
render: function(buffer) {
renderString: function(buffer) {
if (!this.present('topic.details.auto_close_at')) return;
if (this.get("topic.closed")) return;
@ -36,8 +32,7 @@ export default Discourse.View.extend({
}
var basedOnLastPost = this.get("topic.details.auto_close_based_on_last_post");
var key = basedOnLastPost ? 'topic.auto_close_notice_based_on_last_post' : 'topic.auto_close_notice'
var key = basedOnLastPost ? 'topic.auto_close_notice_based_on_last_post' : 'topic.auto_close_notice';
var autoCloseHours = this.get("topic.details.auto_close_hours") || 0;
buffer.push('<h3><i class="fa fa-clock-o"></i> ');

View file

@ -6,7 +6,10 @@ import DiscourseContainerView from 'discourse/views/container';
export default DiscourseContainerView.extend({
classNameBindings: ['hidden', ':topic-map'],
shouldRerender: Discourse.View.renderIfChanged('topic.posts_count'),
_postsChanged: function() {
Ember.run.once(this, 'rerender');
}.observes('topic.posts_count'),
hidden: function() {
if (!this.get('post.firstPost')) return true;

View file

@ -40,16 +40,8 @@ Discourse.View.reopenClass({
});
},
/**
Returns an observer that will re-render if properties change. This is useful for
views where rendering is done to a buffer manually and need to know when to trigger
a new render call.
@method renderIfChanged
@params {String} propertyNames*
@return {Function} observer
**/
renderIfChanged: function() {
Em.warn("`rerenderIfChanged` is deprecated. Use the `StringBuffer` mixin with `rerenderTriggers` instead.");
var args = Array.prototype.slice.call(arguments, 0);
args.unshift(function () {
Ember.run.once(this, 'rerender');

View file

@ -30,47 +30,3 @@ test("registerHelper: enables embedding a child view in a parent view via dedica
equal(parentView.$("#child").length, 1, "child view registered as helper is appended to the parent view");
equal(parentView.$("#child").text(), "foo", "child view registered as helper gets parameters provided during helper invocation in parent's template");
});
test("renderIfChanged: rerenders the whole view template when one of registered view fields changes", function() {
var view, rerenderSpy;
var viewRerendersOnceWhen = function(message, changeCallback) {
rerenderSpy.reset();
Ember.run(function() { changeCallback(); });
ok(rerenderSpy.calledOnce, "view rerenders when " + message);
};
var viewDoesNotRerenderWhen = function(message, changeCallback) {
rerenderSpy.reset();
Ember.run(function() { changeCallback(); });
ok(!rerenderSpy.called, "view does not rerender when " + message);
};
view = Ember.View.extend({
shouldRerender: Discourse.View.renderIfChanged("simple", "complex.@each.nested")
}).create({
simple: "initial value",
complex: [Ember.Object.create({nested: "initial value"})],
unregistered: "initial value"
});
rerenderSpy = sinon.spy(view, "rerender");
Ember.run(function() {
view.appendTo("#qunit-fixture");
});
viewRerendersOnceWhen("a simple field (holding a string) changes", function() {
view.set("simple", "updated value");
});
viewRerendersOnceWhen("a nested sub-field of a complex field (holding an array of objects) changes", function() {
view.get("complex").objectAt(0).set("nested", "updated value");
});
viewDoesNotRerenderWhen("unregistered field changes", function() {
view.set("unregistered", "updated value");
});
});