UNPKG

12.8 kBJavaScriptView Raw
1import kebabCase from 'lodash.kebabcase';
2import camelCase from 'lodash.camelcase';
3import cloneDeep from 'lodash.clonedeep';
4import Property from './property';
5import { noop, decorate, clone, identity } from './utilities';
6import { ByFilter, EqualFilter } from './filters';
7import {
8 isFunction,
9 isNull,
10 isEqualTo,
11 isDeeplyEqual,
12 isArray,
13 isUndefined,
14 isElement,
15 isString,
16 isEmpty,
17 isObject,
18 isPlainObject,
19 assert
20} from './assertions';
21// import StatefulComponent from './decorators/stateful';
22
23
24const internal = {
25 factories: [],
26 allowedHooks: ['attach', 'detach', 'initialize', 'render']
27};
28
29const $pwet = Symbol('pwet');
30
31internal.parseProperties = input => {
32
33 const properties = [];
34
35 if (!isObject(input))
36 return properties;
37
38 const keys = Object.keys(input);
39
40 if (isEmpty(keys))
41 return properties;
42
43 return keys.reduce((properties, key) => {
44
45 let property = input[key];
46
47 if (!isObject(property))
48 property = { defaultValue: property };
49
50 property.name = key;
51
52 property = Property(property);
53
54 properties.push(property);
55
56 return properties;
57 }, properties);
58};
59
60internal.StatelessError = () => {
61 throw new Error('Component is Stateless');
62};
63
64internal.defaultsHooks = {
65 attach(component, attach) {
66
67 attach(true);
68 },
69 initialize: (component, newProperties, initialize) => initialize(true)
70};
71
72
73const Component = (factory, element, dependencies) => {
74
75 assert(Component.get(factory), `'factory' must be a defined component factory`);
76 assert(isElement(element), `'element' must be a HTMLElement`);
77
78 if (element[$pwet] !== void 0)
79 return;
80
81 let _isCreated = false;
82 let _isAttached = false;
83 let _isRendered = false;
84 let _isInitializing = false;
85 let _isInitialized = false;
86 let _properties = {};
87
88 if (factory.shadow)
89 element.attachShadow(factory.shadow);
90
91 const attach = () => {
92
93 if (factory.logLevel > 0)
94 console.log(`[${factory.tagName}]`, 'attach()');
95
96 if (_isAttached)
97 return;
98
99 component.hooks.attach((shouldRender) => {
100 _isAttached = true;
101
102 if (!_isRendered && shouldRender)
103 component.render();
104 });
105
106 };
107
108 attach.after = (shouldUpdate = true) => {
109
110 // console.log('Component.attach.after()', { shouldUpdate, _isAttached });
111
112 if (shouldUpdate)
113 component.render();
114
115 };
116
117 const detach = () => {
118
119 if (factory.logLevel > 0)
120 console.log(`[${factory.tagName}]`, 'detach()');
121
122 if (!_isAttached)
123 return;
124
125 _isAttached = false;
126
127 component.hooks.detach();
128 };
129
130 const initialize = (properties = {}) => {
131
132 if (_isInitializing)
133 return;
134
135 if (factory.logLevel > 0)
136 console.log(`[${factory.tagName}]`, 'initialize()', properties, _properties);
137
138 assert(isObject(properties) && !isNull(properties), `'properties' must be an object`);
139
140 properties = factory.properties.reduce((newProperties, { name, coerce, defaultValue, isDataAttribute }) => {
141
142 let oldValue = _properties[name];
143 let newValue = properties[name];
144
145 if (isUndefined(newValue) && isDataAttribute)
146 newValue = properties[`data-${name}`];
147
148 // console.log('HEY', name, oldValue, newValue);
149
150 // newValue = isUndefined(newValue)
151 // ? (_properties.hasOwnProperty(name) ? oldValue : defaultValue)
152 // : newValue;
153
154 // if (isUndefined(newValue) && _properties.hasOwnProperty(name))
155 // newValue = _properties[name];
156
157
158 newValue = !isUndefined(newValue)
159 ? coerce(newValue)
160 : defaultValue;
161
162 return Object.assign(newProperties, { [name]: newValue })
163 }, {});
164
165 if (isDeeplyEqual(properties, _properties)) {
166 if (factory.logLevel > 0)
167 console.warn(`[${factory.tagName}]`, 'aborted initialization (properties are unchanged)', properties, _properties);
168 return;
169 }
170 // console.log(`[${factory.tagName}]`, 'initializing...', newProperties);
171
172 _isInitializing = true;
173 // console.log(`[${factory.tagName}]`, 'aaaaa', component.hooks.initialize);
174
175 component.hooks.initialize(properties, (shouldRender) => {
176 _properties = properties;
177 // Object.assign(_properties, properties);
178
179 _isInitializing = false;
180 _isInitialized = true;
181
182 if (shouldRender)
183 component.render();
184
185 // console.log(`[${factory.tagName}]`, 'initialized', properties);
186
187 });
188 };
189
190 const render = () => {
191
192 if (factory.logLevel > 0)
193 console.log(`[${factory.tagName}]`, 'render()', { _isAttached, ..._properties });
194
195 if (!_isAttached)
196 return;
197
198 component.hooks.render();
199
200 _isRendered = true;
201 };
202
203 const component = element[$pwet] = {
204 isPwetComponent: true,
205 element,
206 factory,
207 attach,
208 render,
209 detach,
210 get isRendered() {
211 return _isRendered
212 },
213 get isAttached() {
214 return _isAttached
215 },
216 get isInitializing() {
217 return _isInitializing
218 },
219 get isInitialized() {
220 return _isInitialized
221 },
222 get hooks() {
223 return _hooks
224 },
225 // get state() {
226 // return _hooks
227 // },
228 // set properties(newValue) {
229 // return _hooks
230 // }
231 };
232
233 Object.assign(element, {
234 initialize
235 });
236
237 const _hooks = factory.allowedHooks.reduce((hooks, key) => {
238 return Object.assign(hooks, { [key]: factory[key].bind(null, component) });
239 }, {});
240
241 Object.defineProperty(element, 'properties', {
242 get() {
243 return cloneDeep(_properties);
244 },
245 set: initialize
246 });
247
248 initialize(factory.properties.reduce((properties, { name, isDataAttribute, parse, defaultValue }) => {
249
250 Object.defineProperty(element, name, {
251 get() {
252 return _properties[name];
253 },
254 set(newValue) {
255
256 initialize(Object.assign(element.properties, {
257 [name]: newValue
258 }));
259 }
260 });
261
262 let value = defaultValue;
263
264 if (isDataAttribute) {
265
266 const attributeValue = element.dataset[name];
267
268
269 if (!isUndefined(attributeValue))
270 value = parse(attributeValue);
271 }
272
273 return Object.assign(properties, { [name]: value });
274 }, {}));
275
276
277 const returned = factory.create(component, factory.dependencies);
278
279 if (!isObject(returned) || isNull(returned))
280 return component;
281
282 Object.keys(returned)
283 .forEach(key => {
284
285 // if (key === 'properties') {
286 //
287 // _initialProperties = returned.properties;
288 // return;
289 // }
290
291 if (!factory.allowedHooks.includes(key))
292 return;
293
294 const hook = returned[key];
295
296 assert(isFunction(hook), `'${key}' hook must be a function`);
297
298 _hooks[key] = hook;
299 });
300
301
302 assert(_hooks.render !== noop, `'render' method is required`);
303
304 const _attributes = factory.properties
305 .filter(property => property.isAttribute === true)
306 .reduce((attributes, attribute) => {
307
308 let name = kebabCase(attribute.name);
309
310 if (attribute.isDataAttribute)
311 name = `data-${name}`;
312
313 return Object.assign(attributes, { [name]: attribute });
314 }, {});
315
316 const _attributesName = Object.keys(_attributes);
317
318 const _observer = new MutationObserver(mutations => {
319 // mutations.forEach(function(mutation) {
320 // console.error(mutations);
321 // });
322
323 mutations = mutations
324 .filter(({ attributeName }) => _attributesName.includes(attributeName))
325 .map(({ attributeName:name, oldValue }) => ({
326 name,
327 oldValue,
328 value: element.getAttribute(name)
329 }))
330 .filter(({ value, oldValue }) => !isEqualTo(value, oldValue));
331
332 if (isEmpty(mutations))
333 return;
334
335 console.error(`[${factory.tagName}]`, 'ATTRIBUTES MUTATIONS', mutations.map(({ name, value }) => `${name}=${value}`));
336 const { properties } = element;
337
338 initialize(Object.assign(properties, mutations.reduce((attributes, { name, value }) => {
339
340 const { parse, isDataAttribute } = _attributes[name];
341
342 name = camelCase(isDataAttribute ? name.slice(5) : name);
343
344 return Object.assign(attributes, { [name]: parse(value) });
345 }, {})));
346
347 // const { name, parse, isDataAttribute } = _attributes[attributeName];
348 //
349 // // console.error(`[${factory.tagName}]`, 'attributeChangedCallback', name, typeof newValue, this.pages);
350 //
351 // properties[name] = parse(newValue);
352 //
353 // this.initialize(properties);
354
355 });
356
357 _observer.observe(element, { attributes: true, attributeOldValue: true });
358
359 return component;
360};
361
362Component.get = input => internal.factories.find(EqualFilter(input));
363
364const ThinComponent = (factory) => {
365 // console.log(`ThinComponent(${factory.tagName})`);
366
367 // const factory = (component, factory, dependencies) => {
368 //
369 // console.log('ThinComponent()', component);
370 //
371 // return {
372 // render: render.bind(null, component, dependencies)
373 // }
374 // };
375
376 factory.create = decorate(factory.create, (next, component, ...args) => {
377 // let hooks = next(component, ...args);
378
379
380 // if (!isObject(hooks))
381 // hooks = {};
382
383 return {
384 // ...hooks,
385 render: () => next(component, ...args)
386 }
387 });
388
389 return factory;
390};
391
392Component.define = (tagName, factory) => {
393
394 if (isFunction(tagName)) {
395 factory = tagName;
396 tagName = factory.tagName || factory.name;
397 }
398
399 assert(isFunction(factory), `'factory' must be a function`);
400
401 const { dependencies = {} } = factory;
402
403 assert(isString(tagName) && tagName.length > 0, `'tagName' must be a string`);
404
405 tagName = kebabCase(tagName);
406
407 if (!tagName.includes('-'))
408 tagName = `x-${tagName}`;
409
410 assert(!Component.get(factory), `That component factory is already defined`);
411 assert(!internal.factories.find(ByFilter('tagName', tagName)), `'${tagName}' component is already defined`);
412 assert(isObject(dependencies) && !isNull(dependencies), `'dependencies' must be an object`);
413
414 factory.tagName = tagName;
415 factory.allowedHooks = [];
416 if (!isFunction(factory.create))
417 factory.create = factory;
418 if (!isFunction(factory.attach))
419 factory.attach = internal.defaultsHooks.attach;
420 if (!isFunction(factory.initialize))
421 factory.initialize = internal.defaultsHooks.initialize;
422 if (!isFunction(factory.detach))
423 factory.detach = noop;
424 if (!isFunction(factory.render))
425 factory.render = noop;
426
427
428 if (!isUndefined(factory.decorators)) {
429
430 if (!isArray(factory.decorators))
431 factory.decorators = [factory.decorators];
432
433 factory.decorators.forEach(decorator => {
434 assert((isFunction(decorator)), `'decorator' must be a function`);
435 decorator(factory, dependencies);
436 });
437
438 // if (isFunction(factory.define)) {
439 // // factory.define = ThinComponent;
440 // factory = factory.define(factory);
441 //
442 // // if (isObject(factory.properties) && !isNull(factory.properties)) {
443 // // factory.define = ThickComponent;
444 // // }
445 // }
446 }
447 if (!isUndefined(factory.shadow))
448 assert((isPlainObject(factory.shadow)), `'shadow' must be a plain object`);
449
450 assert(isFunction(factory.create), `'create' must be a function`);
451 assert(isFunction(factory.attach), `'attach' must be a function`);
452 assert(isFunction(factory.initialize), `'initialize' must be a function`);
453 assert(isFunction(factory.detach), `'detach' must be a function`);
454 assert(isFunction(factory.render), `'render' must be a function`);
455
456 factory.tagName = tagName;
457 factory.dependencies = dependencies;
458
459 factory.properties = internal.parseProperties(factory.properties);
460
461 factory.allowedHooks = factory.allowedHooks.reduce((hooks, hook) => {
462
463 assert(isString(hook), `'hook' must be a string`);
464
465 if (isString(hook) && !internal.allowedHooks.includes(hook))
466 hooks.push(hook);
467
468 return hooks;
469 }, []).concat(internal.allowedHooks);
470
471 // [
472 // ...factory.allowedHooks,
473 // ...internal.allowedHooks
474 // ]; //.concat(allowedHooks);
475
476 internal.factories.push(factory);
477
478 // console.log(`Component.define(${factory.tagName})`);
479
480 customElements.define(tagName, class extends HTMLElement {
481 constructor() {
482
483 super();
484
485 Component(factory, this, dependencies);
486 }
487 // static get observedAttributes() {
488 //
489 // return _attributesNames;
490 // }
491 connectedCallback() {
492
493 this[$pwet].attach();
494 }
495 disconnectedCallback() {
496
497 this[$pwet].detach();
498 }
499 // attributeChangedCallback(attributeName, oldValue, newValue) {
500 //
501 // const { properties } = this.pwet;
502 //
503 // const { name, parse, isDataAttribute } = _attributes[attributeName];
504 //
505 // console.error(`[${factory.tagName}]`, 'attributeChangedCallback', name, typeof newValue, this.pages);
506 //
507 // properties[name] = parse(newValue);
508 //
509 // this.initialize(properties);
510 //
511 // }
512 });
513};
514
515export {
516 $pwet,
517 ThinComponent,
518 Component as default
519}
\No newline at end of file