-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathCustomElement.js
213 lines (200 loc) · 8.46 KB
/
CustomElement.js
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
"use strict";
/**
* @module CustomElement
*/
/**
* Create a DOM Event without worrying about browser compatibility
*
* @param {string} eventName The name of the event to create
* @param {*} [detail] The optional details to attach to the event
* @return {Event} A valid Event instance that can be dispatched on a DOM node
* @example
* var event = CustomElement.makeEvent('change', { name: this._secretInput.value })
* this.dispatchEvent(event)
*/
exports.makeEvent = makeMakeEvent();
var makeClass = makeMakeClass();
function noOp() {}
/**
* Register a custom element using some straightforward and convenient configuration
*
* @param {Object} config
* @param {string} config.tagName The name of the tag for which to create a custom element. Must contain at least one `-` and must not have already been defined.
* @param {Object<string, Object>} config.properties Getters and setters for properties on the custom element that can be accessed and set with `Html.Attributes.property`. These should map a property name onto a `get` function that returns a value and a `set` function that applies a value from the only argument.
* @param {Object<string, function>} config.methods Functions that can be invoked by the custom element from anywhere. These methods are automatically bound to `this`.
* @param {function} config.initialize Method invoked during setup that can be used to initialize internal state. This function is called whenever a new instance of the element is created.
* @param {function} config.onConnect Method invoked whenever the element is inserted into the DOM.
* @param {function} config.onDisconnect Method invoked whenever the element is removed from the DOM.
* @param {function} config.onAttributeChange Method invoked whenever an observed attribute changes. Takes 3 arguments: the name of the attribute, the old value, and the new value.
* @param {string[]} config.observedAttributes List of attributes that are watched such that onAttributeChange will be invoked when they change.
* @example
* CustomElements.create({
* // This is where you specify the tag for your custom element. You would
* // use this custom element with `Html.node "my-custom-tag"`.
* tagName: 'my-cool-button',
*
* // Initialize any local variables for the element or do whatever else you
* // might do in a class constructor. Takes no arguments.
* // NOTE: the element is NOT in the DOM at this point.
* initialize: function() {
* this._hello = 'world'
* this._button = document.createElement('button')
* },
*
* // Let the custom element runtime know that you want to be notified of
* // changes to the `hello` attribute
* observedAttributes: ['hello'],
*
* // Do any updating when an attribute changes on the element. Note the
* // difference between attributes and properties of an element (see:
* // https://javascript.info/dom-attributes-and-properties). This is a
* // proxy for `attributeChangedCallback` (see:
* // https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#Using_the_lifecycle_callbacks).
* // Takes the name of the attribute that changed, the previous string value,
* // and the new string value.
* onAttributeChange: function(name, previous, next) {
* if (name === 'hello') this._hello = next
* },
*
* // Do any setup work after the element has been inserted into the DOM.
* // Takes no arguments. This is a proxy for `connectedCallback` (see:
* // https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#Using_the_lifecycle_callbacks)
* onConnect: function() {
* document.addEventListener('click', this._onDocClick)
* this._button.addEventListener('click', this._onButtonClick)
* this.appendChild(this._button)
* },
*
* // Do any teardown work after the element has been removed from the DOM.
* // Takes no arguments. This is a proxy for `disconnectedCallback` (see:
* // https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#Using_the_lifecycle_callbacks)
* onDisconnect: function() {
* document.removeEventListener('click', this._onDocClick)
* },
*
* // Set up properties. These allow you to expose data to Elm's virtual DOM.
* // You can use any value that can be encoded as a `Json.Encode.Value`.
* // You'll often want to implement updates to some visual detail of your element
* // from within the setter of a property that controls it. Handlers will be
* // automatically bound to the correct value of `@`, so you don't need to worry
* // about method context.
* properties: {
* hello: {
* get: function() {
* return this._hello
* },
* set: function(value) {
* this._hello = value
* this._button.textContent = value
* }
* }
* },
*
* // Set up methods that you can call from anywhere else in the configuration.
* // Methods will be automatically bound to the correct value of `@`, so you
* // don't need to worry about method context.
* methods: {
* _onDocClick: function() {
* alert('document clicked')
* },
* _onButtonClick: function() {
* alert('clicked on ' + this._hello + ' button')
* }
* }
* })
*/
exports.create = function create(config) {
if (customElements.get(config.tagName)) {
throw Error(
"Custom element with tag name " + config.tagName + " already exists.",
);
}
var observedAttributes = config.observedAttributes || [];
var methods = config.methods || {};
var properties = config.properties || {};
var initialize = config.initialize || noOp;
var onConnect = config.onConnect || noOp;
var onDisconnect = config.onDisconnect || noOp;
var onAttributeChange = config.onAttributeChange || noOp;
var Class = makeClass();
for (var key in methods) {
if (!methods.hasOwnProperty(key)) continue;
Class.prototype[key] = methods[key];
}
Object.defineProperties(Class.prototype, properties);
Class.prototype.connectedCallback = onConnect;
Class.prototype.disconnectedCallback = onDisconnect;
Class.prototype.attributeChangedCallback = onAttributeChange;
if (Array.isArray(observedAttributes)) {
Object.defineProperty(Class, "observedAttributes", {
get: function () {
return observedAttributes;
},
});
}
Class.displayName = "<" + config.tagName + "> custom element";
customElements.define(config.tagName, Class);
};
/**
* Attempt to make an ES6 class using the Function constructor rather than
* ordinary class syntax. The string we pass to the Function constructor is
* static so there is no script injection risk. It allows us to catch
* syntax errors at runtime for older browsers that don't support class and
* fall back to an ES5 constructor function.
*/
function makeMakeClass() {
try {
return new Function(
[
"return class extends HTMLElement {",
" constructor() {",
" super()",
" for (var key in this) {",
" var value = this[key]",
" if (typeof value !== 'function') continue",
" this[key] = value.bind(this)",
" }",
" }",
"}",
].join("\n"),
);
} catch (e) {
return function () {
function Class() {
// This is the best we can do to trick modern browsers into thinking this
// is a real, legitimate class constructor and not a plane old JS function.
var _this = HTMLElement.call(this) || this;
for (var key in _this) {
var value = _this[key];
if (typeof value !== "function") continue;
_this[key] = value.bind(_this);
}
return _this;
}
Class.prototype = Object.create(HTMLElement.prototype);
Class.prototype.constructor = Class;
return Class;
};
}
}
/**
* Return a function for making an event based on what the browser supports.
* IE11 doesn't support Event constructor, and uses the old Java-style
* methods instead
*/
function makeMakeEvent() {
try {
// if calling Event with new works, do it that way
var testEvent = new CustomEvent("myEvent", { detail: 1 });
return function makeEventNewStyle(type, detail) {
return new CustomEvent(type, { detail: detail });
};
} catch (_error) {
// if calling CustomEvent with new throws an error, do it the old way
return function makeEventOldStyle(type, detail) {
var event = document.createEvent("CustomEvent");
event.initCustomEvent(type, false, false, detail);
return event;
};
}
}