Merge "Migrate reviewers plugin to polymer 3"
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/.eslintignore
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..da33ea2
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,165 @@
+{
+  "extends": ["eslint:recommended", "google"],
+  "parserOptions": {
+    "ecmaVersion": 8,
+    "sourceType": "module"
+  },
+  "env": {
+    "browser": true,
+    "es6": true
+  },
+  "globals": {
+    "__dirname": false,
+    "app": false,
+    "page": false,
+    "Polymer": false,
+    "process": false,
+    "require": false,
+    "Gerrit": false,
+    "Promise": false,
+    "assert": false,
+    "test": false,
+    "flushAsynchronousOperations": false
+  },
+  "rules": {
+    "arrow-parens": ["error", "as-needed"],
+    "block-spacing": ["error", "always"],
+    "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
+    "camelcase": "off",
+    "comma-dangle": ["error", {
+      "arrays": "always-multiline",
+      "objects": "always-multiline",
+      "imports": "always-multiline",
+      "exports": "always-multiline",
+      "functions": "never"
+    }],
+    "eol-last": "off",
+    "indent": ["error", 2, {
+      "MemberExpression": 2,
+      "FunctionDeclaration": {"body": 1, "parameters": 2},
+      "FunctionExpression": {"body": 1, "parameters": 2},
+      "CallExpression": {"arguments": 2 },
+      "ArrayExpression": 1,
+      "ObjectExpression": 1,
+      "SwitchCase": 1
+    }],
+    "keyword-spacing": ["error", { "after": true, "before": true }],
+    "lines-between-class-members": ["error", "always"],
+    "max-len": [
+      "error",
+      80,
+      2,
+      {
+        "ignoreComments": true,
+        "ignorePattern": "^import .*;$"
+      }
+    ],
+    "new-cap": ["error", { "capIsNewExceptions": ["Polymer", "LegacyElementMixin", "GestureEventListeners", "LegacyDataMixin"] }],
+    "no-console": "off",
+    "no-multiple-empty-lines": [ "error", { "max": 1 } ],
+    "no-prototype-builtins": "off",
+    "no-redeclare": "off",
+    "no-restricted-syntax": [
+      "error",
+      {
+        "selector": "ExpressionStatement > CallExpression > MemberExpression[object.name='test'][property.name='only']",
+        "message": "Remove test.only."
+      },
+      {
+        "selector": "ExpressionStatement > CallExpression > MemberExpression[object.name='suite'][property.name='only']",
+        "message": "Remove suite.only."
+      }
+    ],
+    "no-undef": "off",
+    "no-useless-escape": "off",
+    "no-var": "error",
+    "object-shorthand": ["error", "always"],
+    "padding-line-between-statements": [
+      "error",
+      {
+        "blankLine": "always",
+        "prev": "class",
+        "next": "*"
+      },
+      {
+        "blankLine": "always",
+        "prev": "*",
+        "next": "class"
+      }
+    ],
+    "prefer-arrow-callback": "error",
+    "prefer-const": "error",
+    "prefer-spread": "error",
+    "quote-props": ["error", "consistent-as-needed"],
+    "semi": [2, "always"],
+    "template-curly-spacing": "error",
+    "valid-jsdoc": "off",
+    "require-jsdoc": 0,
+    "valid-jsdoc": 0,
+    "jsdoc/check-alignment": 2,
+    "jsdoc/check-examples": 0,
+    "jsdoc/check-indentation": 0,
+    "jsdoc/check-param-names": 0,
+    "jsdoc/check-syntax": 0,
+    "jsdoc/check-tag-names": 0,
+    "jsdoc/check-types": 0,
+    "jsdoc/implements-on-classes": 2,
+    "jsdoc/match-description": 0,
+    "jsdoc/newline-after-description": 2,
+    "jsdoc/no-types": 0,
+    "jsdoc/no-undefined-types": 0,
+    "jsdoc/require-description": 0,
+    "jsdoc/require-description-complete-sentence": 0,
+    "jsdoc/require-example": 0,
+    "jsdoc/require-hyphen-before-param-description": 0,
+    "jsdoc/require-jsdoc": 0,
+    "jsdoc/require-param": 0,
+    "jsdoc/require-param-description": 0,
+    "jsdoc/require-param-name": 2,
+    "jsdoc/require-param-type": 2,
+    "jsdoc/require-returns": 0,
+    "jsdoc/require-returns-check": 0,
+    "jsdoc/require-returns-description": 0,
+    "jsdoc/require-returns-type": 2,
+    "jsdoc/valid-types": 2,
+    "jsdoc/require-file-overview": ["error", {
+      "tags": {
+        "license": {
+          "mustExist": true,
+          "preventDuplicates": true
+        }
+      }
+    }],
+    "import/named": 2,
+    "import/no-unresolved": 2,
+    "import/no-self-import": 2,
+    // The no-cycle rule is slow, because it doesn't cache dependencies.
+    // Disable it.
+    "import/no-cycle": 0,
+    "import/no-useless-path-segments": 2,
+    "import/no-unused-modules": 2,
+    "import/no-default-export": 2
+  },
+  "plugins": [
+    "html",
+    "jsdoc",
+    "import"
+  ],
+  "settings": {
+    "html/report-bad-indent": "error"
+  },
+  "overrides": [
+    {
+      "files": ["*_html.js", "*-styles.js", "externs.js"],
+      "rules": {
+        "max-len": "off"
+      }
+    },
+    {
+      "files": ["*.html"],
+      "rules": {
+        "jsdoc/require-file-overview": "off"
+      }
+    }
+  ]
+}
diff --git a/.gitignore b/.gitignore
index adb2912..1c805b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
 /.settings
 /local.properties
 *.pyc
