-
Notifications
You must be signed in to change notification settings - Fork 25.3k
/
ng-element-constructor.ts
141 lines (118 loc) Β· 5.37 KB
/
ng-element-constructor.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ComponentFactory, EventEmitter} from '@angular/core';
import {NgElementImpl, NgElementWithProps} from './ng-element';
import {NgElementApplicationContext} from './ng-element-application-context';
import {camelToKebabCase, throwError} from './utils';
/**
* TODO(gkalpak): Add docs.
* @experimental
*/
export interface NgElementConstructor<T, P> {
readonly is: string;
readonly observedAttributes: string[];
upgrade(host: HTMLElement): NgElementWithProps<T, P>;
new (): NgElementWithProps<T, P>;
}
export interface NgElementConstructorInternal<T, P> extends NgElementConstructor<T, P> {
readonly onConnected: EventEmitter<NgElementWithProps<T, P>>;
readonly onDisconnected: EventEmitter<NgElementWithProps<T, P>>;
upgrade(host: HTMLElement, ignoreUpgraded?: boolean): NgElementWithProps<T, P>;
}
type WithProperties<P> = {
[property in keyof P]: P[property]
};
// For more info on `PotentialCustomElementName` rules see:
// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
const PCEN_RE = createPcenRe();
const PCEN_BLACKLIST = [
'annotation-xml',
'color-profile',
'font-face',
'font-face-src',
'font-face-uri',
'font-face-format',
'font-face-name',
'missing-glyph',
];
export function createNgElementConstructor<T, P>(
appContext: NgElementApplicationContext,
componentFactory: ComponentFactory<T>): NgElementConstructorInternal<T, P> {
const selector = componentFactory.selector;
if (!isPotentialCustomElementName(selector)) {
throwError(
`Using '${selector}' as a custom element name is not allowed. ` +
'See https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name for more info.');
}
const inputs = componentFactory.inputs.map(({propName, templateName}) => ({
propName,
attrName: camelToKebabCase(templateName),
}));
const outputs =
componentFactory.outputs.map(({propName, templateName}) => ({
propName,
// TODO(gkalpak): Verify this is what we want and document.
eventName: templateName,
}));
// Note: According to the spec, this needs to be an ES2015 class
// (i.e. not transpiled to an ES5 constructor function).
// TODO(gkalpak): Document that if you are using ES5 sources you need to include a polyfill (e.g.
// https://github.com/webcomponents/custom-elements/blob/32f043c3a/src/native-shim.js).
class NgElementConstructorImpl extends NgElementImpl<T> {
static readonly is = selector;
static readonly observedAttributes = inputs.map(input => input.attrName);
static readonly onConnected = new EventEmitter<NgElementWithProps<T, P>>();
static readonly onDisconnected = new EventEmitter<NgElementWithProps<T, P>>();
static upgrade(host: HTMLElement, ignoreUpgraded = false): NgElementWithProps<T, P> {
const ngElement = new NgElementConstructorImpl();
ngElement.setHost(host);
ngElement.connectedCallback(ignoreUpgraded);
return ngElement as typeof ngElement & WithProperties<P>;
}
constructor() {
super(appContext, componentFactory, inputs, outputs);
const ngElement = this as this & WithProperties<P>;
this.onConnected.subscribe(() => NgElementConstructorImpl.onConnected.emit(ngElement));
this.onDisconnected.subscribe(() => NgElementConstructorImpl.onDisconnected.emit(ngElement));
}
}
inputs.forEach(({propName}) => {
Object.defineProperty(NgElementConstructorImpl.prototype, propName, {
get: function(this: NgElementImpl<any>) { return this.getInputValue(propName); },
set: function(this: NgElementImpl<any>, newValue: any) {
this.setInputValue(propName, newValue);
},
configurable: true,
enumerable: true,
});
});
return NgElementConstructorImpl as typeof NgElementConstructorImpl & {
new (): NgElementConstructorImpl&WithProperties<P>;
};
}
function createPcenRe() {
// According to [the
// spec](https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name),
// `pcenChar` is allowed to contain Unicode characters in the 10000-EFFFF range. But in order to
// match this characters with a RegExp, we need the implementation to support the `u` flag.
// On browsers that do not support it, valid PotentialCustomElementNames using characters in the
// 10000-EFFFF range will still cause an error (but these characters are not expected to be used
// in practice).
let pcenChar = '-.0-9_a-z\\u00B7\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u037D\\u037F-\\u1FFF' +
'\\u200C-\\u200D\\u203F-\\u2040\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF' +
'\\uF900-\\uFDCF\\uFDF0-\\uFFFD';
let flags = '';
if (RegExp.prototype.hasOwnProperty('unicode')) {
pcenChar += '\\u{10000}-\\u{EFFFF}';
flags += 'u';
}
return RegExp(`^[a-z][${pcenChar}]*-[${pcenChar}]*$`, flags);
}
function isPotentialCustomElementName(name: string): boolean {
return PCEN_RE.test(name) && (PCEN_BLACKLIST.indexOf(name) === -1);
}