Skip to content

Instantly share code, notes, and snippets.

@SMotaal
Last active March 17, 2018 13:36
Show Gist options
  • Save SMotaal/05889c2cd0f44cbd4314bf4703d27bcc to your computer and use it in GitHub Desktop.
Save SMotaal/05889c2cd0f44cbd4314bf4703d27bcc to your computer and use it in GitHub Desktop.
Polymer 2 / TypeScript (using 2.0-preview branches)

Rational

The TypeScript 2.0-typescript branch seems to be lagging and isolated, with all components having 2.0-preview branches yet the 2.0-typescript README has not even been changed from the 2.0-preview text. Until a proper Polymer/TypeScript workflow is recommended and supported by the Polymer team, the basic idea is to maximize on the use of the essential features that TypeScript offers and not invest too much into hard-core features that require the workflow to be tightly coupled with late-bound transpilation of complex TypeScript projects (with modules / loaders). Another thing to address is how Hydrolysis fails to analyze and iron-component-page fails to document TypeScript-based components, not making good use of the elegant property declarations and decorators used in TypeScript. For now, a temporary workaround is needed to ensure that properties are defined and documented with proper types and default values, but the workaround needs to be easily replaceable once we have better Polymer/TypeScript support from the Polymer team.

While we wait for awesome TypeScript libraries and definitions from the team, it is important to figure out the road with least resistance and easier refactoring!

Methodology

Polymer Components

Since the 2.0-typescript branch is not getting priority at the moment, I suggest we use the 2.0-preview branch as much as possible to get the most cross-component compatibility until a more stable 2.0-typescript release.

Custome Elements

It is more predictable to use approaches that closely mimic the conventions of working with Polymer/Vanilla until a more stable Polymer/TypeScript ecosystem is in place. So custom elements need to be 100% link/import compatible, transpiled on save and not later in the workflow, which means we must avoid using modules and module loaders (like System.js), and structure the classes properly to be compatible with Hydrolysis Analyzer. To overcome some of the issues that are obviously going to be phased out, custom elements should ideally extend an abstract class which extends from Polymer.Element, this abstract class will be used to resolve any transient issues (hide the ugly workarounds) until things are more stable, then it would be possible to remove the abstract class and extend from Polymer.Element directly.

TypeScript Awesomeness

Until there is a stable Polymer/TypeScript workflow, we need to make TypeScript happy with Polymer. By extracting a copy of the types from the 2.0-typescript branch and keeping them locally scoped it is possible to create transpilable TypeScript implementations today with a greater degree of stability, making the transition to the up-coming Polymer/TypeScript workflow predictably controlled. It is also a good idea to extract the decorators.ts and compile them de-modularized into a decorators.js to be able to use decorators without depending on module loaders until an ideal workflow is identified.

Testing

Until Polymer 2.0-preview testing is more stable, let's not touch this topic!

