Last active
January 25, 2019 09:42
-
-
Save mmis1000/26375f6816f68aebadbcc0ea5b7452f4 to your computer and use it in GitHub Desktop.
Typescript to web component experiment
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const camelToKebab = (string: string) => { | |
return string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); | |
}; | |
interface Callback { | |
(this: HTMLElement, newValue: string|null, oldValue: string|null): void | |
} | |
interface Transformer<T> { | |
fromAttribute(attr: string|null): T, | |
toAttribute(value: T): string|null | |
} | |
const defaultTransformer: Transformer<string|null> = { | |
fromAttribute(arg){ return arg }, | |
toAttribute(arg) { return arg } | |
} | |
const callbacks = new WeakMap<HTMLElement, Map<string, Callback[]>>() | |
const propAttrMap = new WeakMap<HTMLElement, Map<string, string>>() | |
const attrPropMap = new WeakMap<HTMLElement, Map<string, string>>() | |
const observed = new WeakMap<HTMLElement, string[]>() | |
function typedAttribute<T>(transformer: Transformer<T>) { | |
return function attribute( | |
/** attribute name of the bound attribute, default to the property name if not specified */ | |
name?: string | |
): PropertyDecorator { | |
return function (target: HTMLElement, propName: string) { | |
const attrName = name || camelToKebab(propName) | |
const observedList = observed.get(target) || [] | |
observed.set(target, observedList) | |
const singlePropAttrKey = propAttrMap.get(target) || new Map<string, string>() | |
propAttrMap.set(target, singlePropAttrKey) | |
const singleAttrPropKey = attrPropMap.get(target) || new Map<string, string>() | |
attrPropMap.set(target, singleAttrPropKey) | |
observedList.push(attrName) | |
singlePropAttrKey.set(propName, attrName) | |
singleAttrPropKey.set(attrName, propName) | |
Object.defineProperty(target, propName, { | |
set(this: HTMLElement, newValue: T) { | |
const transformed = transformer.toAttribute(newValue) | |
if (transformed == null) { | |
this.removeAttribute(attrName) | |
} else { | |
this.setAttribute(attrName, transformed) | |
} | |
}, | |
get(this: HTMLElement): T { | |
return transformer.fromAttribute(this.getAttribute(attrName)) | |
} | |
}) | |
} as any | |
} | |
} | |
function autoListen<T extends {new(...args:any[]): HTMLElement}>(constructor:T) { | |
return class Connected extends constructor { | |
attributeChangedCallback(name: string, oldValue: string|null, newValue: string) { | |
const listenerMap = callbacks.get(constructor.prototype) || new Map<string, Callback[]>() | |
const listeners = listenerMap.get(name) || [] as Callback[] | |
listeners.forEach(cb => { | |
cb.call(this, newValue, oldValue) | |
}); | |
const superMethod = constructor.prototype.attributeChangedCallback; | |
if (superMethod) { | |
superMethod.call(this, name, oldValue, newValue) | |
} | |
} | |
connectedCallback() { | |
const superMethod = constructor.prototype.connectedCallback; | |
if (superMethod) { | |
superMethod.call(this) | |
} | |
const listenerMap = callbacks.get(constructor.prototype) || new Map<string, Callback[]>() | |
const singleAttrPropKey = attrPropMap.get(constructor.prototype) || new Map<string, string>() | |
for (let [attrName, callbacks] of listenerMap) { | |
for (let listener of callbacks) { | |
const propName = singleAttrPropKey.get(attrName)!! | |
console.log(propName) | |
listener.call(this, (this as any)[propName], null) | |
} | |
} | |
} | |
static get observedAttributes() { | |
const observedList = observed.get(constructor.prototype) || [] | |
return observedList.concat( (constructor as any).observedAttributes || []) | |
} | |
} | |
} | |
function listener<T = any>(name: keyof T & string) { | |
return function (target: HTMLElement, propertyKey: string, descriptor: PropertyDescriptor) { | |
const attrName = propAttrMap.get(target)!!.get(name)!! | |
const listenerMap = callbacks.get(target) || new Map<string, Callback[]>() | |
callbacks.set(target, listenerMap) | |
const listeners = listenerMap.get(attrName) || [] as Callback[] | |
listenerMap.set(attrName, listeners) | |
listeners.push(descriptor.value) | |
}; | |
} | |
const string = typedAttribute(defaultTransformer) | |
const number = typedAttribute<number|null>({ | |
fromAttribute(attr: string|null): number|null { | |
if (attr != null) { | |
return parseInt(attr) | |
} else { | |
return null | |
} | |
}, | |
toAttribute(value: number|null): string|null { | |
return value != null? value.toString(): null | |
} | |
}) | |
const date = typedAttribute<Date|null>({ | |
fromAttribute(attr: string|null): Date|null { | |
if (attr != null) { | |
return new Date(attr) | |
} else { | |
return null | |
} | |
}, | |
toAttribute(value: Date|null): string|null { | |
if (value != null) { | |
return value.toUTCString() | |
} else { | |
return null | |
} | |
} | |
}) | |
@autoListen | |
export default class MyElement extends HTMLElement { | |
@string("test-redirect") | |
test: string | undefined | null | |
@string() | |
testVar: string | undefined | null | |
@number() | |
test2: number | undefined | null | |
@date() | |
test3: Date | undefined | null | |
dom?: ShadowRoot | |
timerId?: ReturnType<typeof setTimeout> | |
constructor() { | |
super() | |
} | |
connectedCallback() { | |
// Create a shadow root | |
var shadow = this.attachShadow({mode: 'open'}); | |
var text = document.createElement('span'); | |
text.textContent = "test" | |
text.id = "date" | |
shadow.append(text) | |
var number = document.createElement('span'); | |
number.textContent = "test" | |
number.id = "number" | |
shadow.append(number) | |
this.dom = shadow | |
this.timerId = setInterval(()=>{ | |
if (this.test2 == null) { | |
this.test2 = 0 | |
} | |
this.test2++; | |
}, 1000) | |
} | |
disconnectedCallback() { | |
clearInterval(this.timerId) | |
} | |
@listener<MyElement>("test") | |
onTestChange(newValue: string|null, oldValue:string|null) { | |
console.log("new value", newValue) | |
console.log("old value", oldValue) | |
} | |
@listener<MyElement>("test2") | |
onTest2Change(newValue: number|null, oldValue: number|null) { | |
console.log("new value", newValue) | |
console.log("old value", oldValue) | |
if (this.dom) { | |
const element = this.dom.querySelector("#number") | |
if (element) { | |
element.textContent = newValue + "" | |
} | |
} | |
} | |
@listener<MyElement>("test3") | |
onTest3Change(newValue: Date|null, oldValue:Date|null) { | |
console.log("new value", newValue) | |
console.log("old value", oldValue) | |
if (this.dom) { | |
const element = this.dom.querySelector("#date") | |
if (element) { | |
element.textContent = newValue ? newValue.toLocaleString(): "no date set " | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment