class Filpper { duration: 1000; templateAsStr: string; tokens: string[]; templateData = { curSize: '', curVal: '0', nextSize: '', nextVal: '0' }; constructor( private el: HTMLElement, private template: HTMLTemplateElement, private startVal = 0, private startDir: 'up'|'down' = 'up' ) { this.templateAsStr = template.innerHTML as string; this.tokens = getSubstitutionTokens(this.templateAsStr); this.templateData.curVal = startVal.toString(); this.setDirection(startDir); this.update(startVal); } update(newVal: number|string): void { this.templateData.nextVal = newVal.toString(); this.setSizes(); this.runAnimation(); } setDirection(direction: 'up'|'down'): void { if (direction.toLowerCase() === 'up') { this.el.classList.add('up'); this.el.classList.remove('down'); } else { this.el.classList.add('down'); this.el.classList.remove('up'); } } setSizes(): void { const td = this.templateData; td.curSize = this.getSize(td.curVal); td.nextSize = this.getSize(td.nextVal); } getSize(val: number|string): string { return val.toString().length > 1 ? 'small' : ''; } runAnimation(): void { const inner = processTemplateSubstitutions(this.templateAsStr, this.tokens, this.templateData); this.el.innerHTML = inner; window.requestAnimationFrame(() => { this.el.classList.remove('changed'); window.requestAnimationFrame(() => this.el.classList.add('changing')); }); setTimeout(() => { this.el.classList.add('changed'); this.el.classList.remove('changing'); this.templateData.curVal = this.templateData.nextVal; }, this.duration * 0.9); } }; // -------------------------------------------------------------------------------------------------------------------- // Teplating Helpers const processTemplateSubstitutions = (template: string, tokens: string[], data: { [key: string]: any }): string => { const processedTemplate = tokens.reduce( (str, token) => str.replace(new RegExp(`{{\\s${token}\\s+}}`, 'gm'), path(data, token)), template ); return processedTemplate; } const getSubstitutionTokens = (template: string) => { let resArr = []; let tempArr; const regex = new RegExp('{{\\s+(.*?)\\s+}}', 'g'); while ((tempArr = regex.exec(template)) !== null) { resArr.push(tempArr[1]); } return Array.from(new Set(resArr)); } const path = (obj: { [key: string]: any }, path: string, defaultval = '') => { // Safely get nested properties without triggering type errors // Usage: fetch(obj, 'a.b.c.d') // Returns d if d exists, else defaultval or an empty string if (typeof defaultval === 'undefined') defaultval = null; if (typeof obj !== 'object') return defaultval; if (typeof path !== 'string') return defaultval; var props = path.split('.'); var test_first = (obj: { [key: string]: any }, props: string[]): any => { if (props.length) { var first = props[0]; if (obj.hasOwnProperty(first)) { var rest = props.slice(1); return test_first(obj[first], rest); // Recursive chomp } else { return defaultval; // The default value, or null } } else { return obj; // The chomped object's last primitive property if it exists } } return test_first(obj, props); } // -------------------------------------------------------------------------------------------------------------------- // Run the flipper var card = document.querySelector('.flipper-card'); var template = document.querySelector('#flipper-card-template'); var c = new (Filpper as any)(card, template); c.update(1); setTimeout(() => c.update(2), 1000); setTimeout(() => c.update(3), 2000); setTimeout(() => c.update(4), 3000); setTimeout(() => c.update(5), 4000); setTimeout(() => c.update('A'), 5000); setTimeout(() => c.update('B'), 6000); setTimeout(() => c.update('C'), 7000); setTimeout(() => c.update('D'), 8000); setTimeout(() => c.update('E'), 9000);