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[];