Rename polymer template databinding expressions and event functions with closure-compiler
JavaScript
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
lib
test
.eslintrc.json
.gitignore
.travis.yml
README.md
index.js
package.json
polymer-rename-externs.js
yarn.lock

README.md

polymer-rename

Build Status npm version

Updated for Polymer 2

Closure-compiler with ADVANCED optimizations, offers the powerful ability to rename properties. However, it can only safely be used on code bases which follow its required conventions. With the introduction of polymer templates, data binding expressions become external uses of code which the compiler must be made aware of or optimizations such as dead-code elimination and property renaming will break the template references. While it's possible to quote or export all of the properties referenced from polymer templates, that action significantly decreases both the final compression as well as the type checking ability of the compiler.

This project offers an alternative approach. Prior to compilation with Closure-compiler, the Polymer template html is parsed and any data binding expressions are extracted. These extracted expressions are then output in an alternative form as javascript. This extracted javascript is never intended to be actually executed, but it is provided to Closure-compiler during compilation. The compiler can then safely rename references and thus the need to export or quote properties used in data-binding is eliminated.

In addition, considerable type checking is enabled for data-binding expressions.

Original

<dom-module id="add-or-subtract">
  <template>
    <div><button on-tap="_addOne">Increase</button></div>
    <div><button on-tap="_subtractOne">Decrease</button></div>
  </template>
  <script>
    /**
     * @polymer
     * @customElement
     * @extends {Polymer.Element}
     */
    class AddOrSubtractElement extends Polymer.Element {
      static get is() { return 'add-or-subtract'; }
      static get properties() {
        return {
          value: {
            type: Number,
            notify: true
          }
        };
      }
      _addOne() { this.value++; }
      _subtractOne() { this.value--; }
    }
    customElements.define(AddOrSubtractElement.is, AddOrSubtractElement);
  </script>
</dom-module>
<dom-module id="foo-bar">
  <template>
    <template is="dom-repeat" items="[[numList]]" as="num">
      <add-or-subtract on-value-changed="{{_valueChanged}}" value="[[num]]"></add-or-subtract>
    </template>
  </template>
  <script>
    /**
     * @polymer
     * @customElement
     * @extends {Polymer.Element}
     */
    class FooBarElement extends Polymer.Element {
      static get is() { return 'foo-bar'; }
      static get properties() {
        return {
          numList: {
            type: Array,
            value: () => [1, 2, 3, 4]
          }
        };
      }
      _valueChanged() {}
    }
    customElements.define(FooBarElement.is, FooBarElement);
  </script>
</dom-module>

After Compilation/Renaming

<dom-module id="add-or-subtract">
  <template>
    <div><button on-tap="a">Increase</button></div>
    <div><button on-tap="b">Decrease</button></div>
  </template>
  <script>
    class A extends Polymer.Element {
      static get is() { return 'add-or-subtract'; }
      static get properties() {
        return {
          c: {
            type: Number,
            notify: true
          }
        };
      }
      a() { this.c++; }
      b() { this.c--; }
    }
    customElements.define(A.is, A);
  </script>
</dom-module>
<dom-module id="foo-bar">
  <template>
    <template is="dom-repeat" items="[[d]]" as="e">
      <add-or-subtract on-c-changed="{{f}}" value="[[e]]"></add-or-subtract>
    </template>
  </template>
  <script>
    class B extends Polymer.Element {
      static get is() { return 'foo-bar'; }
      static get properties() {
        return {
          d: {
            type: Array,
            value: () => [1, 2, 3, 4]
          }
        };
      }
      f() {}
    }
    customElements.define(B.is, B);
  </script>
</dom-module>

Using Polymer 2 Renaming

For Polymer 1, Closure-Compiler blocked renaming of any declared property. With Polymer 2, the compiler now uses the standard conventions: quoted properties are not renamed and other properties are. Declared properties with the reflectToAttribute or readOnly properties will never be renamed.

In addition, this project will rename attributes which map to properties of a custom element.

Usage

This project functions in two phases: pre and post closure-compiler compilation. Each phase has its own gulp plugin or can be used as native JS functions.

Pre-compilation: Extracting Data-binding Expressions

This first phase of the project parses polymer element templates and produces a javascript file to pass to closure-compiler.

The plugin makes use of the polymer-analyzer and requires both the HTML templates as well as any external JS source that contains element definitions.

Gulp

const gulp = require('gulp');
const polymerRename = require('polymer-rename');

gulp.task('extract-data-binding-expressions', function() {
  gulp.src('/src/components/**/*.html') // Usually this will be the bundled file - may also need to add .js files
      .pipe(polymerRename.extract({
        outputFilename: 'foo-bar.template.js'
      }))
      .pipe(gulp.dest('./build'));
});

Example Compilation

Closure-compiler's code-splitting flags allow the output to be divided into separate files. The compilation must reference the extern file included with this package.

In addition, the compiler can now warn about mismatched types, misspelled or missing property references and other checks.

let closureCompiler = require('google-closure-compiler');

