mirror of
https://github.com/janishutz/fundamentals-of-webengineering.git
synced 2025-11-25 05:44:24 +00:00
Start framework refactor
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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' );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -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
|
|
||||||
};
|
|
||||||
30
task_2_ts/ts/rendering/index.ts
Normal file
30
task_2_ts/ts/rendering/index.ts
Normal 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
|
||||||
|
};
|
||||||
124
task_2_ts/ts/rendering/list.ts
Normal file
124
task_2_ts/ts/rendering/list.ts
Normal 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
|
||||||
|
};
|
||||||
|
};
|
||||||
115
task_2_ts/ts/rendering/primitives.ts
Normal file
115
task_2_ts/ts/rendering/primitives.ts
Normal 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
|
||||||
|
};
|
||||||
|
};
|
||||||
2
task_2_ts/ts/rendering/rendering.d.ts
vendored
2
task_2_ts/ts/rendering/rendering.d.ts
vendored
@@ -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;
|
||||||
|
|||||||
5
task_2_ts/ts/types.d.ts
vendored
5
task_2_ts/ts/types.d.ts
vendored
@@ -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[];
|
||||||
|
|||||||
Reference in New Issue
Block a user