Start framework refactor

This commit is contained in:
2025-10-20 12:45:06 +02:00
parent d35a47e4b0
commit b156ddedb9
8 changed files with 335 additions and 243 deletions

View File

@@ -82,7 +82,7 @@
</header> </header>
<table id="table-content"> <table id="table-content">
<thead id="table-header"></thead> <thead id="table-header"></thead>
<tbody id="data-table"> <tbody id="table-body">
</tbody> </tbody>
</table> </table>

View File

@@ -5,14 +5,17 @@ import {
listRef, ref listRef, ref
} from './rendering/framework'; } from './rendering/framework';
import { import {
CSV_Data CSVRecord
} from './types'; } from './types';
import { import {
RenderTemplate RenderTemplate
} from './rendering/rendering'; } from './rendering/rendering';
import {
readCSV
} from './csv';
const dataList = listRef<CSV_Data>( const dataList = listRef<CSVRecord>(
document.getElementById( 'data-table' )!, document.getElementById( 'table-body' )!,
[], [],
'table-body', 'table-body',
{ {
@@ -22,7 +25,7 @@ const dataList = listRef<CSV_Data>(
} }
); );
const headerList = listRef<string>( const headerList = listRef<string>(
document.getElementById( 'data-header' )!, document.getElementById( 'table-header' )!,
[], [],
'table-header', 'table-header',
{ {
@@ -31,18 +34,64 @@ const headerList = listRef<string>(
'children': [] 'children': []
} }
); );
const tableRowElement: RenderTemplate = { const filter = ref<string>( [ document.getElementById( 'filter' )! ], '' );
'type': 'td',
'cssClasses': [],
'children': [],
};
const filename = ref<string>( [ document.getElementById( 'data-filename' )! ], '' ); const filename = ref<string>( [ document.getElementById( 'data-filename' )! ], '' );
const filetype = ref<string>( [ document.getElementById( 'data-filetype' )! ], '' ); const filetype = ref<string>( [ document.getElementById( 'data-filetype' )! ], '' );
const filesize = ref<string>( [ document.getElementById( 'data-filesize' )! ], '' ); const filesize = ref<string>( [ document.getElementById( 'data-filesize' )! ], '' );
const rowCount = ref<string>( [ document.getElementById( 'data-rowcount' )! ], '' ); const rowCount = ref<string>( [ document.getElementById( 'data-rowcount' )! ], '' );
const filter = ref<string>( [ document.getElementById( 'filter' )! ], '' );
const columnName = ref<string>( [ document.getElementById( 'column-selected' )! ], '' ); const columnName = ref<string>( [ document.getElementById( 'column-selected' )! ], '' );
const columnDatatype = ref<string>( [ document.getElementById( 'column-datatype' )! ], '' ); const columnDatatype = ref<string>( [ document.getElementById( 'column-datatype' )! ], '' );
const columnEntries = ref<string>( [ document.getElementById( 'column-entries' )! ], '' ); const columnEntries = ref<string>( [ document.getElementById( 'column-entries' )! ], '' );
const columnMax = ref<string>( [ document.getElementById( 'column-max' )! ], '' ); const columnMax = ref<string>( [ document.getElementById( 'column-max' )! ], '' );
const columnMin = ref<string>( [ document.getElementById( 'column-min' )! ], '' ); const columnMin = ref<string>( [ 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' );
}
};

View File

@@ -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 = <T>( elements: HTMLElement[], data: T ): Ref<T> => {
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 = <T>( parent: HTMLElement, data: T[], name: string, template: RenderTemplate ): ListRef<T> => {
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
};

View File

@@ -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
};

View File

@@ -0,0 +1,124 @@
import {
ListRef, RenderTemplate
} from './rendering';
import listRenderer from './list-renderer';
export const listRef = <T>( parent: HTMLElement, data: T[], name: string, template: RenderTemplate ): ListRef<T> => {
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<void> )[] = [];
/**
* @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
};
};

View File

@@ -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 = <T>( elements: HTMLElement[], data: T ): Ref<T> => {
let value: T = data;
let conditionalElements: HTMLElement[] = [];
const onChangeFunctions: ( () => Promise<void> )[] = [];
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
};
};

View File

@@ -4,6 +4,7 @@ export interface Ref<T> {
'setConditionalElements': ( elements: HTMLElement[] ) => void; 'setConditionalElements': ( elements: HTMLElement[] ) => void;
'addConditionalClasses': ( element: HTMLElement, onTrue: string, onFalse: string ) => void; 'addConditionalClasses': ( element: HTMLElement, onTrue: string, onFalse: string ) => void;
'bind': ( element: HTMLInputElement, castFunction: ( val: string ) => T ) => void; 'bind': ( element: HTMLInputElement, castFunction: ( val: string ) => T ) => void;
'onChange': ( callback: () => void ) => void;
} }
export interface ListRef<T> { export interface ListRef<T> {
@@ -12,6 +13,7 @@ export interface ListRef<T> {
'sort': ( compare: ( a: T, b: T ) => number ) => void; 'sort': ( compare: ( a: T, b: T ) => number ) => void;
'filter': ( predicate: ( value: T ) => boolean ) => void; 'filter': ( predicate: ( value: T ) => boolean ) => void;
'setTemplate': ( newTemplate: RenderTemplate ) => void; 'setTemplate': ( newTemplate: RenderTemplate ) => void;
'onChange': ( callback: () => void ) => void;
} }
export type HTMLTagNames = keyof HTMLElementTagNameMap; export type HTMLTagNames = keyof HTMLElementTagNameMap;

View File

@@ -1 +1,4 @@
export type CSV_Data = Array<Record<string, unknown>>; // Array<> is unnecessary, simply use below
export type CSVRecord = Record<string, unknown>;
export type CSV_Data = CSVRecord[];