+/package-lock.json
diff --git a/BUILD b/BUILD
index 8fd0a8e..6379a0d 100644
--- a/BUILD
+++ b/BUILD
@@ -1,5 +1,7 @@
 load("@rules_java//java:defs.bzl", "java_library")
+load("@npm_bazel_rollup//:index.bzl", "rollup_bundle")
 load("//tools/bzl:junit.bzl", "junit_tests")
+load("//tools/js:eslint.bzl", "eslint")
 load(
     "//tools/bzl:plugin.bzl",
     "PLUGIN_DEPS",
@@ -34,11 +36,20 @@
 
 polygerrit_plugin(
     name = "rv-reviewers",
-    srcs = glob([
-        "rv-reviewers/*.html",
-        "rv-reviewers/*.js",
-    ]),
-    app = "plugin.html",
+    app = "reviewers-bundle.js",
+    plugin_name = "rv-reviewers",
+)
+
+rollup_bundle(
+    name = "reviewers-bundle",
+    srcs = glob(["rv-reviewers/*.js"]),
+    entry_point = "rv-reviewers/plugin.js",
+    format = "iife",
+    rollup_bin = "//tools/node_tools:rollup-bin",
+    sourcemap = "hidden",
+    deps = [
+        "@tools_npm//rollup-plugin-node-resolve",
+    ],
 )
 
 junit_tests(
@@ -50,3 +61,24 @@
         ":reviewers__plugin",
     ],
 )
+
+# Define the eslinter for the plugin
+# The eslint macro creates 2 rules: lint_test and lint_bin
+eslint(
+    name = "lint",
+    srcs = glob([
+        "rv-reviewers/*.js",
+    ]),
+    config = ".eslintrc.json",
+    data = [],
+    extensions = [
+        ".js",
+    ],
+    ignore = ".eslintignore",
+    plugins = [
+        "@npm//eslint-config-google",
+        "@npm//eslint-plugin-html",
+        "@npm//eslint-plugin-import",
+        "@npm//eslint-plugin-jsdoc",
+    ],
+)
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..2064647
--- /dev/null
+++ b/package.json
@@ -0,0 +1,13 @@
+{
+  "name": "reviewers",
+  "description": "Reviewers plugin",
+  "browser": true,
+  "scripts": {
+    "safe_bazelisk": "if which bazelisk >/dev/null; then bazel_bin=bazelisk; else bazel_bin=bazel; fi && $bazel_bin",
+    "eslint": "npm run safe_bazelisk test :lint_test",
+    "eslintfix": "npm run safe_bazelisk run :lint_bin -- -- --fix $(pwd)"
+  },
+  "devDependencies": {},
+  "license": "Apache-2.0",
+  "private": true
+}
diff --git a/plugin.html b/plugin.html
deleted file mode 100644
index 29c10cb..0000000
--- a/plugin.html
+++ /dev/null
@@ -1,27 +0,0 @@
-<!--
-@license
-Copyright (C) 2019 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="./rv-reviewers/rv-reviewers.html">
-
-<dom-module id="reviewers">
-  <script>
-    if (window.Polymer) {
-      Gerrit.install(plugin => {
-        plugin.registerCustomComponent('repo-command', 'rv-reviewers');
-      });
-    }
-  </script>
-</dom-module>
diff --git a/rv-reviewers/plugin.js b/rv-reviewers/plugin.js
new file mode 100644
index 0000000..ba7e1d2
--- /dev/null
+++ b/rv-reviewers/plugin.js
@@ -0,0 +1,22 @@
+/**
+ * @license
+ * Copyright (C) 2020 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.
+ */
+import './rv-reviewers.js';
+
+Gerrit.install(plugin => {
+  plugin.registerCustomComponent(
+      'repo-command', 'rv-reviewers');
+});
diff --git a/rv-reviewers/rv-edit-screen.js b/rv-reviewers/rv-edit-screen.js
index be302a8..36ff185 100644
--- a/rv-reviewers/rv-edit-screen.js
+++ b/rv-reviewers/rv-edit-screen.js
@@ -1,72 +1,84 @@
-// Copyright (C) 2019 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() {
-  class RvEditScreen extends Polymer.Element {
-    static get is() { return 'rv-edit-screen'; }
+/**
+ * @license
+ * Copyright (C) 2019 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.
+ */
+import {htmlTemplate} from './rv-edit-screen_html.js';
 
-    static get properties() {
-      return {
-        pluginRestApi: {
-          type: Object,
-          observer: '_loadFilterSections',
-        },
-        repoName: String,
-        loading: Boolean,
-        canModifyConfig: Boolean,
-        _editingFilter: {
-          type: Boolean,
-          value: false,
-        },
-        _filterSections: Array,
-      };
-    }
+class RvEditScreen extends Polymer.Element {
+  /** @returns {string} name of the component */
+  static get is() { return 'rv-edit-screen'; }
 
-    _loadFilterSections() {
-      this.pluginRestApi.get(this._getReviewersUrl(this.repoName))
-          .then(filterSections => {
-            this._filterSections = filterSections;
-          });
-    }
+  /** @returns {?} template for this component */
+  static get template() { return htmlTemplate; }
 
-    _computeAddFilterBtnHidden(canModifyConfig, editingFilter) {
-      return !canModifyConfig || editingFilter;
-    }
-
-    _computeLoadingClass(loading) {
-      return loading ? 'loading' : '';
-    }
-
-    _getReviewersUrl(repoName) {
-      return `/projects/${encodeURIComponent(repoName)}/reviewers`;
-    }
-
-    _handleCreateSection() {
-      const section = {filter: '', reviewers: [], editing: true};
-      this._editingFilter = true;
-      this.push('_filterSections', section);
-    }
-
-    _handleCloseTap(e) {
-      e.preventDefault();
-      this.fire('close', null, {bubbles: false});
-    }
-
-    _handleReviewerChanged(e) {
-      this._filterSections = e.detail.result;
-      this._editingFilter = false;
-    }
+  /**
+   * Defines properties of the component
+   *
+   * @returns {?}
+   */
+  static get properties() {
+    return {
+      pluginRestApi: {
+        type: Object,
+        observer: '_loadFilterSections',
+      },
+      repoName: String,
+      loading: Boolean,
+      canModifyConfig: Boolean,
+      _editingFilter: {
+        type: Boolean,
+        value: false,
+      },
+      _filterSections: Array,
+    };
   }
 
-  customElements.define(RvEditScreen.is, RvEditScreen);
-})();
+  _loadFilterSections() {
+    this.pluginRestApi.get(this._getReviewersUrl(this.repoName))
+        .then(filterSections => {
+          this._filterSections = filterSections;
+        });
+  }
+
+  _computeAddFilterBtnHidden(canModifyConfig, editingFilter) {
+    return !canModifyConfig || editingFilter;
+  }
+
+  _computeLoadingClass(loading) {
+    return loading ? 'loading' : '';
+  }
+
+  _getReviewersUrl(repoName) {
+    return `/projects/${encodeURIComponent(repoName)}/reviewers`;
+  }
+
+  _handleCreateSection() {
+    const section = {filter: '', reviewers: [], editing: true};
+    this._editingFilter = true;
+    this.push('_filterSections', section);
+  }
+
+  _handleCloseTap(e) {
+    e.preventDefault();
+    this.fire('close', null, {bubbles: false});
+  }
+
+  _handleReviewerChanged(e) {
+    this._filterSections = e.detail.result;
+    this._editingFilter = false;
+  }
+}
+
+customElements.define(RvEditScreen.is, RvEditScreen);
diff --git a/rv-reviewers/rv-edit-screen.html b/rv-reviewers/rv-edit-screen_html.js
similarity index 70%
rename from rv-reviewers/rv-edit-screen.html
rename to rv-reviewers/rv-edit-screen_html.js
index 3291e3c..c6e46a5 100644
--- a/rv-reviewers/rv-edit-screen.html
+++ b/rv-reviewers/rv-edit-screen_html.js
@@ -1,23 +1,22 @@
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2019 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.
+ */
+import './rv-filter-section.js';
 
