diff --git a/app/assets/javascripts/discourse/components/mount-widget.js.es6 b/app/assets/javascripts/discourse/components/mount-widget.js.es6 index 869e4a0ec..8958cfd05 100644 --- a/app/assets/javascripts/discourse/components/mount-widget.js.es6 +++ b/app/assets/javascripts/discourse/components/mount-widget.js.es6 @@ -1,6 +1,6 @@ import { diff, patch } from 'virtual-dom'; import { WidgetClickHook } from 'discourse/widgets/click-hook'; -import { renderedKey } from 'discourse/widgets/widget'; +import { renderedKey, queryRegistry } from 'discourse/widgets/widget'; const _cleanCallbacks = {}; export function addWidgetCleanCallback(widgetName, fn) { @@ -17,7 +17,9 @@ export default Ember.Component.extend({ init() { this._super(); - this._widgetClass = this.container.lookupFactory(`widget:${this.get('widget')}`); + const name = this.get('widget'); + + this._widgetClass = queryRegistry(name) || this.container.lookupFactory(`widget:${name}`); this._connected = []; }, diff --git a/app/assets/javascripts/discourse/widgets/click-hook.js.es6 b/app/assets/javascripts/discourse/widgets/click-hook.js.es6 index 2b6066eb5..815f2b10d 100644 --- a/app/assets/javascripts/discourse/widgets/click-hook.js.es6 +++ b/app/assets/javascripts/discourse/widgets/click-hook.js.es6 @@ -40,7 +40,7 @@ WidgetClickHook.setupDocumentCallback = function() { while (node) { const widget = node[CLICK_ATTRIBUTE_NAME]; if (widget) { - return widget.click(e); + return widget.rerenderResult(() => widget.click(e)); } node = node.parentNode; } diff --git a/app/assets/javascripts/discourse/widgets/post-gap.js.es6 b/app/assets/javascripts/discourse/widgets/post-gap.js.es6 index d14d1b811..68e0be1ad 100644 --- a/app/assets/javascripts/discourse/widgets/post-gap.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-gap.js.es6 @@ -18,8 +18,6 @@ export default createWidget('post-gap', { if (state.loading) { return; } state.loading = true; - this.scheduleRerender(); - const args = { gap: attrs.gap, post: this.model }; return this.sendWidgetAction(attrs.pos === 'before' ? 'fillGapBefore' : 'fillGapAfter', args); } diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6 index e482398d8..8da490a50 100644 --- a/app/assets/javascripts/discourse/widgets/post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post.js.es6 @@ -401,9 +401,7 @@ export default createWidget('post', { const likeAction = post.get('likeAction'); if (likeAction && likeAction.get('canToggle')) { - const promise = likeAction.togglePromise(post); - this.scheduleRerender(); - return promise; + return likeAction.togglePromise(post); } }, diff --git a/app/assets/javascripts/discourse/widgets/widget.js.es6 b/app/assets/javascripts/discourse/widgets/widget.js.es6 index 4b2cda659..ec470c0fe 100644 --- a/app/assets/javascripts/discourse/widgets/widget.js.es6 +++ b/app/assets/javascripts/discourse/widgets/widget.js.es6 @@ -15,6 +15,10 @@ export function renderedKey(key) { delete _dirty[key]; } +export function queryRegistry(name) { + return _registry[name]; +} + const _decorators = {}; export function decorateWidget(widgetName, cb) { @@ -203,7 +207,7 @@ export default class Widget { } } - sendComponentAction(name, param) { + _sendComponentAction(name, param) { const view = this._findAncestorWithProperty('_emberView'); let promise; @@ -233,9 +237,7 @@ export default class Widget { } } - if (promise) { - return promise.then(() => this.scheduleRerender()); - } + return this.rerenderResult(() => promise); } findAncestorModel() { @@ -245,19 +247,25 @@ export default class Widget { } } - sendWidgetAction(name, param) { - const widget = this._findAncestorWithProperty(name); - if (widget) { - const result = widget[name](param); - if (result && result.then) { - return result.then(() => this.scheduleRerender()); - } else { - this.scheduleRerender(); - return result; - } + rerenderResult(fn) { + this.scheduleRerender(); + const result = fn(); + // re-render after any promises complete, too! + if (result && result.then) { + return result.then(() => this.scheduleRerender()); } + return result; + } - return this.sendComponentAction(name, param || this.findAncestorModel()); + sendWidgetAction(name, param) { + return this.rerenderResult(() => { + const widget = this._findAncestorWithProperty(name); + if (widget) { + return widget[name](param); + } + + return this._sendComponentAction(name, param || this.findAncestorModel()); + }); } } diff --git a/test/javascripts/widgets/widget-test.js.es6 b/test/javascripts/widgets/widget-test.js.es6 new file mode 100644 index 000000000..64ed5b7d4 --- /dev/null +++ b/test/javascripts/widgets/widget-test.js.es6 @@ -0,0 +1,194 @@ +import { moduleForWidget, widgetTest } from 'helpers/widget-test'; +import { decorateWidget, createWidget } from 'discourse/widgets/widget'; + +moduleForWidget('base'); + +widgetTest('widget attributes are passed in via args', { + template: `{{mount-widget widget="hello-test" args=args}}`, + + setup() { + createWidget('hello-test', { + tagName: 'div.test', + + html(attrs) { + return `Hello ${attrs.name}`; + }, + }); + + this.set('args', { name: 'Robin' }); + }, + + test(assert) { + assert.equal(this.$('.test').text(), "Hello Robin"); + } +}); + +widgetTest('buildClasses', { + template: `{{mount-widget widget="classname-test" args=args}}`, + + setup() { + createWidget('classname-test', { + tagName: 'div.test', + + buildClasses(attrs) { + return ['static', attrs.dynamic]; + } + }); + + this.set('args', { dynamic: 'cool-class' }); + }, + + test(assert) { + assert.ok(this.$('.test.static.cool-class').length, 'it has all the classes'); + } +}); + +widgetTest('buildAttributes', { + template: `{{mount-widget widget="attributes-test" args=args}}`, + + setup() { + createWidget('attributes-test', { + tagName: 'div.test', + + buildAttributes(attrs) { + return { "data-evil": 'trout', "aria-label": attrs.label }; + } + }); + + this.set('args', { label: 'accessibility' }); + }, + + test(assert) { + assert.ok(this.$('.test[data-evil=trout]').length); + assert.ok(this.$('.test[aria-label=accessibility]').length); + } +}); + +widgetTest('buildId', { + template: `{{mount-widget widget="id-test" args=args}}`, + + setup() { + createWidget('id-test', { + buildId(attrs) { + return `test-${attrs.id}`; + } + }); + + this.set('args', { id: 1234 }); + }, + + test(assert) { + assert.ok(this.$('#test-1234').length); + } +}); + +widgetTest('widget state', { + template: `{{mount-widget widget="state-test"}}`, + + setup() { + createWidget('state-test', { + tagName: 'button.test', + + defaultState() { + return { clicks: 0 }; + }, + + html(attrs, state) { + return `${state.clicks} clicks`; + }, + + click() { + this.state.clicks++; + } + }); + }, + + test(assert) { + assert.ok(this.$('button.test').length, 'it renders the button'); + assert.equal(this.$('button.test').text(), "0 clicks"); + + click(this.$('button')); + andThen(() => { + assert.equal(this.$('button.test').text(), "1 clicks"); + }); + } +}); + +widgetTest('widget update with promise', { + template: `{{mount-widget widget="promise-test"}}`, + + setup() { + createWidget('promise-test', { + tagName: 'button.test', + + html(attrs, state) { + return state.name || "No name"; + }, + + click() { + return new Ember.RSVP.Promise(resolve => { + Ember.run.next(() => { + this.state.name = "Robin"; + resolve(); + }); + }); + } + }); + }, + + test(assert) { + assert.equal(this.$('button.test').text(), "No name"); + + click(this.$('button')); + andThen(() => { + assert.equal(this.$('button.test').text(), "Robin"); + }); + } +}); + +widgetTest('widget attaching', { + template: `{{mount-widget widget="attach-test"}}`, + + setup() { + createWidget('test-embedded', { tagName: 'div.embedded' }); + + createWidget('attach-test', { + tagName: 'div.container', + html() { + return this.attach('test-embedded'); + }, + }); + }, + + test(assert) { + assert.ok(this.$('.container').length, "renders container"); + assert.ok(this.$('.container .embedded').length, "renders attached"); + } +}); + +widgetTest('widget decorating', { + template: `{{mount-widget widget="decorate-test"}}`, + + setup() { + createWidget('decorate-test', { + tagName: 'div.decorate', + html() { + return "main content"; + }, + }); + + decorateWidget('decorate-test:before', dec => { + return dec.h('b', 'before'); + }); + + decorateWidget('decorate-test:after', dec => { + return dec.h('i', 'after'); + }); + }, + + test(assert) { + assert.ok(this.$('.decorate').length); + assert.equal(this.$('.decorate b').text(), 'before'); + assert.equal(this.$('.decorate i').text(), 'after'); + } +});