Added bind_children_to function to efficiently update child nodes of an HTML element based on the contents of a ListProperty.

This commit is contained in:
Daan Vanden Bosch 2019-09-17 17:26:25 +02:00
parent 859d85da45
commit 8622b07bde
3 changed files with 118 additions and 106 deletions

View File

@ -1,13 +1,10 @@
import { Widget, WidgetOptions } from "./Widget";
import { el } from "./dom";
import {
ListChangeType,
ListProperty,
ListPropertyChangeEvent,
} from "../observable/property/list/ListProperty";
import { bind_children_to, el } from "./dom";
import { ListProperty } from "../observable/property/list/ListProperty";
import { Disposer } from "../observable/Disposer";
import "./Table.css";
import Logger = require("js-logger");
import { Disposable } from "../observable/Disposable";
const logger = Logger.get("core/gui/Table");
@ -41,7 +38,6 @@ export type TableOptions<T> = WidgetOptions & {
export class Table<T> extends Widget {
readonly element = el.table({ class: "core_Table" });
private readonly table_disposer = this.disposable(new Disposer());
private readonly tbody_element = el.tbody();
private readonly footer_row_element?: HTMLTableRowElement;
private readonly values: ListProperty<T>;
@ -138,77 +134,54 @@ export class Table<T> extends Widget {
this.create_footer();
}
this.disposables(this.values.observe_list(this.update_table));
this.splice_rows(0, this.values.length.val, this.values.val);
this.disposables(
bind_children_to(this.tbody_element, this.values, this.create_row),
this.values.observe(this.update_footer),
);
this.finalize_construction(Table.prototype);
}
private update_table = (change: ListPropertyChangeEvent<T>): void => {
if (change.type === ListChangeType.ListChange) {
this.splice_rows(change.index, change.removed.length, change.inserted);
this.update_footer();
} else if (change.type === ListChangeType.ValueChange) {
// TODO: update rows
}
};
private splice_rows = (index: number, amount: number, inserted: T[]) => {
for (let i = 0; i < amount; i++) {
this.tbody_element.children[index].remove();
}
this.table_disposer.dispose_at(index, amount);
const rows = inserted.map((value, i) => this.create_row(index + i, value));
if (index >= this.tbody_element.childElementCount) {
this.tbody_element.append(...rows);
} else {
for (let i = 0; i < amount; i++) {
this.tbody_element.children[index + i].insertAdjacentElement(
"beforebegin",
rows[i],
);
}
}
};
private create_row = (index: number, value: T): HTMLTableRowElement => {
const disposer = this.table_disposer.add(new Disposer());
private create_row = (value: T, index: number): [HTMLTableRowElement, Disposable] => {
const disposer = new Disposer();
let left = 0;
return el.tr(
{},
...this.columns.map((column, i) => {
const cell = column.fixed ? el.th() : el.td();
return [
el.tr(
{},
...this.columns.map((column, i) => {
const cell = column.fixed ? el.th() : el.td();
try {
const content = column.render_cell(value, disposer);
try {
const content = column.render_cell(value, disposer);
cell.append(content);
cell.append(content);
if (column.input) cell.classList.add("input");
if (column.input) cell.classList.add("input");
if (column.fixed) {
cell.classList.add("fixed");
cell.style.left = `${left}px`;
left += column.width || 0;
if (column.fixed) {
cell.classList.add("fixed");
cell.style.left = `${left}px`;
left += column.width || 0;
}
cell.style.width = `${column.width}px`;
if (column.text_align) cell.style.textAlign = column.text_align;
if (column.tooltip) cell.title = column.tooltip(value);
} catch (e) {
logger.warn(
`Error while rendering cell for index ${index}, column ${i}.`,
e,
);
}
cell.style.width = `${column.width}px`;
if (column.text_align) cell.style.textAlign = column.text_align;
if (column.tooltip) cell.title = column.tooltip(value);
} catch (e) {
logger.warn(`Error while rendering cell for index ${index}, column ${i}.`, e);
}
return cell;
}),
);
return cell;
}),
),
disposer,
];
};
private create_footer(): void {
@ -240,7 +213,7 @@ export class Table<T> extends Widget {
this.footer_row_element!.append(...footer_cells);
}
private update_footer(): void {
private update_footer = (): void => {
if (!this.footer_row_element) return;
const col_count = this.columns.length;
@ -254,5 +227,5 @@ export class Table<T> extends Widget {
cell.title = column.footer.tooltip ? column.footer.tooltip() : "";
}
}
}
};
}

View File

@ -2,6 +2,12 @@ import { Disposable } from "../observable/Disposable";
import { Observable } from "../observable/Observable";
import { is_property } from "../observable/property/Property";
import { SectionId } from "../model";
import {
ListChangeType,
ListProperty,
ListPropertyChangeEvent,
} from "../observable/property/list/ListProperty";
import { Disposer } from "../observable/Disposer";
type ElementAttributes = {
class?: string;
@ -188,3 +194,56 @@ export function disposable_listener(
},
};
}
export function bind_children_to<T>(
element: HTMLElement,
list: ListProperty<T>,
create_child: (value: T, index: number) => HTMLElement | [HTMLElement, Disposable],
): Disposable {
const children_disposer = new Disposer();
const observer = list.observe_list((change: ListPropertyChangeEvent<T>) => {
if (change.type === ListChangeType.ListChange) {
splice_children(change.index, change.removed.length, change.inserted);
} else if (change.type === ListChangeType.ValueChange) {
// TODO: update children
}
});
function splice_children(index: number, removed_count: number, inserted: T[]): void {
for (let i = 0; i < removed_count; i++) {
element.children[index].remove();
}
children_disposer.dispose_at(index, removed_count);
const children = inserted.map((value, i) => {
const child = create_child(value, index + i);
if (Array.isArray(child)) {
children_disposer.insert(index + i, child[1]);
return child[0];
} else {
return child;
}
});
if (index >= element.childElementCount) {
element.append(...children);
} else {
for (let i = 0; i < removed_count; i++) {
element.children[index + i].insertAdjacentElement("beforebegin", children[i]);
}
}
}
splice_children(0, 0, list.val);
return {
dispose(): void {
observer.dispose();
children_disposer.dispose();
element.innerHTML = "";
},
};
}

View File

@ -1,24 +1,20 @@
import { el, Icon } from "../../core/gui/dom";
import { bind_children_to, el, Icon } from "../../core/gui/dom";
import "./WantedItemsView.css";
import { Button } from "../../core/gui/Button";
import { Disposer } from "../../core/observable/Disposer";
import { Widget } from "../../core/gui/Widget";
import {
ListChangeType,
ListPropertyChangeEvent,
} from "../../core/observable/property/list/ListProperty";
import { WantedItemModel } from "../model";
import { NumberInput } from "../../core/gui/NumberInput";
import { hunt_optimizer_stores } from "../stores/HuntOptimizerStore";
import { ComboBox } from "../../core/gui/ComboBox";
import { list_property } from "../../core/observable";
import { ItemType } from "../../core/model/items";
import { Disposable } from "../../core/observable/Disposable";
export class WantedItemsView extends Widget {
readonly element = el.div({ class: "hunt_optimizer_WantedItemsView" });
private readonly tbody_element = el.tbody();
private readonly table_disposer = this.disposable(new Disposer());
private readonly store_disposer = this.disposable(new Disposer());
constructor() {
@ -56,7 +52,11 @@ export class WantedItemsView extends Widget {
this.store_disposer.dispose_all();
this.store_disposer.add_all(
hunt_optimizer_store.wanted_items.observe_list(this.update_table),
bind_children_to(
this.tbody_element,
hunt_optimizer_store.wanted_items,
this.create_row,
),
combo_box.selected.observe(({ value: item_type }) => {
if (item_type) {
@ -78,31 +78,8 @@ export class WantedItemsView extends Widget {
this.finalize_construction(WantedItemsView.prototype);
}
private update_table = (change: ListPropertyChangeEvent<WantedItemModel>): void => {
if (change.type === ListChangeType.ListChange) {
for (let i = 0; i < change.removed.length; i++) {
this.tbody_element.children[change.index].remove();
}
this.table_disposer.dispose_at(change.index, change.removed.length);
const rows = change.inserted.map(this.create_row);
if (change.index >= this.tbody_element.childElementCount) {
this.tbody_element.append(...rows);
} else {
for (let i = 0; i < change.inserted.length; i++) {
this.tbody_element.children[change.index + i].insertAdjacentElement(
"beforebegin",
rows[i],
);
}
}
}
};
private create_row = (wanted_item: WantedItemModel): HTMLTableRowElement => {
const row_disposer = this.table_disposer.add(new Disposer());
private create_row = (wanted_item: WantedItemModel): [HTMLTableRowElement, Disposable] => {
const row_disposer = new Disposer();
const amount_input = row_disposer.add(
new NumberInput(wanted_item.amount.val, { min: 0, step: 1 }),
@ -121,11 +98,14 @@ export class WantedItemsView extends Widget {
),
);
return el.tr(
{},
el.td({}, amount_input.element),
el.td({ text: wanted_item.item_type.name }),
el.td({}, remove_button.element),
);
return [
el.tr(
{},
el.td({}, amount_input.element),
el.td({ text: wanted_item.item_type.name }),
el.td({}, remove_button.element),
),
row_disposer,
];
};
}