-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="./rv-filter-section.html">
-
-<dom-module id="rv-edit-screen">
-  <template>
+export const htmlTemplate = Polymer.html`
     <style include="shared-styles"></style>
     <style include="gr-menu-page-styles"></style>
     <style include="gr-subpage-styles">
@@ -72,6 +71,4 @@
             hidden="[[_computeAddFilterBtnHidden(canModifyConfig, _editingFilter)]]">Add New Filter</gr-button>
       </div>
     </div>
-  </template>
-  <script src="./rv-edit-screen.js"></script>
-</dom-module>
+`;
diff --git a/rv-reviewers/rv-filter-section.js b/rv-reviewers/rv-filter-section.js
index 962a54f..568ba6d 100644
--- a/rv-reviewers/rv-filter-section.js
+++ b/rv-reviewers/rv-filter-section.js
@@ -1,104 +1,116 @@
-// Copyright (C) 2019 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() {
-  class RvFilterSection extends Polymer.Element {
-    static get is() { return 'rv-filter-section'; }
+/**
+ * @license
+ * Copyright (C) 2019 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.
+ */
+import {htmlTemplate} from './rv-filter-section_html.js';
 
-    static get properties() {
-      return {
-        pluginRestApi: Object,
-        repoName: String,
-        reviewers: Array,
-        filter: String,
-        canModifyConfig: Boolean,
-        _originalFilter: String,
-        _editingReviewer: {
-          type: Boolean,
-          value: false,
-        },
-        reviewersUrl: String,
-      };
+class RvFilterSection extends Polymer.Element {
+  /** @returns {string} name of the component */
+  static get is() { return 'rv-filter-section'; }
+
+  /** @returns {?} template for this component */
+  static get template() { return htmlTemplate; }
+
+  /**
+   * Defines properties of the component
+   *
+   * @returns {?}
+   */
+  static get properties() {
+    return {
+      pluginRestApi: Object,
+      repoName: String,
+      reviewers: Array,
+      filter: String,
+      canModifyConfig: Boolean,
+      _originalFilter: String,
+      _editingReviewer: {
+        type: Boolean,
+        value: false,
+      },
+      reviewersUrl: String,
+    };
+  }
+
+  connectedCallback() {
+    super.connectedCallback();
+    this._updateSection();
+  }
+
+  _updateSection() {
+    this._originalFilter = this.filter;
+  }
+
+  _computeEditing(filter, _originalFilter) {
+    if (_originalFilter === '') {
+      return true;
     }
+    return filter === '';
+  }
 
-    connectedCallback() {
-      super.connectedCallback();
-      this._updateSection();
-    }
+  _computeCancelHidden(filter, _originalFilter) {
+    return !this._computeEditing(filter, _originalFilter);
+  }
 
-    _updateSection() {
-      this._originalFilter = this.filter;
-    }
+  _computeAddBtnHidden(canModifyConfig, editingReviewer) {
+    return !(canModifyConfig && !editingReviewer);
+  }
 
-    _computeEditing(filter, _originalFilter) {
-      if (_originalFilter === '') {
-        return true;
-      }
-      return filter === '';
-    }
+  _computeFilterInputDisabled(canModifyConfig, originalFilter) {
+    return !canModifyConfig || originalFilter !== '';
+  }
 
-    _computeCancelHidden(filter, _originalFilter) {
-      return !this._computeEditing(filter, _originalFilter);
-    }
+  _handleCancel() {
+    this.remove();
+  }
 
-    _computeAddBtnHidden(canModifyConfig, editingReviewer) {
-      return !(canModifyConfig && !editingReviewer);
-    }
-
-    _computeFilterInputDisabled(canModifyConfig, originalFilter) {
-      return !canModifyConfig || originalFilter !== '';
-    }
-
-    _handleCancel() {
-      this.remove();
-    }
-
-    _handleReviewerDeleted(e) {
-      if (e.detail.editing) {
-        this.reviewers.pop();
-        this._editingReviewer = false;
-      } else {
-        const index = e.model.index;
-        const deleted = this.reviewers[index];
-        this._putReviewer(deleted, 'DELETE');
-      }
-    }
-
-    _handleReviewerAdded(e) {
+  _handleReviewerDeleted(e) {
+    if (e.detail.editing) {
+      this.reviewers.pop();
       this._editingReviewer = false;
-      this._putReviewer(e.detail.reviewer, 'ADD').catch(err => {
-        this.fire('show-alert', {message: err});
-        throw err;
-      });
-    }
-
-    _putReviewer(reviewer, action) {
-      return this.pluginRestApi.put(this.reviewersUrl, {
-        action,
-        reviewer,
-        filter: this.filter,
-      }).then(result => {
-        const detail = {result};
-        this.dispatchEvent(
-            new CustomEvent('reviewer-changed', {detail, bubbles: true}));
-      });
-    }
-
-    _handleAddReviewer() {
-      this.push('reviewers', '');
-      this._editingReviewer = true;
+    } else {
+      const index = e.model.index;
+      const deleted = this.reviewers[index];
+      this._putReviewer(deleted, 'DELETE');
     }
   }
 
-  customElements.define(RvFilterSection.is, RvFilterSection);
-})();
+  _handleReviewerAdded(e) {
+    this._editingReviewer = false;
+    this._putReviewer(e.detail.reviewer, 'ADD').catch(err => {
+      this.fire('show-alert', {message: err});
+      throw err;
+    });
+  }
+
+  _putReviewer(reviewer, action) {
+    return this.pluginRestApi.put(this.reviewersUrl, {
+      action,
+      reviewer,
+      filter: this.filter,
+    }).then(result => {
+      const detail = {result};
+      this.dispatchEvent(
+          new CustomEvent('reviewer-changed', {detail, bubbles: true}));
+    });
+  }
+
+  _handleAddReviewer() {
+    this.push('reviewers', '');
+    this._editingReviewer = true;
+  }
+}
+
+customElements.define(RvFilterSection.is, RvFilterSection);
diff --git a/rv-reviewers/rv-filter-section.html b/rv-reviewers/rv-filter-section_html.js
similarity index 77%
rename from rv-reviewers/rv-filter-section.html
rename to rv-reviewers/rv-filter-section_html.js
index 7513675..e850b39 100644
--- a/rv-reviewers/rv-filter-section.html
+++ b/rv-reviewers/rv-filter-section_html.js
@@ -1,23 +1,22 @@
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2019 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.
+ */
+import './rv-reviewer.js';
 
-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="./rv-reviewer.html">
-
-<dom-module id="rv-filter-section">
-  <template>
+export const htmlTemplate = Polymer.html`
     <style include="shared-styles">
       :host {
         display: block;
@@ -99,6 +98,4 @@
         </div><!-- reviewers -->
       </div>
     </fieldset>
-  </template>
-  <script src="./rv-filter-section.js"></script>
-</dom-module>
+`;
diff --git a/rv-reviewers/rv-reviewer.js b/rv-reviewers/rv-reviewer.js
index bb0b792..dd516c4 100644
--- a/rv-reviewers/rv-reviewer.js
+++ b/rv-reviewers/rv-reviewer.js
@@ -1,141 +1,153 @@
-// Copyright (C) 2019 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() {
-  class RvReviewer extends Polymer.Element {
-    static get is() { return 'rv-reviewer'; }
+/**
+ * @license
+ * Copyright (C) 2019 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.
+ */
+import {htmlTemplate} from './rv-reviewer_html.js';
 
-    static get properties() {
-      return {
-        canModifyConfig: Boolean,
-        pluginRestAPi: Object,
-        repoName: String,
-        reviewer: String,
-        _reviewerSearchId: String,
-        _queryReviewers: {
-          type: Function,
-          value() {
-            return this._getReviewerSuggestions.bind(this);
-          },
+class RvReviewer extends Polymer.Element {
+  /** @returns {string} name of the component */
+  static get is() { return 'rv-reviewer'; }
+
+  /** @returns {?} template for this component */
+  static get template() { return htmlTemplate; }
+
+  /**
+   * Defines properties of the component
+   *
+   * @returns {?}
+   */
+  static get properties() {
+    return {
+      canModifyConfig: Boolean,
+      pluginRestAPi: Object,
+      repoName: String,
+      reviewer: String,
+      _reviewerSearchId: String,
+      _queryReviewers: {
+        type: Function,
+        value() {
+          return this._getReviewerSuggestions.bind(this);
         },
-        _originalReviewer: String,
-        _deleted: Boolean,
-        _editing: {
-          type: Boolean,
-          computed: '_computeEditing(reviewer, _originalReviewer)',
-        },
-      };
-    }
-
-    connectedCallback() {
-      super.connectedCallback();
-      this._originalReviewer = this.reviewer;
-    }
-
-    _computeEditing(reviewer, _originalReviewer) {
-      if (_originalReviewer === '') {
-        return true;
-      }
-      return reviewer === '';
-    }
-
-    _computeDeleteCancel(reviewer, _originalReviewer) {
-      return this._computeEditing(reviewer, _originalReviewer) ?
-        'Cancel' : 'Delete';
-    }
-
-    _computeHideAddButton(reviewer, _originalReviewer) {
-      return !(this._computeEditing(reviewer, _originalReviewer)
-      && this._reviewerSearchId);
-    }
-
-    _computeHideDeleteButton(canModifyConfig) {
-      return !canModifyConfig;
-    }
-
-    _getReviewerSuggestions(input) {
-      if (input.length === 0) { return Promise.resolve([]); }
-      const promises = [];
-      promises.push(this._getSuggestedGroups(input));
-      promises.push(this._getSuggestedAccounts(input));
-      return Promise.all(promises).then(result => {
-        return result.flat();
-      });
-    }
-
-    _getSuggestedGroups(input) {
-      const suggestUrl = `/groups/?suggest=${input}&p=${this.repoName}`;
-      return this.pluginRestApi.get(suggestUrl).then(groups => {
-        if (!groups) { return []; }
-        const groupSuggestions = [];
-        for (const key in groups) {
-          if (!groups.hasOwnProperty(key)) { continue; }
-          groupSuggestions.push({
-            name: key,
-            value: key,
-          });
-        }
-        return groupSuggestions;
-      });
-    }
-
-    _getSuggestedAccounts(input) {
-      const suggestUrl = `/accounts/?suggest&q=${input}`;
-      return this.pluginRestApi.get(suggestUrl).then(accounts => {
-        const accountSuggestions = [];
-        let nameAndEmail;
-        let value;
-        if (!accounts) { return []; }
-        for (const key in accounts) {
-          if (!accounts.hasOwnProperty(key)) { continue; }
-          if (accounts[key].email) {
-            nameAndEmail = accounts[key].name +
-              ' <' + accounts[key].email + '>';
-          } else {
-            nameAndEmail = accounts[key].name;
-          }
-          if (accounts[key].username) {
-            value = accounts[key].username;
-          } else if (accounts[key].email) {
-            value = accounts[key].email;
-          } else {
-            value = accounts[key]._account_id;
-          }
-          accountSuggestions.push({
-            name: nameAndEmail,
-            value,
-          });
-        }
-        return accountSuggestions;
-      });
-    }
-
-    _handleDeleteCancel() {
-      const detail = {editing: this._editing};
-      if (this._editing) {
-        this.remove();
-      }
-      this.dispatchEvent(
-          new CustomEvent('reviewer-deleted', {detail, bubbles: true}));
-    }
-
-    _handleAddReviewer() {
-      const detail = {reviewer: this._reviewerSearchId};
-      this._originalReviewer = this.reviewer;
-      this.dispatchEvent(
-          new CustomEvent('reviewer-added', {detail, bubbles: true}));
-    }
+      },
+      _originalReviewer: String,
+      _deleted: Boolean,
+      _editing: {
+        type: Boolean,
+        computed: '_computeEditing(reviewer, _originalReviewer)',
+      },
+    };
   }
 
-  customElements.define(RvReviewer.is, RvReviewer);
-})();
+  connectedCallback() {
+    super.connectedCallback();
+    this._originalReviewer = this.reviewer;
+  }
+
+  _computeEditing(reviewer, _originalReviewer) {
+    if (_originalReviewer === '') {
+      return true;
+    }
+    return reviewer === '';
+  }
+
+  _computeDeleteCancel(reviewer, _originalReviewer) {
+    return this._computeEditing(reviewer, _originalReviewer) ?
+      'Cancel' : 'Delete';
+  }
+
+  _computeHideAddButton(reviewer, _originalReviewer) {
+    return !(this._computeEditing(reviewer, _originalReviewer)
+    && this._reviewerSearchId);
+  }
+
+  _computeHideDeleteButton(canModifyConfig) {
+    return !canModifyConfig;
+  }
+
+  _getReviewerSuggestions(input) {
+    if (input.length === 0) { return Promise.resolve([]); }
+    const promises = [];
+    promises.push(this._getSuggestedGroups(input));
+    promises.push(this._getSuggestedAccounts(input));
+    return Promise.all(promises).then(result => {
+      return result.flat();
+    });
+  }
+
+  _getSuggestedGroups(input) {
+    const suggestUrl = `/groups/?suggest=${input}&p=${this.repoName}`;
+    return this.pluginRestApi.get(suggestUrl).then(groups => {
+      if (!groups) { return []; }
+      const groupSuggestions = [];
+      for (const key in groups) {
+        if (!groups.hasOwnProperty(key)) { continue; }
+        groupSuggestions.push({
+          name: key,
+          value: key,
+        });
+      }
+      return groupSuggestions;
+    });
+  }
+
+  _getSuggestedAccounts(input) {
+    const suggestUrl = `/accounts/?suggest&q=${input}`;
+    return this.pluginRestApi.get(suggestUrl).then(accounts => {
+      const accountSuggestions = [];
+      let nameAndEmail;
+      let value;
+      if (!accounts) { return []; }
+      for (const key in accounts) {
+        if (!accounts.hasOwnProperty(key)) { continue; }
+        if (accounts[key].email) {
+          nameAndEmail = accounts[key].name +
+            ' <' + accounts[key].email + '>';
+        } else {
+          nameAndEmail = accounts[key].name;
+        }
+        if (accounts[key].username) {
+          value = accounts[key].username;
+        } else if (accounts[key].email) {
+          value = accounts[key].email;
+        } else {
+          value = accounts[key]._account_id;
+        }
+        accountSuggestions.push({
+          name: nameAndEmail,
+          value,
+        });
+      }
+      return accountSuggestions;
+    });
+  }
+
+  _handleDeleteCancel() {
+    const detail = {editing: this._editing};
+    if (this._editing) {
+      this.remove();
+    }
+    this.dispatchEvent(
+        new CustomEvent('reviewer-deleted', {detail, bubbles: true}));
+  }
+
+  _handleAddReviewer() {
+    const detail = {reviewer: this._reviewerSearchId};
+    this._originalReviewer = this.reviewer;
+    this.dispatchEvent(
+        new CustomEvent('reviewer-added', {detail, bubbles: true}));
+  }
+}
+
+customElements.define(RvReviewer.is, RvReviewer);
\ No newline at end of file
diff --git a/rv-reviewers/rv-reviewer.html b/rv-reviewers/rv-reviewer_html.js
similarity index 69%
rename from rv-reviewers/rv-reviewer.html
rename to rv-reviewers/rv-reviewer_html.js
index 10ab166..a4eee47 100644
--- a/rv-reviewers/rv-reviewer.html
+++ b/rv-reviewers/rv-reviewer_html.js
@@ -1,21 +1,20 @@
-<!--
-@license
-Copyright (C) 2019 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.
--->
-<dom-module id="rv-reviewer">
-  <template>
+/**
+ * @license
+ * Copyright (C) 2019 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.
+ */
+export const htmlTemplate = Polymer.html`
     <style include="shared-styles">
       #editReviewerInput {
         display: block;
@@ -45,11 +44,11 @@
         <span class="value">
             <!--
               TODO:
-              Investigate wether we could reuse gr-account-list.
+              Investigate whether we could reuse gr-account-list.
               If the REST API returns AccountInfo instead of an account
               identifier String we should be able to use gr-account-list(size=1)
               for all reviewers, including those who are non-editable
-              (#reviewerField below) and allign the plugin with how accounts
+              (#reviewerField below) and align the plugin with how accounts
               are displayed in core Gerrit's UI.
             -->
             <gr-autocomplete
@@ -74,6 +73,4 @@
           on-tap="_handleAddReviewer"
           hidden$="[[_computeHideAddButton(reviewer, _originalReviewer)]]">Add</gr-button>
     </div> <!-- reviewerRow -->
-  </template>
-  <script src="./rv-reviewer.js"></script>
-</dom-module>
+`;
\ No newline at end of file
diff --git a/rv-reviewers/rv-reviewers.html b/rv-reviewers/rv-reviewers.html
deleted file mode 100644
index f44aa6f..0000000
--- a/rv-reviewers/rv-reviewers.html
+++ /dev/null
@@ -1,41 +0,0 @@
-<!--
-@license
-Copyright (C) 2019 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="./rv-edit-screen.html">
-
-<dom-module id="rv-reviewers">
-  <template>
-    <style include="shared-styles">
-      #rvScreenOverlay {
-        width: 50em;
-        overflow: auto;
-      }
-    </style>
-    <gr-repo-command
-        title="Reviewers Config"
-        on-command-tap="_handleCommandTap">
-    </gr-repo-command>
-    <gr-overlay id="rvScreenOverlay" with-backdrop>
-      <rv-edit-screen
-          plugin-rest-api="[[pluginRestApi]]"
-          repo-name="[[repoName]]"
-          loading="[[_loading]]"
-          can-modify-config="[[_canModifyConfig]]"
-          on-close="_handleRvEditScreenClose"></rv-edit-screen>
-    </gr-overlay>
-  </template>
-  <script src="./rv-reviewers.js"></script>
-</dom-module>
diff --git a/rv-reviewers/rv-reviewers.js b/rv-reviewers/rv-reviewers.js
index 8c10ea4..9782b8c 100644
--- a/rv-reviewers/rv-reviewers.js
+++ b/rv-reviewers/rv-reviewers.js
@@ -1,87 +1,100 @@
-// Copyright (C) 2019 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() {
-  class RvReviewers extends Polymer.Element {
-    static get is() { return 'rv-reviewers'; }
+/**
+ * @license
+ * Copyright (C) 2019 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.
+ */
+import {htmlTemplate} from './rv-reviewers_html.js';
 
-    static get properties() {
-      return {
-        pluginRestApi: Object,
-        repoName: String,
-        _canModifyConfig: {
-          type: Boolean,
-          computed: '_computeCanModifyConfig(_isOwner, _hasModifyCapability)',
-        },
-        _loading: {
-          type: Boolean,
-          value: true,
-        },
-        _isOwner: {
-          type: Boolean,
-          value: false,
-        },
-        _hasModifyCapability: {
-          type: Boolean,
-          value: false,
-        },
-      };
-    }
+class RvReviewers extends Polymer.Element {
+  /** @returns {string} name of the component */
+  static get is() { return 'rv-reviewers'; }
 
-    connectedCallback() {
-      super.connectedCallback();
-      this.pluginRestApi = this.plugin.restApi();
-      this._setCanModifyConfig();
-    }
+  /** @returns {?} template for this component */
+  static get template() { return htmlTemplate; }
 
-    _handleCommandTap() {
-      this.$.rvScreenOverlay.open();
-    }
-
-    _handleRvEditScreenClose() {
-      this.$.rvScreenOverlay.close();
-    }
-
-    _setCanModifyConfig() {
-      const promises = [];
-      promises.push(this._getRepoAccess(this.repoName).then( access => {
-        if (access && access[this.repoName] && access[this.repoName].is_owner) {
-          this._isOwner = true;
-        }
-      }));
-      promises.push(this._getCapabilities().then(capabilities => {
-        if (capabilities['reviewers-modifyReviewersConfig']) {
-          this._hasModifyCapability = true;
-        }
-      }));
-      Promise.all(promises).then(() => {
-        this._loading = false;
-      });
-    }
-
-    _computeCanModifyConfig(isOwner, hasModifyCapability) {
-      return isOwner || hasModifyCapability;
-    }
-
-    _getRepoAccess(repoName) {
-      return this.pluginRestApi.get(
-          '/access/?project=' + encodeURIComponent(repoName));
-    }
-
-    _getCapabilities() {
-      return this.pluginRestApi.get('/accounts/self/capabilities');
-    }
+  /**
+   * Defines properties of the component
+   *
+   * @returns {?}
+   */
+  static get properties() {
+    return {
+      pluginRestApi: Object,
+      repoName: String,
+      _canModifyConfig: {
+        type: Boolean,
+        computed: '_computeCanModifyConfig(_isOwner, _hasModifyCapability)',
+      },
+      _loading: {
+        type: Boolean,
+        value: true,
+      },
+      _isOwner: {
+        type: Boolean,
+        value: false,
+      },
+      _hasModifyCapability: {
+        type: Boolean,
+        value: false,
+      },
+    };
   }
 
-  customElements.define(RvReviewers.is, RvReviewers);
-})();
+  connectedCallback() {
+    super.connectedCallback();
+    this.pluginRestApi = this.plugin.restApi();
+    this._setCanModifyConfig();
+  }
+
+  _handleCommandTap() {
+    this.$.rvScreenOverlay.open();
+  }
+
+  _handleRvEditScreenClose() {
+    this.$.rvScreenOverlay.close();
+  }
+
+  _setCanModifyConfig() {
+    const promises = [];
+    promises.push(this._getRepoAccess(this.repoName).then( access => {
+      if (access && access[this.repoName] && access[this.repoName].is_owner) {
+        this._isOwner = true;
+      }
+    }));
+    promises.push(this._getCapabilities().then(capabilities => {
+      if (capabilities['reviewers-modifyReviewersConfig']) {
+        this._hasModifyCapability = true;
+      }
+    }));
+    Promise.all(promises).then(() => {
+      this._loading = false;
+    });
+  }
+
+  _computeCanModifyConfig(isOwner, hasModifyCapability) {
+    return isOwner || hasModifyCapability;
+  }
+
+  _getRepoAccess(repoName) {
+    return this.pluginRestApi.get(
+        '/access/?project=' + encodeURIComponent(repoName));
+  }
+
+  _getCapabilities() {
+    return this.pluginRestApi.get('/accounts/self/capabilities');
+  }
+}
+
+customElements.define(RvReviewers.is, RvReviewers);
+
diff --git a/rv-reviewers/rv-reviewers_html.js b/rv-reviewers/rv-reviewers_html.js
new file mode 100644
index 0000000..358e5e5
--- /dev/null
+++ b/rv-reviewers/rv-reviewers_html.js
@@ -0,0 +1,38 @@
+/**
+ * @license
+ * Copyright (C) 2020 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.
+ */
+import './rv-edit-screen.js';
+
+export const htmlTemplate = Polymer.html`
+    <style include="shared-styles">
+      #rvScreenOverlay {
+        width: 50em;
+        overflow: auto;
+      }
+    </style>
+    <gr-repo-command
+        title="Reviewers Config"
+        on-command-tap="_handleCommandTap">
+    </gr-repo-command>
+    <gr-overlay id="rvScreenOverlay" with-backdrop>
+      <rv-edit-screen
+          plugin-rest-api="[[pluginRestApi]]"
+          repo-name="[[repoName]]"
+          loading="[[_loading]]"
+          can-modify-config="[[_canModifyConfig]]"
+          on-close="_handleRvEditScreenClose"></rv-edit-screen>
+    </gr-overlay>
+`;