-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathshadow-import.js
309 lines (264 loc) · 10.4 KB
/
shadow-import.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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
/**
* <shadow-import> HTML tag to create custom HTML elements.
* @author Nate Ferrero
* @author Trey Cordova
* @muse Jarrod Perez
* @energy Jeremy Bernstein
*/
(function () {
var shadowImport = Object.create(HTMLElement.prototype);
shadowImport.linkImports = {};
shadowImport.renderTemplate = {};
shadowImport.renderQueue = {};
var component;
this.ShadowComponent = function ShadowComponent(fn) {
if (component) {
throw new Error("ShadowComponent() called outside of <shadow-import> lifecycle");
}
if (typeof fn != 'function') {
throw new Error("ShadowComponent() called without a function as the first argument");
}
component = function ShadowComponent (el, attrs, content) {
this.el = el;
this.attrs = attrs;
this.content = content;
typeof this.init === 'function' && this.init();
};
fn(component, component.prototype);
};
/**
* Attributes on the custom element
*/
var ShadowAttributes = function (elem) {
this.$elem = elem;
this.$watchers = {};
};
ShadowAttributes.prototype.init = function () {
var len = this.$elem.attributes.length;
for (var i = 0; i < len; i++) {
this.changed(this.$elem.attributes[i].name, this.$elem.attributes[i].value, undefined);
}
};
ShadowAttributes.prototype.get = function (attrName, def) {
var attr = this.$elem.attributes.getNamedItem(attrName);
return attr ? attr.value : def;
};
ShadowAttributes.prototype.set = function (attrName, val) {
var attr = document.createAttribute(attrName);
attr.value = val;
return this.$elem.attributes.setNamedItem(attr);
};
ShadowAttributes.prototype.changed = function (attrName, newVal, oldVal) {
if (Array.isArray(this.$watchers[attrName])) {
this.$watchers[attrName].forEach(function (watcher) {
watcher.call(this.$elem.shadowComponent, newVal, oldVal);
}, this);
}
};
ShadowAttributes.prototype.watch = function (attrName, fn) {
if (!Array.isArray(this.$watchers[attrName])) {
this.$watchers[attrName] = [];
}
this.$watchers[attrName].push(fn);
};
/**
* A function to create a <link rel="import"> tag to load a template
* and render when complete.
*/
var loadTemplateContent = function (templateUrl, callback) {
/**
* Load the template asynchronously and process any waiting queue
* of custom element nodes when ready.
*/
var link = document.createElement('link');
link.rel = 'import';
link.href = templateUrl;
/**
* The template is now available, proceed with rendering
*/
link.onload = function (e) {
var templateNode = e.target.import.querySelector('template');
/**
* Component HTML files must contain a <template> tag
* or else we throw this error.
*/
if (!templateNode) {
throw new Error('ShadowImport: <template> tag not found in: ' + templateUrl);
}
callback(templateNode.content);
};
/**
* The template failed to load for some reason
*/
link.onerror = function(e) {
throw new Error('ShadowImport: Unable to load component template: ' + templateUrl)
};
/**
* Append the link import to the document <head> tag
*/
document.head.appendChild(link);
};
/**
* Create a custom element
*/
shadowImport.createCustomElement = function () {
/**
* Prototype for the custom element
*/
var customElement = Object.create(HTMLElement.prototype);
/**
* Callback for any attribute changes
* Note the reversed order (many use cases will not need the argument oldVal)
*/
customElement.attributeChangedCallback = function (attrName, oldVal, newVal) {
this.shadowAttributes.changed(attrName, newVal, oldVal);
};
/**
* Callback for any time the custom tag is created
*/
customElement.createdCallback = function () {
/**
* Create a shadow root on the custom element
*/
this.createShadowRoot();
/**
* Cache the content
*/
this.content = document.createDocumentFragment();
while(this.childNodes.length) {
this.content.appendChild(this.childNodes.item(0));
}
var elem = this;
/**
* If the template loader has not been initialized for the URL in this.template,
* create a new asychronous template loader function and put it under
* shadowImport.linkImports[this.template]
*/
if (!(elem.href in shadowImport.linkImports)) {
/**
* Call this function at any time with a custom element node,
* and when the template is available, the shadow DOM will
* have the template cloned onto it.
*/
shadowImport.linkImports[elem.href] = function (elem) {
/**
* If the template function is ready, just call it now
* with the custom element node.
*/
if (elem.href in shadowImport.renderTemplate) {
shadowImport.renderTemplate[elem.href](elem);
}
/**
* Otherwise, put it in the queue to be processed when the template
* is finally loaded.
*/
else {
if (!shadowImport.renderQueue[elem.href]) {
shadowImport.renderQueue[elem.href] = [];
}
shadowImport.renderQueue[elem.href].push(elem);
}
};
/**
* Load the template content from the elem.href and instantiate the component class
*/
loadTemplateContent(elem.href, function (templateContent) {
/**
* Ensure that the component was registered
*/
if (!component) {
throw new Error("ShadowComponent() not called during <shadow-import> tag initialization, should have been included in " + elem.href);
}
/**
* Retain a reference to the imported component
*/
var CustomClass = component;
/**
* Reset the component after use
*/
component = null;
/**
* Function to call when the template is loaded
*/
var templateLoaded = shadowImport.renderTemplate[elem.href] = function (elem) {
/**
* Clone the template, and append it to the custom element node
*/
elem.shadowRoot.appendChild(templateContent.cloneNode(true));
/**
* Finally, instantiate the new component class with the custom element node
* and the attribute manager
*/
elem.shadowComponent = new CustomClass(elem.shadowRoot, elem.shadowAttributes, elem.content);
elem.shadowAttributes.init();
};
/**
* Process all queued shadow DOM nodes that require this template
*/
if (Array.isArray(shadowImport.renderQueue[elem.href])) {
shadowImport.renderQueue[elem.href].forEach(templateLoaded);
}
});
}
/**
* Initiate the link import process with a template URL and a custom element node
*/
elem.shadowAttributes = new ShadowAttributes(elem);
shadowImport.linkImports[elem.href](elem);
};
return customElement;
};
/**
* Callback for any time the <shadow-import> element is created
*/
shadowImport.createdCallback = function () {
/**
* Load the custom tag name for the new custom element
*/
var tag = this.attributes.getNamedItem('tag');
/**
* All <shadow-import> tags must contain a tag attribute
* This is what the new custom element will be created as
*/
if (!tag) {
throw new Error('<shadow-import> found without tag="" attribute');
}
/**
* Custom element tag names require a dash in the name, and cannot start with a dash
*/
else if (tag.value.indexOf('-') < 1) {
throw new Error('<shadow-import> tag="" attribute must contain a dash');
}
/**
* <shadow-import> tags may define a template attribute, otherwise the default
* will be used
*/
var href = this.attributes.getNamedItem('href');
/**
* <shadow-import> tags may define a shadow-class attribute, otherwise the default
* will be used
*/
var shadowClass = this.attributes.getNamedItem('shadow-class');
var defaultClass = tag.value.replace(/^.|-./g, function (x) {
return x.toUpperCase().replace('-', '');
}) + 'Element';
/**
* Create the custom element
*/
var customElement = this.createCustomElement();
customElement.href = href ? href.value : 'components/' + tag.value + '/component.html';
customElement.shadowClass = shadowClass ? shadowClass.value : defaultClass;
/**
* Register the custom element as <tag.value>
*/
document.registerElement(tag.value, {
prototype: customElement
});
};
/**
* Register the <shadow-import> tag on the document
*/
var ShadowImport = document.registerElement('shadow-import', {
prototype: shadowImport
});
})();