/**
* @license
* Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
// This file requires the reflect-metadata package to be loaded.
/// <reference path="../bower_components/reflect-metadata/Reflect.d.ts" />
// /**
// * A TypeScript class decorator that defines a custom element with name
// * `tagname` and the decorated class.
// */
// export function customElement(tagname: string) {
// return (clazz: any) => {
// clazz.is = tagname;
// window.customElements.define(tagname, clazz);
// }
// }
/**
* A TypeScript class decorator that defines with name `tagname` or dash-case
* format derived from `prototype.name`.
*
* Convention-friendly implementation (also, I really hate the word clazz)
*/
function customElement(tagname?: string) {
return (prototype: any) => {
prototype.is = prototype.is || tagname || prototype.name.replace(/([a-z])(([0-9]+|[A-Z]+)[a-z]*)/g, '$1-$2').toLowerCase();
window.customElements.define(prototype.is, prototype);
}
}
/*export*/ interface PropertyOptions {
notify?: boolean;
};
/**
* A TypeScript property decorator factory that defines this as a Polymer
* property.
*
* This function must be invoked to return a decorator.
*/
/*export*/ function property<T>(options?: PropertyOptions | boolean) {
return (proto: any, propName: string) : any => {
const notify: boolean = typeof options == "object" && options.notify || options === true;
const type = Reflect.getMetadata("design:type", proto, propName);
const config = _ensureConfig(proto);
config.properties[propName] = {
type,
notify,
};
}
}
/**
* A TypeScript property decorator factory that causes the decorated method to
* be called when a property changes. `targets` is either a single property
* name, or a list of property names.
*
* This function must be invoked to return a decorator.
*/
/*export*/ function observe(targets: string|string[]) {
return (proto: any, propName: string) : any => {
const targetString = typeof targets === 'string' ? targets : targets.join(',');
const config = _ensureConfig(proto);
config.observers.push(`${propName}(${targetString})`);
}
}
/**
* A TypeScript property decorator factory that converts a class property into a
* getter that executes a querySelector on the element's shadow root.
*
* By annotating the property with the correct type, element's can have
* type-checked access to internal elements.
*
* This function must be invoked to return a decorator.
*/
/*export*/ const query = _query(
(target: NodeSelector, selector: string) => target.querySelector(selector));
/**
* A TypeScript property decorator that converts a class property into a getter
* that executes a querySelectorAll on the element's shadow root.
*
* By annotating the property with the correct type, element's can have
* type-checked access to internal elements. The type should be NodeList
* with the correct type argument.
*
* This function must be invoked to return a decorator.
*/
/*export*/ const queryAll = _query(
(target: NodeSelector, selector: string) => target.querySelectorAll(selector));
interface Config {
properties: {[name: string]: PropertyDefinition};
observers: string[];
}
interface PropertyDefinition {
notify?: boolean;
type: Function;
}
function _ensureConfig(proto: any): Config {
const ctor = proto.constructor;
if (ctor.hasOwnProperty('__polymer_ts_config')) {
return ctor.__polymer_ts_config;
}
Object.defineProperty(ctor, 'config', {
get() { return ctor.__polymer_ts_config; }
});
const config: Config = ctor.__polymer_ts_config = ctor.__polymer_ts_config || {};
config.properties = config.properties || {};
config.observers = config.observers || [];
return config;
}
function _query(queryFn: (target: NodeSelector, selector: string) => Element|NodeList) {
return (selector: string) => (proto: any, propName: string): any => {
Object.defineProperty(proto, propName, {
get() {
return queryFn(this.shadowRoot, selector);
},
enumerable: true,
configurable: true,
});
};
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Polymer 2/TypeScript Custom Element v1 Demo</title>
<script src="../../webcomponentsjs/webcomponents-lite.js"></script>
<link rel="import" href="../flow-magic.html">
</head>
<body>
<my-custom-element>No worky yet! No worky at all?!</my-custom-element>
</body>
</html>
<link rel="import" href="../polymer/polymer-element.html">
<script src="../reflect-metadata/reflect.js"></script> <!-- bower install -S reflect-metadata -->
<script src="polymer/decorators.js"></script> <!-- modified non-module version -->
<script src="flow-element.js"></script> <!-- transpiled TypeScript base class -->
/// <reference path="types.d.ts" />
/**
* # `<flow-element>` thenerdyguy's Polymer 2.0-preview element base-class for Typescript
*
* This is an abstract element and should be extended by other elements. It deals
* with the issues that should be resolved over time to make classes as close to
* the final anticipated Polymer 2/Typescript format as possible.
*
* @group Flow Elements
*/
class FlowElement extends (<IPolymerElementConstructor>Polymer.Element) {
static is: string;
static properties: IPolymerProperties;
/**
* @private Workaround for documentation with Hydrolysis Analyzer
* To sync TypeScript properties for extending class, define
* a similar static beforeRegister() and call it immeditaly
* after declaring the extending class.
*/
static beforeRegister() {
this.is = "flow-element";
this.properties = {};
}
/**
* Workaround to prevent super.connectedCallback() from
* raising a TypeScript error. Although TypeScript indicates
* an error, it will still compile, but now any subclass
* can safely call super.connectedCallback() without squigglies
*/
connectedCallback() {
typeof super.connectedCallback == 'function' && (<any>super.connectedCallback)();
}
}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Polymer 2/TypeScript Custom Element v1</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="../webcomponentsjs/webcomponents-lite.js"></script>
<link rel="import" href="../iron-component-page/iron-component-page.html">
</head>
<body unresolved>
<iron-component-page></iron-component-page>
</body>
</html>
<link rel="import" href="flow-element.html">
<!-- <link rel="import" href="my-shared-styles.html"> to use with style includes-->
<dom-module id="my-custom-element">
<template>
<style>
:host { display: block; }
</style>
<div id="element-container">
Test1: {{test1}}
</div>
</template>
<script type="text/javascript" src="my-custom-element.js"></script>
</dom-module>
/**
* # `<my-custom-element>` is an awesome Polymer/TypeScript element
*
* In typical use, just slap some `<my-custom-element>` at the top of your body:
*
* <body>
* <my-custom-element></my-custom-element>
*
* @demo demo.html
*/
class MyCustomElement extends FlowElement {
@property() test1:String = MyCustomElement.getDefault("test1", "def");
/**
* @private Workaround to Synchronize Hydrolysis/Typescript
*/
static beforeRegister() {
this.is = "my-custom-element"; // needed for Hydrolysis
this.properties = {
// Description for test1
test1: { type: String, value: "def" }
};
delete this.beforeRegister, customElement(this.is)(this); // make sure the method is only called once!
}
/**
* @private Workaround to Synchronize Hydrolysis/Typescript property values;
*/
static getDefault = (property: string, fallback?: any) => ("value" in MyCustomElement.properties[property] || {}) ? MyCustomElement.properties[property].value : fallback || undefined
/**
* @private Workaround to Synchronize Hydrolysis/Typescript descriptions;
*/
static autoInitialized = MyCustomElement.beforeRegister();
}
// /* No longer needed */ MyCustomElement.beforeRegister(), customElements.define(MyCustomElement.is, MyCustomElement);
{
"compileOnSave": true,
"compilerOptions": {
"target": "es2015",
"module": "none",
"isolatedModules": false,
"declaration": true,
"removeComments": false,
"preserveConstEnums": true,
"suppressImplicitAnyIndexErrors": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictNullChecks": true,
"noImplicitThis": true,
"noImplicitAny": true,
"sourceMap": true,
"lib": ["es2017", "dom"],
"typeRoots": ["types"]
},
"include": [
"types/polymer/polymer.d.ts",
"types/polymer/decorators.d.ts",
"types/element.d.ts",
"types/shadow.d.ts",
"types/types/dom.d.ts",
"polymer/decorators.ts",
"types.d.ts",
"*.ts"
]
}
/// <reference path="types/polymer/polymer.d.ts" />
/// <reference path="types/element.d.ts" />
/// <reference path="types/shadow.d.ts" />
/// <reference path="types/dom.d.ts" />
declare interface IPolymerElement {
connectedCallback?(): void;
}
declare interface IPolymerElementConstructor {
new (): IPolymerElement;
}
declare interface IPolymerPropertyDefinition extends PropertyDefinition {
value?: any;
}
declare interface IPolymerProperties {
[name: string]: IPolymerPropertyDefinition
}
@SMotaal
Copy link
Author

SMotaal commented Mar 17, 2018

Where has this taken you 😉

Amazing how fast the world moves… Sadly not everything in it does though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment