node: added MapModel (#3946)

---------

Co-authored-by: Simon Hausmann <simon.hausmann@slint.dev>
This commit is contained in:
Florian Blasius 2023-11-23 12:20:06 +01:00 committed by GitHub
parent b903c60a3a
commit b19cbba7ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 225 additions and 9 deletions

View file

@ -2,7 +2,6 @@
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
import test from 'ava';
import * as path from 'node:path';
import { private_api } from '../index.js'

View file

@ -6,7 +6,7 @@ import * as path from 'node:path';
import { fileURLToPath } from 'url';
import Jimp = require("jimp");
import { private_api, ImageData, ArrayModel } from '../index.js'
import { private_api, ImageData, ArrayModel, Model, MapModel } from '../index.js'
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
@ -443,6 +443,45 @@ test('ArrayModel', (t) => {
t.deepEqual(structArrayModel.values(), new ArrayModel([{ "name": "simon", "age": 22 }, { "name": "florian", "age": 22 }]).values());
})
test("MapModel", (t) => {
let compiler = new private_api.ComponentCompiler();
let definition = compiler.buildFromSource(`
export component App {
in-out property <[string]> model;
}`, "");
t.not(definition, null);
let instance = definition!.create();
t.not(instance, null);
interface Name {
first: string;
last: string;
}
const nameModel: ArrayModel<Name> = new ArrayModel([
{ first: "Hans", last: "Emil" },
{ first: "Max", last: "Mustermann" },
{ first: "Roman", last: "Tisch" },
]);
const mapModel = new MapModel(
nameModel,
(data) => {
return data.last + ", " + data.first;
}
);
instance!.setProperty("model", mapModel);
nameModel.setRowData(1, { first: "Simon", last: "Hausmann" });
const checkModel = instance!.getProperty("model") as Model<string>;
t.is(checkModel.rowData(0), "Emil, Hans");
t.is(checkModel.rowData(1), "Hausmann, Simon");
t.is(checkModel.rowData(2), "Tisch, Roman");
})
test('ArrayModel rowCount', (t) => {
let compiler = new private_api.ComponentCompiler;
let definition = compiler.buildFromSource(`

View file

@ -118,6 +118,8 @@ export interface ImageData {
* A model is organized like a table with rows of data. The
* fields of the data type T behave like columns.
*
* @template T the type of the model's items.
*
* ### Example
* As an example let's see the implementation of {@link ArrayModel}
*
@ -178,6 +180,18 @@ export abstract class Model<T> {
this.notify = new NullPeer();
}
/**
* Returns a new Model where all elements are mapped by the function `mapFunction`.
* @template T the type of the source model's items.
* @param mapFunction functions that maps
* @returns a new {@link MapModel} that wraps the current model.
*/
map<U>(
mapFunction: (data: T) => U
): MapModel<T, U> {
return new MapModel(this, mapFunction);
}
/**
* Implementations of this function must return the current number of rows.
*/
@ -195,7 +209,11 @@ export abstract class Model<T> {
* @param row index in range 0..(rowCount() - 1).
* @param data new data item to store on the given row index
*/
abstract setRowData(row: number, data: T): void;
setRowData(_row: number, _data: T): void {
console.log(
"setRowData called on a model which does not re-implement this method. This happens when trying to modify a read-only model"
);
}
/**
* Notifies the view that the data of the current row is changed.
@ -209,7 +227,7 @@ export abstract class Model<T> {
* Notifies the view that multiple rows are added to the model.
* @param row index of the first added row.
* @param count the number of added items.
*/
*/
protected notifyRowAdded(row: number, count: number): void {
this.notify.rowAdded(row, count);
}
@ -218,7 +236,7 @@ export abstract class Model<T> {
* Notifies the view that multiple rows are removed to the model.
* @param row index of the first removed row.
* @param count the number of removed items.
*/
*/
protected notifyRowRemoved(row: number, count: number): void {
this.notify.rowRemoved(row, count);
}
@ -235,10 +253,10 @@ export abstract class Model<T> {
* @hidden
*/
class NullPeer {
rowDataChanged(row: number): void { }
rowAdded(row: number, count: number): void { }
rowRemoved(row: number, count: number): void { }
reset(): void { }
rowDataChanged(row: number): void {}
rowAdded(row: number, count: number): void {}
rowRemoved(row: number, count: number): void {}
reset(): void {}
}
/**
@ -332,6 +350,162 @@ export class ArrayModel<T> extends Model<T> {
}
}
/**
* Provides rows that are generated by a map function based on the rows of another Model.
*
* @template T item type of source model that is mapped to U.
* @template U the type of the mapped items
*
* ## Example
*
* Here we have a {@link ArrayModel} holding rows of a custom interface `Name` and a {@link MapModel} that maps the name rows
* to single string rows.
*
* ```ts
* import { Model, ArrayModel, MapModel } from "./index";
*
* interface Name {
* first: string;
* last: string;
* }
*
* const model = new ArrayModel<Name>([
* {
* first: "Hans",
* last: "Emil",
* },
* {
* first: "Max",
* last: "Mustermann",
* },
* {
* first: "Roman",
* last: "Tisch",
* },
* ]);
*
* const mappedModel = new MapModel(
* model,
* (data) => {
* return data.last + ", " + data.first;
* }
* );
*
* // prints "Emil, Hans"
* console.log(mappedModel.rowData(0));
*
* // prints "Mustermann, Max"
* console.log(mappedModel.rowData(1));
*
* // prints "Tisch, Roman"
* console.log(mappedModel.rowData(2));
*
* // Alternatively you can use the shortcut {@link MapModel.map}.
*
* const model = new ArrayModel<Name>([
* {
* first: "Hans",
* last: "Emil",
* },
* {
* first: "Max",
* last: "Mustermann",
* },
* {
* first: "Roman",
* last: "Tisch",
* },
* ]);
*
* const mappedModel = model.map(
* (data) => {
* return data.last + ", " + data.first;
* }
* );
*
*
* // prints "Emil, Hans"
* console.log(mappedModel.rowData(0));
*
* // prints "Mustermann, Max"
* console.log(mappedModel.rowData(1));
*
* // prints "Tisch, Roman"
* console.log(mappedModel.rowData(2));
*
* // You can modifying the underlying {@link ArrayModel}:
*
* const model = new ArrayModel<Name>([
* {
* first: "Hans",
* last: "Emil",
* },
* {
* first: "Max",
* last: "Mustermann",
* },
* {
* first: "Roman",
* last: "Tisch",
* },
* ]);
*
* const mappedModel = model.map(
* (data) => {
* return data.last + ", " + data.first;
* }
* );
*
* model.setRowData(1, { first: "Minnie", last: "Musterfrau" } );
*
* // prints "Emil, Hans"
* console.log(mappedModel.rowData(0));
*
* // prints "Musterfrau, Minnie"
* console.log(mappedModel.rowData(1));
*
* // prints "Tisch, Roman"
* console.log(mappedModel.rowData(2));
* ```
*/
export class MapModel<T, U> extends Model<U> {
readonly sourceModel: Model<T>;
#mapFunction: (data: T) => U
/**
* Constructs the MapModel with a source model and map functions.
* @template T item type of source model that is mapped to U.
* @template U the type of the mapped items.
* @param sourceModel the wrapped model.
* @param mapFunction maps the data from T to U.
*/
constructor(
sourceModel: Model<T>,
mapFunction: (data: T) => U
) {
super();
this.sourceModel = sourceModel;
this.#mapFunction = mapFunction;
this.notify = this.sourceModel.notify;
}
/**
* Returns the number of entries in the model.
*/
rowCount(): number {
return this.sourceModel.rowCount();
}
/**
* Returns the data at the specified row.
* @param row index in range 0..(rowCount() - 1).
* @returns undefined if row is out of range otherwise the data.
*/
rowData(row: number): U {
return this.#mapFunction(this.sourceModel.rowData(row));
}
}
/**
* This interface describes the public API of a Slint component that is common to all instances. Use this to
* show() the window on the screen, access the window and subsequent window properties, or start the