diff --git a/task_2_ts/index.html b/task_2_ts/index.html index aab9055..de69812 100644 --- a/task_2_ts/index.html +++ b/task_2_ts/index.html @@ -55,14 +55,16 @@

Selected column infos

-

Filename

-

-

File type

-

-

File size

-

-

Number of rows

-

+

Selected column

+

+

Data type

+

+

Different entries

+

+

Min

+

+

Max

+

@@ -78,7 +80,12 @@

Data table

-
+ + + + + +
diff --git a/task_2_ts/ts/framework.ts b/task_2_ts/ts/framework.ts deleted file mode 100644 index 7b87069..0000000 --- a/task_2_ts/ts/framework.ts +++ /dev/null @@ -1,140 +0,0 @@ -// 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 -// -// It was also a nice exercise to get familiar with Generics in TypeScript, something I haven't -// really used before - -export interface Ref { - 'set': ( data: T ) => void; - 'get': () => T; - 'addConditionalElements': ( elements: HTMLElement[] ) => void; -} - -export interface ListRef { - 'set': ( data: T[] ) => void; - 'get': () => T[]; - 'sort': ( compare: ( a: T, b: T ) => number ) => void; - 'filter': ( predicate: ( value: T ) => boolean ) => void; -} - - -/** - * 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 - */ -const ref = ( elements: HTMLElement[], data: T ): Ref => { - let value: T = data; - let conditionalElements: HTMLElement[] = []; - - // ─────────────────────────────────────────────────────────────────── - const get = (): T => { - return value; - }; - - // ─────────────────────────────────────────────────────────────────── - const set = ( data: T ): void => { - value = data; - - // Update normal ref elements - elements.forEach( el => { - el.innerHTML = String( data ); - } ); - - // Update conditional elements - conditionalElements.forEach( el => { - // convert to boolean (explicitly) - el.hidden = Boolean( data ); - } ); - }; - - /** - * Add elements to be rendered conditionally on this ref. Treats type as booleanish - * @param elements - The elements that are rendered consistently - */ - const addConditionalElements = ( elements: HTMLElement[] ): void => { - conditionalElements = elements; - }; - - return { - set, - get, - addConditionalElements - }; -}; - - -// ─────────────────────────────────────────────────────────────────── -// ╭───────────────────────────────────────────────╮ -// │ List ref, dynamic list rendering │ -// ╰───────────────────────────────────────────────╯ -export type HTMLTagNames = 'div' | 'button' | 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'span'; - -export interface RenderTemplate { - [name: string]: { - 'type': HTMLTagNames; - 'attribute': string; - 'cssClasses': string[]; - } -} - -interface DiffList { - 'modified': T[]; - 'added': T[]; - 'removed': T[]; -} - -const listRef = ( parent: HTMLElement, data: T[] ): ListRef => { - let value: T[] = data; - let rendered: T[] = []; - - // ─────────────────────────────────────────────────────────────────── - const get = (): T[] => { - return value; - }; - - // ─────────────────────────────────────────────────────────────────── - const set = ( data: T[] ): void => { - const diffList: DiffList = { - 'modified': [], - 'added': [], - 'removed': [] - }; - - value = data; - rendered = value; - }; - - const sort = ( compare: ( a: T, b: T ) => number ): void => { - // Re-render based on compare function - }; - - const filter = ( predicate: ( value: T ) => boolean ): void => { - const diffList: DiffList = { - 'modified': [], - 'added': [], - 'removed': [] - }; - }; - - return { - get, - set, - sort, - filter - }; -}; - -// The list renderer using a DiffList -const renderList = ( diffList: DiffList ) => { - -}; - - -export default { - ref, - listRef -}; diff --git a/task_2_ts/ts/main.ts b/task_2_ts/ts/main.ts index 610e813..a2ba7c1 100644 --- a/task_2_ts/ts/main.ts +++ b/task_2_ts/ts/main.ts @@ -1,5 +1,48 @@ import '@fortawesome/fontawesome-free/css/all.css'; import '../css/layout.css'; import '@picocss/pico/css/pico.min.css'; +import { + listRef, ref +} from './rendering/framework'; +import { + CSV_Data +} from './types'; +import { + RenderTemplate +} from './rendering/rendering'; -// TODO start here with the first entry point +const dataList = listRef( + document.getElementById( 'data-table' )!, + [], + 'table-body', + { + 'type': 'tr', + 'cssClasses': [], + 'children': [] + } +); +const headerList = listRef( + document.getElementById( 'data-header' )!, + [], + 'table-header', + { + 'type': 'td', + 'cssClasses': [], + 'children': [] + } +); +const tableRowElement: RenderTemplate = { + 'type': 'td', + 'cssClasses': [], + 'children': [], +}; +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' )! ], '' ); diff --git a/task_2_ts/ts/rendering/framework.ts b/task_2_ts/ts/rendering/framework.ts new file mode 100644 index 0000000..d4156d2 --- /dev/null +++ b/task_2_ts/ts/rendering/framework.ts @@ -0,0 +1,231 @@ +/* + * 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/list-renderer.ts b/task_2_ts/ts/rendering/list-renderer.ts new file mode 100644 index 0000000..659e048 --- /dev/null +++ b/task_2_ts/ts/rendering/list-renderer.ts @@ -0,0 +1,40 @@ +import { + RenderTemplate, + StringIndexedObject +} from './rendering'; + +const renderList = ( + data: T, + template: RenderTemplate, + name: string, id: number +): HTMLElement => { + const parent = renderer( data, template ); + + parent.id = `${ name }--${ id }`; + + return parent; +}; + +const renderer = ( data: T, template: RenderTemplate ): HTMLElement => { + const parent = document.createElement( template.type ); + + for ( let i = 0; i < template.children.length; i++ ) { + const element = template.children[i]!; + + parent.appendChild( renderer( data, element ) ); + } + + if ( template.children.length === 0 ) { + if ( template.attribute ) { + parent.innerText = String( data[ template.attribute ] ); + } else { + parent.innerText = String( data ); + } + } + + return parent; +}; + +export default { + renderList +}; diff --git a/task_2_ts/ts/rendering/rendering.d.ts b/task_2_ts/ts/rendering/rendering.d.ts new file mode 100644 index 0000000..8590b6c --- /dev/null +++ b/task_2_ts/ts/rendering/rendering.d.ts @@ -0,0 +1,46 @@ +export interface Ref { + 'set': ( data: T ) => void; + 'get': () => T; + 'setConditionalElements': ( elements: HTMLElement[] ) => void; + 'addConditionalClasses': ( element: HTMLElement, onTrue: string, onFalse: string ) => void; + 'bind': ( element: HTMLInputElement, castFunction: ( val: string ) => T ) => void; +} + +export interface ListRef { + 'set': ( data: T[] ) => void; + 'get': () => T[]; + 'sort': ( compare: ( a: T, b: T ) => number ) => void; + 'filter': ( predicate: ( value: T ) => boolean ) => void; + 'setTemplate': ( newTemplate: RenderTemplate ) => void; +} + +export type HTMLTagNames = keyof HTMLElementTagNameMap; + +export interface RenderTemplate { + /** + * What kind of element to render. Not all HTML elements supported (couldn't be arsed to do it) + */ + 'type': HTMLTagNames; + + /** + * The attribute of the element to render. Leave blank if type is not object + * Will be ignored if you also set children. If no children or attribute set, + * will simply treat type as string and render accordingly + */ + 'attribute'?: string; + + /** + * Children to render. Can be used to nest + */ + 'children': RenderTemplate[]; + + /** + * CSS classes to append to the element + */ + 'cssClasses': string[]; +} + + +export interface StringIndexedObject { + [key: string]: unknown +}