Low-level helper plugin API for data binding
Utility wrapper for tracking Polymer element properties updates.
Usage example:
``` js
Gerrit.install(plugin => {
plugin.getDomHook('change-view').onAttached(element => {
if (!element.content) { return; }
plugin.attributeHelper(element.content)
.get('change')
.then(change => {
// Is executed once on switching to change view.
});
});
plugin.getDomHook('reply-text').onAttached(element => {
if (!element.content) { return; }
plugin.attributeHelper(element.content)
.bind('text', replyText => {
// Is called every time reply text changes.
});
});
});
```
Change-Id: Ia95364df58489f71ea1fd591a160b73ac1d60e96
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html
new file mode 100644
index 0000000..c495c94
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html
@@ -0,0 +1,21 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-attribute-helper">
+ <script src="gr-attribute-helper.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
new file mode 100644
index 0000000..301c12e
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
@@ -0,0 +1,87 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function(window) {
+ 'use strict';
+
+ function GrAttributeHelper(element) {
+ this.element = element;
+ this._promises = {};
+ }
+
+ GrAttributeHelper.prototype._getChangedEventName = function(name) {
+ return name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() + '-changed';
+ };
+
+ /**
+ * Returns true if the property is defined on wrapped element.
+ * @param {string} name
+ * @return {boolean}
+ */
+ GrAttributeHelper.prototype._elementHasProperty = function(name) {
+ return this.element[name] !== undefined;
+ };
+
+ GrAttributeHelper.prototype._reportValue = function(callback, value) {
+ try {
+ callback(value);
+ } catch (e) {
+ console.info(e);
+ }
+ };
+
+ /**
+ * Binds callback to property updates.
+ *
+ * @param {string} name Property name.
+ * @param {function(?)} callback
+ * @return {function()} Unbind function.
+ */
+ GrAttributeHelper.prototype.bind = function(name, callback) {
+ const attributeChangedEventName = this._getChangedEventName(name);
+ const changedHandler = e => this._reportValue(callback, e.detail.value);
+ const unbind = () => this.element.removeEventListener(
+ attributeChangedEventName, changedHandler);
+ this.element.addEventListener(
+ attributeChangedEventName, changedHandler);
+ if (this._elementHasProperty(name)) {
+ this._reportValue(callback, this.element[name]);
+ }
+ return unbind;
+ };
+
+ /**
+ * Get value of the property from wrapped object. Waits for the property
+ * to be initialized if it isn't defined.
+ *
+ * @param {string} name Property name.
+ * @return {!Promise<?>}
+ */
+ GrAttributeHelper.prototype.get = function(name) {
+ if (this._elementHasProperty(name)) {
+ return Promise.resolve(this.element[name]);
+ }
+ if (!this._promises[name]) {
+ let resolve;
+ const promise = new Promise(r => resolve = r);
+ const unbind = this.bind(name, value => {
+ resolve(value);
+ unbind();
+ });
+ this._promises[name] = promise;
+ }
+ return this._promises[name];
+ };
+
+ window.GrAttributeHelper = GrAttributeHelper;
+})(window);
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
new file mode 100644
index 0000000..5dababe
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-attribute-helper</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-attribute-helper.html"/>
+
+<script>void(0);</script>
+
+<dom-element id="some-element">
+ <script>
+ Polymer({
+ is: 'some-element',
+ properties: {
+ fooBar: {
+ type: Object,
+ notify: true,
+ },
+ },
+ });
+ </script>
+</dom-element>
+
+<test-fixture id="basic">
+ <template>
+ <some-element></some-element>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-attribute-helper tests', () => {
+ let element;
+ let instance;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ instance = new GrAttributeHelper(element);
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('resolved on value change from undefined', () => {
+ const promise = instance.get('fooBar').then(value => {
+ assert.equal(value, 'foo! bar!');
+ });
+ element.fooBar = 'foo! bar!';
+ return promise;
+ });
+
+ test('resolves to current attribute value', () => {
+ element.fooBar = 'foo-foo-bar';
+ const promise = instance.get('fooBar').then(value => {
+ assert.equal(value, 'foo-foo-bar');
+ });
+ element.fooBar = 'no bar';
+ return promise;
+ });
+
+ test('bind', () => {
+ const stub = sandbox.stub();
+ element.fooBar = 'bar foo';
+ const unbind = instance.bind('fooBar', stub);
+ element.fooBar = 'partridge in a foo tree';
+ element.fooBar = 'five gold bars';
+ assert.equal(stub.callCount, 3);
+ assert.deepEqual(stub.args[0], ['bar foo']);
+ assert.deepEqual(stub.args[1], ['partridge in a foo tree']);
+ assert.deepEqual(stub.args[2], ['five gold bars']);
+ stub.reset();
+ unbind();
+ instance.fooBar = 'ladies dancing';
+ assert.isFalse(stub.called);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
index f73f731..f6e2b64 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
@@ -17,6 +17,7 @@
<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
+<link rel="import" href="../../plugins/gr-attribute-helper/gr-attribute-helper.html">
<link rel="import" href="../../plugins/gr-dom-hooks/gr-dom-hooks.html">
<link rel="import" href="../../plugins/gr-theme-api/gr-theme-api.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index 1236ca4..ca0f372 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -318,6 +318,10 @@
}
});
+ test('attributeHelper', () => {
+ assert.isOk(plugin.attributeHelper());
+ });
+
suite('test plugin with base url', () => {
setup(() => {
sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/r');
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index 97b67ae..a631c2f 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -158,6 +158,10 @@
return new GrThemeApi(this);
};
+ Plugin.prototype.attributeHelper = function(element) {
+ return new GrAttributeHelper(element);
+ };
+
Plugin.prototype.getDomHook = function(endpointName, opt_options) {
const hook = this._domHooks.getDomHook(endpointName);
const moduleName = hook.getModuleName();
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index e503812..ed898f9 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -92,6 +92,7 @@
'diff/gr-selection-action-box/gr-selection-action-box_test.html',
'diff/gr-syntax-layer/gr-syntax-layer_test.html',
'diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html',
+ 'plugins/gr-attribute-helper/gr-attribute-helper_test.html',
'plugins/gr-external-style/gr-external-style_test.html',
'plugins/gr-plugin-host/gr-plugin-host_test.html',
'settings/gr-account-info/gr-account-info_test.html',