From b156ddedb9257a2304e3a3bc8db0e73f8752116b Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Mon, 20 Oct 2025 12:45:06 +0200 Subject: [PATCH] Start framework refactor --- task_2_ts/index.html | 2 +- task_2_ts/ts/main.ts | 69 ++++++-- task_2_ts/ts/rendering/framework.ts | 231 -------------------------- task_2_ts/ts/rendering/index.ts | 30 ++++ task_2_ts/ts/rendering/list.ts | 124 ++++++++++++++ task_2_ts/ts/rendering/primitives.ts | 115 +++++++++++++ task_2_ts/ts/rendering/rendering.d.ts | 2 + task_2_ts/ts/types.d.ts | 5 +- 8 files changed, 335 insertions(+), 243 deletions(-) delete mode 100644 task_2_ts/ts/rendering/framework.ts create mode 100644 task_2_ts/ts/rendering/index.ts create mode 100644 task_2_ts/ts/rendering/list.ts create mode 100644 task_2_ts/ts/rendering/primitives.ts diff --git a/task_2_ts/index.html b/task_2_ts/index.html index de69812..39bf0b3 100644 --- a/task_2_ts/index.html +++ b/task_2_ts/index.html @@ -82,7 +82,7 @@ - +
diff --git a/task_2_ts/ts/main.ts b/task_2_ts/ts/main.ts index a2ba7c1..5628225 100644 --- a/task_2_ts/ts/main.ts +++ b/task_2_ts/ts/main.ts @@ -5,14 +5,17 @@ import { listRef, ref } from './rendering/framework'; import { - CSV_Data + CSVRecord } from './types'; import { RenderTemplate } from './rendering/rendering'; +import { + readCSV +} from './csv'; -const dataList = listRef( - document.getElementById( 'data-table' )!, +const dataList = listRef( + document.getElementById( 'table-body' )!, [], 'table-body', { @@ -22,7 +25,7 @@ const dataList = listRef( } ); const headerList = listRef( - document.getElementById( 'data-header' )!, + document.getElementById( 'table-header' )!, [], 'table-header', { @@ -31,18 +34,64 @@ const headerList = listRef( 'children': [] } ); -const tableRowElement: RenderTemplate = { - 'type': 'td', - 'cssClasses': [], - 'children': [], -}; +const filter = ref( [ document.getElementById( 'filter' )! ], '' ); const filename = ref( [ document.getElementById( 'data-filename' )! ], '' ); const filetype = ref( [ document.getElementById( 'data-filetype' )! ], '' ); const filesize = ref( [ document.getElementById( 'data-filesize' )! ], '' ); const rowCount = ref( [ document.getElementById( 'data-rowcount' )! ], '' ); -const filter = ref( [ document.getElementById( 'filter' )! ], '' ); const columnName = ref( [ document.getElementById( 'column-selected' )! ], '' ); const columnDatatype = ref( [ document.getElementById( 'column-datatype' )! ], '' ); const columnEntries = ref( [ document.getElementById( 'column-entries' )! ], '' ); const columnMax = ref( [ document.getElementById( 'column-max' )! ], '' ); const columnMin = ref( [ document.getElementById( 'column-min' )! ], '' ); +const fileInput = document.getElementById( 'file-input' )! as HTMLInputElement; + + +// Bind to file input event +fileInput.addEventListener( 'change', event => { + loadFile( event ); +} ); + +const loadFile = ( event: Event ) => { + if ( fileInput.files && fileInput.files.length > 0 ) { + const file = fileInput.files[0]!; + + filename.set( file.name ); + filetype.set( file.type ); + filesize.set( String( file.size ) + 'B' ); // TODO: KB / MB conversion stuff + readCSV( event ) + .then( data => { + // Row count + rowCount.set( String( data.length ) ); + + // Header will be the keyset of any row + const header = Object.keys( data[0]! ); + + headerList.set( header ); + + // ── Generate list. Need to first generate the correct template ─── + // Reset, to not trigger expensive rerender + dataList.set( [] ); + dataList.setTemplate( { + 'type': 'tr', + 'cssClasses': [], + 'children': header.map( val => { + return { + 'type': 'td', + 'cssClasses': [], + 'children': [], + 'attribute': val + }; + } ) + } ); + + dataList.set( data ); + } ) + .catch( e => { + console.warn( e ); + alert( 'Failed to read CSV' ); + } ); + } else { + alert( 'No file selected' ); + } +}; diff --git a/task_2_ts/ts/rendering/framework.ts b/task_2_ts/ts/rendering/framework.ts deleted file mode 100644 index d4156d2..0000000 --- a/task_2_ts/ts/rendering/framework.ts +++ /dev/null @@ -1,231 +0,0 @@ -/* - * fundamentals-of-webengineering - framework.ts - * - * Created by Janis Hutz 10/20/2025, Licensed under the GPL V3 License - * https://janishutz.com, development@janishutz.com - * - * -*/ -// Yes, I could not be arsed to keep track of state manually, so wrote a framework real quick that -// does that for me. I am well aware that this is well over engineered, but it was a lot of fun -// and no, this is *NOT* AI generated (I know Claude likes to hallucinate that kinda stuff) -// I will be trying to somewhat follow Vue naming here, as that is what I am familiar with -// (The only thing that is AI generated is the name of the little framework) -// -// It was also a nice exercise to get familiar with Generics in TypeScript, something I haven't -// really used before - -import { - ListRef, Ref, RenderTemplate -} from './rendering'; -import listRenderer from './list-renderer'; - - -/** - * Responsive data (similar behaviour as in Vue.js) - * @template T - The data type you wish to use (as long as you don't want it to be a list) - * @param data - The data stored in this ref - * @param elements - The elements to bind to - */ -export const ref = ( elements: HTMLElement[], data: T ): Ref => { - let value: T = data; - let conditionalElements: HTMLElement[] = []; - - const conditionalClasses: { - 'element': HTMLElement, - 'onTrue': string, - 'onFalse': string - }[] = []; - const boundElements: HTMLInputElement[] = []; - - // ─────────────────────────────────────────────────────────────────── - const get = (): T => { - return value; - }; - - // ─────────────────────────────────────────────────────────────────── - const set = ( data: T ): void => { - value = data; - - // Update normal ref elements - elements.forEach( el => { - el.innerText = String( data ); - } ); - - // Update conditional elements - conditionalElements.forEach( el => { - // convert to boolean (explicitly) - el.hidden = Boolean( data ); - } ); - - conditionalClasses.forEach( el => { - el.element.classList.value = data ? el.onTrue : el.onFalse; - } ); - - // Update boundElements - boundElements.forEach( el => { - el.value = String( value ); - } ); - }; - - /** - * Bind to input change of an HTMLInputElement (two way bind) - * @param element - The element to bind to (i.e. add a two-way bind to) - * @param castFunction - Function used for type casting from string to T - */ - const bind = ( element: HTMLInputElement, castFunction: ( val: string ) => T ) => { - element.addEventListener( 'change', () => { - set( castFunction( element.value ) ); - } ); - boundElements.push( element ); - }; - - /** - * Add elements to be rendered conditionally on this ref. Treats type as booleanish - * @param elements - The elements that are rendered consistently - */ - const setConditionalElements = ( elements: HTMLElement[] ): void => { - conditionalElements = elements; - }; - - /** - * @param element - The element to do the operation on - * @param onTrue - The classes (as strings) to set if true(ish) - * @param onFalse - The classes to set on false(ish) - */ - const addConditionalClasses = ( - element: HTMLElement, - onTrue: string, - onFalse: string - ) => { - conditionalClasses.push( { - 'element': element, - 'onTrue': onTrue, - 'onFalse': onFalse - } ); - }; - - return { - set, - get, - setConditionalElements, - addConditionalClasses, - bind - }; -}; - - -// ─────────────────────────────────────────────────────────────────── -// ╭───────────────────────────────────────────────╮ -// │ List ref, dynamic list rendering │ -// ╰───────────────────────────────────────────────╯ -export const listRef = ( parent: HTMLElement, data: T[], name: string, template: RenderTemplate ): ListRef => { - let list: T[] = data; // contains all values passed in - - const nodes: HTMLElement[] = []; - const rendered: boolean[] = []; // Mask for - - - /** - * @returns All currently rendered elements - */ - const get = (): T[] => { - return list.filter( ( _, index ) => { - return rendered[ index ]; - } ); - }; - - - /** - * Deletes all child nodes and recreates them based on the new data. - * @param data - The new data to set - */ - const set = ( data: T[] ): void => { - // Yes, I know, really bad performance, etc, but it's not needed for any other use case - // here, other than a full replace of the data (no dynamic updates) - list = data; - - // Render the list based on template - for ( let i = 0; i < data.length; i++ ) { - const element = data[i]!; - - // Render list - nodes[ i ] = listRenderer.renderList( - element, template, name, i - ); - rendered[ i ] = true; - } - }; - - const setTemplate = ( newTemplate: RenderTemplate ): void => { - template = newTemplate; - set( data ); - }; - - - /** - * Sort function, a wrapper for native JS's sort on arrays. - * Will be more performant than doing a re-render by sorting and setting, - * as it will sort in-place, instead of regenerating. - * @param compare - The comparison function to use - */ - const sort = ( compare: ( a: T, b: T ) => number ): void => { - // Re-render based on compare function - const children = [ ...parent.children ]; - - children.sort( ( elA, elB ) => { - // Need array index somehow on the element to make comparison easier for consumer - const a = parseInt( elA.id.split( '--' )[1]! ); - const b = parseInt( elB.id.split( '--' )[1]! ); - - // Coaxing the TypeScript compiler into believing this value will exist - return compare( list[a]!, list[b]! ); - } ); - - children.forEach( el => { - parent.appendChild( el ); - } ); - }; - - - /** - * Filter elements. More performant than doing it with set operation, as it is cheaper to reverse. - * It also does not touch the nodes that are going to remain in DOM - * @param predicate - Filtering predicate - */ - const filter = ( predicate: ( value: T ) => boolean ): void => { - let currentIndexInChildrenList = 0; - - const children = [ ...parent.children ]; - - list.forEach( ( val, index ) => { - const evaluation = predicate( val ); - - if ( !evaluation && rendered[ index ] ) { - // can use ! here, as semantics of program tell us that this index will exist - nodes[ index ]!.remove(); - } else if ( evaluation && !rendered[ index ] ) { - currentIndexInChildrenList++; - parent.insertBefore( nodes[ index ]!, children[ currentIndexInChildrenList ]! ); - } else { - currentIndexInChildrenList++; - } - } ); - }; - - set( data ); - - return { - get, - set, - sort, - filter, - setTemplate - }; -}; - - -export default { - ref, - listRef -}; diff --git a/task_2_ts/ts/rendering/index.ts b/task_2_ts/ts/rendering/index.ts new file mode 100644 index 0000000..3fb866c --- /dev/null +++ b/task_2_ts/ts/rendering/index.ts @@ -0,0 +1,30 @@ + +/* + * fundamentals-of-webengineering - framework.ts + * + * Created by Janis Hutz 10/20/2025, Licensed under the GPL V3 License + * https://janishutz.com, development@janishutz.com + * + * +*/ +// Yes, I could not be arsed to keep track of state manually, so wrote a framework real quick that +// does that for me. I am well aware that this is well over engineered, but it was a lot of fun +// and no, this is *NOT* AI generated (I know Claude likes to hallucinate that kinda stuff) +// I will be trying to somewhat follow Vue naming here, as that is what I am familiar with +// (The only thing that is AI generated is the name of the little framework) +// +// It was also a nice exercise to get familiar with Generics in TypeScript, something I haven't +// really used before + +import { + listRef +} from './list'; +import { + ref +} from './primitives'; + + +export default { + ref, + listRef +}; diff --git a/task_2_ts/ts/rendering/list.ts b/task_2_ts/ts/rendering/list.ts new file mode 100644 index 0000000..5f2cbb8 --- /dev/null +++ b/task_2_ts/ts/rendering/list.ts @@ -0,0 +1,124 @@ +import { + ListRef, RenderTemplate +} from './rendering'; +import listRenderer from './list-renderer'; + +export const listRef = ( parent: HTMLElement, data: T[], name: string, template: RenderTemplate ): ListRef => { + if ( parent === null ) throw new Error( 'Parent is null!' ); + + let list: T[] = data; // contains all values passed in + + const nodes: HTMLElement[] = []; + const rendered: boolean[] = []; // Mask for + const onChangeFunctions: ( () => Promise )[] = []; + + /** + * @returns All currently rendered elements + */ + const get = (): T[] => { + return list.filter( ( _, index ) => { + return rendered[ index ]; + } ); + }; + + + /** + * Deletes all child nodes and recreates them based on the new data. + * @param data - The new data to set + */ + const set = ( data: T[] ): void => { + // Yes, I know, really bad performance, etc, but it's not needed for any other use case + // here, other than a full replace of the data (no dynamic updates) + list = data; + console.log( data ); + + // Render the list based on template + for ( let i = 0; i < data.length; i++ ) { + const element = data[i]!; + + // Render list + nodes[ i ] = listRenderer.renderList( + element, template, name, i + ); + rendered[ i ] = true; + parent.appendChild( nodes[ i ]! ); + } + }; + + const setTemplate = ( newTemplate: RenderTemplate ): void => { + template = newTemplate; + set( data ); + }; + + + /** + * Sort function, a wrapper for native JS's sort on arrays. + * Will be more performant than doing a re-render by sorting and setting, + * as it will sort in-place, instead of regenerating. + * @param compare - The comparison function to use + */ + const sort = ( compare: ( a: T, b: T ) => number ): void => { + // Re-render based on compare function + const children = [ ...parent.children ]; + + children.sort( ( elA, elB ) => { + // Need array index somehow on the element to make comparison easier for consumer + const a = parseInt( elA.id.split( '--' )[1]! ); + const b = parseInt( elB.id.split( '--' )[1]! ); + + // Coaxing the TypeScript compiler into believing this value will exist + return compare( list[a]!, list[b]! ); + } ); + + children.forEach( el => { + parent.appendChild( el ); + } ); + }; + + + /** + * Filter elements. More performant than doing it with set operation, as it is cheaper to reverse. + * It also does not touch the nodes that are going to remain in DOM + * @param predicate - Filtering predicate + */ + const filter = ( predicate: ( value: T ) => boolean ): void => { + let currentIndexInChildrenList = 0; + + const children = [ ...parent.children ]; + + list.forEach( ( val, index ) => { + const evaluation = predicate( val ); + + if ( !evaluation && rendered[ index ] ) { + // can use ! here, as semantics of program tell us that this index will exist + nodes[ index ]!.remove(); + } else if ( evaluation && !rendered[ index ] ) { + currentIndexInChildrenList++; + parent.insertBefore( nodes[ index ]!, children[ currentIndexInChildrenList ]! ); + } else { + currentIndexInChildrenList++; + } + } ); + }; + + /** + * Connect to change event + * @param callback - The callback that is executed each time the value is updated + */ + const onChange = ( callback: () => void ) => { + const asyncWrapper = async () => callback(); + + onChangeFunctions.push( asyncWrapper ); + }; + + set( data ); + + return { + get, + set, + sort, + filter, + setTemplate, + onChange + }; +}; diff --git a/task_2_ts/ts/rendering/primitives.ts b/task_2_ts/ts/rendering/primitives.ts new file mode 100644 index 0000000..e02e5d2 --- /dev/null +++ b/task_2_ts/ts/rendering/primitives.ts @@ -0,0 +1,115 @@ +import { + Ref +} from './rendering'; + +/** + * Responsive data (similar behaviour as in Vue.js) + * @template T - The data type you wish to use (as long as you don't want it to be a list) + * @param data - The data stored in this ref + * @param elements - The elements to bind to + */ +export const ref = ( elements: HTMLElement[], data: T ): Ref => { + let value: T = data; + let conditionalElements: HTMLElement[] = []; + + const onChangeFunctions: ( () => Promise )[] = []; + const conditionalClasses: { + 'element': HTMLElement, + 'onTrue': string, + 'onFalse': string + }[] = []; + const boundElements: HTMLInputElement[] = []; + + // ─────────────────────────────────────────────────────────────────── + const get = (): T => { + return value; + }; + + // ─────────────────────────────────────────────────────────────────── + const set = ( data: T ): void => { + value = data; + + // Update normal ref elements + elements.forEach( el => { + el.innerText = String( data ); + } ); + + // Update conditional elements + conditionalElements.forEach( el => { + // convert to boolean (explicitly) + el.hidden = Boolean( data ); + } ); + + conditionalClasses.forEach( el => { + el.element.classList.value = data ? el.onTrue : el.onFalse; + } ); + + // Update boundElements + boundElements.forEach( el => { + el.value = String( value ); + } ); + + for ( let i = 0; i < onChangeFunctions.length; i++ ) { + onChangeFunctions[ i ]!(); + } + }; + + /** + * Bind to input change of an HTMLInputElement (two way bind) + * @param element - The element to bind to (i.e. add a two-way bind to) + * @param castFunction - Function used for type casting from string to T + */ + const bind = ( element: HTMLInputElement, castFunction: ( val: string ) => T ) => { + element.addEventListener( 'change', () => { + set( castFunction( element.value ) ); + } ); + boundElements.push( element ); + }; + + /** + * Add elements to be rendered conditionally on this ref. Treats type as booleanish + * @param elements - The elements that are rendered consistently + */ + const setConditionalElements = ( elements: HTMLElement[] ): void => { + conditionalElements = elements; + }; + + /** + * @param element - The element to do the operation on + * @param onTrue - The classes (as strings) to set if true(ish) + * @param onFalse - The classes to set on false(ish) + */ + const addConditionalClasses = ( + element: HTMLElement, + onTrue: string, + onFalse: string + ) => { + conditionalClasses.push( { + 'element': element, + 'onTrue': onTrue, + 'onFalse': onFalse + } ); + }; + + + /** + * Connect to change event + * @param callback - The callback that is executed each time the value is updated + */ + const onChange = ( callback: () => void ) => { + const asyncWrapper = async () => callback(); + + onChangeFunctions.push( asyncWrapper ); + }; + + set( data ); + + return { + set, + get, + setConditionalElements, + addConditionalClasses, + bind, + onChange + }; +}; diff --git a/task_2_ts/ts/rendering/rendering.d.ts b/task_2_ts/ts/rendering/rendering.d.ts index 8590b6c..c6fe86f 100644 --- a/task_2_ts/ts/rendering/rendering.d.ts +++ b/task_2_ts/ts/rendering/rendering.d.ts @@ -4,6 +4,7 @@ export interface Ref { 'setConditionalElements': ( elements: HTMLElement[] ) => void; 'addConditionalClasses': ( element: HTMLElement, onTrue: string, onFalse: string ) => void; 'bind': ( element: HTMLInputElement, castFunction: ( val: string ) => T ) => void; + 'onChange': ( callback: () => void ) => void; } export interface ListRef { @@ -12,6 +13,7 @@ export interface ListRef { 'sort': ( compare: ( a: T, b: T ) => number ) => void; 'filter': ( predicate: ( value: T ) => boolean ) => void; 'setTemplate': ( newTemplate: RenderTemplate ) => void; + 'onChange': ( callback: () => void ) => void; } export type HTMLTagNames = keyof HTMLElementTagNameMap; diff --git a/task_2_ts/ts/types.d.ts b/task_2_ts/ts/types.d.ts index d7160e7..407cefc 100644 --- a/task_2_ts/ts/types.d.ts +++ b/task_2_ts/ts/types.d.ts @@ -1 +1,4 @@ -export type CSV_Data = Array>; +// Array<> is unnecessary, simply use below +export type CSVRecord = Record; + +export type CSV_Data = CSVRecord[];