-
Notifications
You must be signed in to change notification settings - Fork 406
/
base-skate-element.js
309 lines (269 loc) · 7.6 KB
/
base-skate-element.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
import { dashCase, empty, keys } from 'skatejs/dist/esnext/util';
var _extends =
Object.assign ||
function(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
export function normalizeAttributeDefinition(name, prop) {
const { attribute } = prop;
const obj =
typeof attribute === 'object'
? _extends({}, attribute)
: {
source: attribute,
target: attribute,
};
if (obj.source === true) {
obj.source = dashCase(name);
}
if (obj.target === true) {
obj.target = dashCase(name);
}
return obj;
}
function identity(v) {
return v;
}
export function normalizePropertyDefinition(name, prop) {
const { coerce, default: def, deserialize, serialize } = prop;
return {
attribute: normalizeAttributeDefinition(name, prop),
coerce: coerce || identity,
default: def,
deserialize: deserialize || identity,
serialize: serialize || identity,
};
}
const defaultTypesMap = new Map();
function defineProps(constructor) {
if (constructor.hasOwnProperty('_propsNormalized')) return;
const { props } = constructor;
keys(props).forEach(name => {
let func = props[name] || props.any;
if (defaultTypesMap.has(func)) func = defaultTypesMap.get(func);
if (typeof func !== 'function') func = prop(func);
func({ constructor }, name);
});
}
function delay(fn) {
if (window.Promise) {
Promise.resolve().then(fn);
} else {
setTimeout(fn);
}
}
export function prop(definition) {
const propertyDefinition = definition || {};
// Allows decorators, or imperative definitions.
const func = function({ constructor }, name) {
const normalized = normalizePropertyDefinition(name, propertyDefinition);
// Ensure that we can cache properties. We have to do this so the _props object literal doesn't modify parent
// classes or share the instance anywhere where it's not intended to be shared explicitly in userland code.
if (!constructor.hasOwnProperty('_propsNormalized')) {
constructor._propsNormalized = {};
}
// Cache the value so we can reference when syncing the attribute to the property.
constructor._propsNormalized[name] = normalized;
const {
attribute: { source, target },
} = normalized;
if (source) {
constructor._observedAttributes.push(source);
constructor._attributeToPropertyMap[source] = name;
if (source !== target) {
constructor._attributeToAttributeMap[source] = target;
}
}
Object.defineProperty(constructor.prototype, name, {
configurable: true,
get() {
const val = this._props[name];
return val == null ? normalized.default : val;
},
set(val) {
const {
attribute: { target },
serialize,
} = normalized;
if (target) {
const serializedVal = serialize ? serialize(val) : val;
if (serializedVal == null) {
this.removeAttribute(target);
} else {
this.setAttribute(target, serializedVal);
}
}
this._props[name] = normalized.coerce(val);
this.triggerUpdate();
},
});
};
// Allows easy extension of pre-defined props { ...prop(), ...{} }.
Object.keys(propertyDefinition).forEach(
key => (func[key] = propertyDefinition[key])
);
return func;
}
export class SkateElement extends HTMLElement {
constructor(...args) {
var _temp;
return (
(_temp = super(...args)),
(this._prevProps = {}),
(this._prevState = {}),
(this._props = {}),
(this._state = {}),
_temp
);
}
static get observedAttributes() {
// We have to define props here because observedAttributes are retrieved
// only once when the custom element is defined. If we did this only in
// the constructor, then props would not link to attributes.
defineProps(this);
return this._observedAttributes.concat(super.observedAttributes || []);
}
static get props() {
return this._props;
}
static set props(props) {
this._props = props;
}
get props() {
return keys(this.constructor.props).reduce((prev, curr) => {
prev[curr] = this[curr];
return prev;
}, {});
}
set props(props) {
const ctorProps = this.constructor.props;
keys(props).forEach(k => k in ctorProps && (this[k] = props[k]));
}
get state() {
return this._state;
}
set state(state) {
this._state = state;
this.triggerUpdate();
}
attributeChangedCallback(name, oldValue, newValue) {
const {
_attributeToAttributeMap,
_attributeToPropertyMap,
_propsNormalized,
} = this.constructor;
if (super.attributeChangedCallback) {
super.attributeChangedCallback(name, oldValue, newValue);
}
const propertyName = _attributeToPropertyMap[name];
if (propertyName) {
const propertyDefinition = _propsNormalized[propertyName];
if (propertyDefinition) {
const { default: defaultValue, deserialize } = propertyDefinition;
const propertyValue = deserialize ? deserialize(newValue) : newValue;
this._props[propertyName] =
propertyValue == null ? defaultValue : propertyValue;
this.triggerUpdate();
}
}
const targetAttributeName = _attributeToAttributeMap[name];
if (targetAttributeName) {
if (newValue == null) {
this.removeAttribute(targetAttributeName);
} else {
this.setAttribute(targetAttributeName, newValue);
}
}
}
connectedCallback() {
if (super.connectedCallback) {
super.connectedCallback();
}
this.triggerUpdate();
}
shouldUpdate() {
return true;
}
triggerUpdate() {
if (this._updating) {
return;
}
this._updating = true;
delay(() => {
const { _prevProps, _prevState } = this;
if (this.updating) {
this.updating(_prevProps, _prevState);
}
if (this.updated && this.shouldUpdate(_prevProps, _prevState)) {
this.updated(_prevProps, _prevState);
}
this._prevProps = this.props;
this._prevState = this.state;
this._updating = false;
});
}
}
SkateElement._attributeToAttributeMap = {};
SkateElement._attributeToPropertyMap = {};
SkateElement._observedAttributes = [];
SkateElement._props = {};
const { parse, stringify } = JSON;
const attribute = Object.freeze({ source: true });
const zeroOrNumber = val => (empty(val) ? 0 : Number(val));
const any = prop({
attribute,
});
const array = prop({
attribute,
coerce: val => (Array.isArray(val) ? val : empty(val) ? null : [val]),
default: Object.freeze([]),
deserialize: parse,
serialize: stringify,
});
const boolean = prop({
attribute,
coerce: Boolean,
default: false,
deserialize: val => !empty(val),
serialize: val => (val ? '' : null),
});
const number = prop({
attribute,
default: 0,
coerce: zeroOrNumber,
deserialize: zeroOrNumber,
serialize: val => (empty(val) ? null : String(Number(val))),
});
const object = prop({
attribute,
default: Object.freeze({}),
deserialize: parse,
serialize: stringify,
});
const string = prop({
attribute,
default: '',
coerce: String,
serialize: val => (empty(val) ? null : String(val)),
});
defaultTypesMap.set(Array, array);
defaultTypesMap.set(Boolean, boolean);
defaultTypesMap.set(Number, number);
defaultTypesMap.set(Object, object);
defaultTypesMap.set(String, string);
export const props = {
any,
array,
boolean,
number,
object,
string,
};