gulp.task('compile-js', function() {
  gulp.src(['./src/js/app.js', './build/foo-bar.template.js'])
      .pipe(closureCompiler({
        compilation_level: 'ADVANCED',
        warning_level: 'VERBOSE',
        polymer_pass: true,
        module: [
          'app:1',
          'foo-bar.template:1:app'
        ],
        externs: [
          require.resolve('google-closure-compiler/contrib/externs/polymer-1.0.js'),
          require.resolve('polymer-rename/polymer-rename-externs.js')
        ]
      })
      .pipe(gulp.dest('./dist'));
});

Post-compilation: Find the Renamed Properties And Update the Template

After compilation, the compiler will have consistently renamed references to the properties. The generated javascript contains indexes into the original template which will now be replaced with their renamed versions.

Gulp

const polymerRename = require('polymer-rename');

gulp.task('update-html-template', function() {
  gulp.src('./src/components/foo-bar.html') // Usually this will be the bundled file
      .pipe(polymerRename.replace('./dist/foo-bar.template.js'))
      .pipe(gulp.dest('./dist/components'));
});

Element Type Names

polymer-rename obtains the type names of elements from polymer-analyzer. Type names for elements must be global.

Examples

Giving the following polymer template, the first phase of the project will extract the data-binding expressions and create a valid JS file:

<dom-module id="foo-bar" assetpath="/">
  <template>
    <div on-click="nameClicked">[[formatName(name)]]</div>
    <div>[[employer]]</div>
    <template is="dom-repeat" items="[[addresses]]">
      <div>[[item.street]]</div>
      <div>[[item.city]], [[item.state]] [[item.zip]]</div>
    </template>
  </template>
  <script>
  /**
   * @polymer
   * @customElement
   * @extends {Polymer.Element}
   */
  class FooBarElement extends Polymer.Element {
    static get is() { return "foo-bar"; }
    static get properties() {
      return {
        name: String,
        employer: String,
        
        /** @type {Array<{street: string, city: string, state: string, zip: string}>} */
        addresses: Array
      };
    }
    
    /**
     * @param {?string} a
     * @return {string}
     */
    formatName(a) { return a || ''; },
    
    /** @param {!Event=} evt */
    nameClicked(evt) {
      console.log(this.name);
    }
  }
  customElements.define(FooBarElement.is, FooBarElement);
  </script>
</dom-module>

Generated JavaScript from the extracted data-bound properties:

(/** @this {FooBarElement} */ function() {
  polymerRename.eventListener(72, 83, this.nameClicked);
  polymerRename.identifier(98, 102, this.name);
  this.formatName(this.name);
  polymerRename.method(87, 97, this.formatName);
  polymerRename.identifier(123, 131, this.employer);
  polymerRename.identifier(179, 188, this.addresses);
  for (let index = 0; index < this.addresses.length; index++) {
    let item = this.addresses[index];
    polymerRename.identifier(206, 217, item.street, item, 'item');
    polymerRename.identifier(239, 248, item.city, item, 'item');
    polymerRename.identifier(254, 264, item.state, item, 'item');
    polymerRename.identifier(269, 277, item.zip, item, 'item');
  }
}).call(/** @type {FooBarElement} */ (document.createElement("foo-bar")))

Each of the special function calls is defined as an extern. Each call takes a pair of indexes where the expression resides in the original file. After compilation, these indexes are used to replace the original expression with the now renamed properties and methods.

The compiler consumes the JS file and outputs the renamed expressions:

(function() {
  polymerRename.eventListener(72, 83, this.a);
  polymerRename.identifier(98, 102, this.b);
  this.c(this.b);
  polymerRename.method(87, 97, this.c);
  polymerRename.identifier(123, 131, this.d);
  polymerRename.identifier(179, 188, this.e);
  for (let a = 0; a < this.e.length; a++) {
    let b = this.e[a];
    polymerRename.identifier(206, 217, b.f, b, 'item');
    polymerRename.identifier(239, 248, b.g, b, 'item');
    polymerRename.identifier(254, 264, b.h, b, 'item');
    polymerRename.identifier(269, 277, b.i, b, 'item');
  }
}).call(document.createElement("foo-bar"))

The provided indexes are now used to update the original Polymer template.

Supporting String Based Polymer Features

Several functions and options in Polymer require providing property names as strings. This is problematic as Closure-Compiler does not rename strings. This would include the following features:

To work around such limitations, Closure-Compiler recognizes two special functions defined in Closure-Library. If your project does not utilize Closure-Library, you can simply copy the definitions for these two functions to your code base. As long as they are named the same, the compiler will recognize them.

Property Reflection with goog.reflect.objectProperty

goog.reflect.objectProperty returns a renamed string for an object instance. It's particularly helpful when calling notifyPath, notifySplices or set.

this.notifyPath(goog.reflect.objectProperty('foo', this), this.foo);

Property Reflection with goog.reflect.object

goog.reflect.object renames the keys of an object literal consistently with a provided constructor. It's useful when an instance of the object is not available.

// If using closure-library, this function is goog.object.transpose
function swapKeysAndValues(obj) {
  let swappedObj = {};
  Object.keys(obj).map(key => {
    swappedObj[obj[key]] = key;
  });
  return swappedObj;
}

var myCustomElementProps = swapKeysAndValues(
  goog.reflect.object(MyCustomElement, {
    _fooChanged: '_fooChanged'
  })
);

var MyCustomElement = Polymer({
  is: 'my-custom',
  properties: {
    foo: {
      type: Boolean,
      observer: myCustomElementProps['_fooChanged']
    }
  },
  _fooChanged: function(newValue, oldValue) {}
});

Limitations of This Project

Properties which are quoted are no longer renamed. However sub-paths are not analyzed. If the a property subpath should be blocked from renaming, use extern types to ensure that the compiler will not rename the paths.