Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add selector option to useValueIterator #18

Merged
merged 1 commit into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions integration-tests/create-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,94 @@ describe("createState", () => {
expect(unmountSpy).toHaveBeenCalledTimes(1);
});

test("useValueIterator does support selector option", async () => {
const user = userEvent.setup();
type Item = { id: number; text: string };
const item1: Item = { id: 1, text: "first item" };
const item2: Item = { id: 2, text: "second item" };
const item3: Item = { id: 3, text: "third item" };
const item4: Item = { id: 4, text: "fourth item" };
const item5: Item = { id: 5, text: "fifth item" };
const unmountSpy = jest.fn();
function IteratorComponent() {
const state = createState<{ value: Item[] }>({
value: [item1, item3, item4],
});

return createElement("div", {
children: [
createElement("button", {
"data-testid": "updateArrayButton",
onClick: () => {
state.setValue(() => ({ value: [item2, item1, item3, item5] }));
},
children: ["update array values"],
}),
createElement("button", {
"data-testid": "updateFirstItem",
onClick: () => {
state.setValue((currentValues) => ({
value: currentValues.value.map((value) => {
if (value.id === 1) {
return {
id: 1,
text: "updated first value",
};
} else {
return value;
}
}),
}));
},
}),
createElement("ul", {
"data-testid": "listComponent",
children: [
state.useValueIterator(
{ key: "id", selector: (state) => state.value },
({ elementState }) =>
createElement(() => {
onUnmount(unmountSpy);
return createElement("li", {
children: [
elementState.useValueSelector(
(element) => element.text,
(text) => createElement("span", { children: text })
),
],
});
})
),
],
}),
],
});
}

cleanup = attachComponent({
htmlElement: document.body,
component: createElement(IteratorComponent),
});

const listElement = screen.getByTestId("listComponent");
expect(listElement.childNodes.length).toBe(3);
const firstListElement = listElement.childNodes[0];
const secondListElement = listElement.childNodes[1];
const thirdListElement = listElement.childNodes[2];

expect(firstListElement.textContent).toBe(item1.text);
expect(secondListElement.textContent).toBe(item3.text);
expect(thirdListElement.textContent).toBe(item4.text);

await user.click(screen.getByTestId("updateFirstItem"));
expect(unmountSpy).not.toHaveBeenCalled();
expect(listElement.childNodes[0].textContent).toBe("updated first value");

await user.click(screen.getByTestId("updateArrayButton"));
expect(listElement.childNodes.length).toBe(4);
expect(unmountSpy).toHaveBeenCalledTimes(1);
});

test("useAttribute does not re-mount the component", async () => {
const user = userEvent.setup();
const spyFn = jest.fn();
Expand Down
35 changes: 28 additions & 7 deletions src/hooks/create-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ export type State<ValueType> = {
useValueIterator<Element>(
options: {
key: string | ((options: { element: Element; index: number }) => string);
selector?: (value: ValueType) => Element[];
},
cb: (props: {
elementState: State<Element>;
index: number;
}) => VelesElement | VelesComponent
): VelesComponent | VelesElement;
): VelesComponent | VelesElement | null;
getValue(): ValueType;
getPreviousValue(): undefined | ValueType;
setValue(newValueCB: (currentValue: ValueType) => ValueType): void;
Expand All @@ -59,6 +60,7 @@ type TrackingParams = {
elementState: State<any>;
index: number;
}) => VelesElement | VelesComponent;
selector?: (value: unknown) => any[];
renderedElements: [VelesElement | VelesComponent, string, State<unknown>][];
key: string | ((options: { element: unknown; index: number }) => string);
elementsByKey: {
Expand Down Expand Up @@ -155,10 +157,10 @@ function createState<T>(
});
return node;
},
// TODO: add a version with a selector
useValueIterator<Element>(
options: {
key: string | ((options: { element: any; index: number }) => string);
selector?: (value: T) => Element[];
},
cb: (props: {
elementState: State<Element>;
Expand All @@ -178,7 +180,14 @@ function createState<T>(
};
} = {};

(value as Element[]).forEach((element, index) => {
const elements = options.selector ? options.selector(value) : value;

if (!Array.isArray(elements)) {
console.error("useValueIterator received non-array value");
return null;
}

(elements as Element[]).forEach((element, index) => {
// we calculate a key for each element. This key determines whether we render the element from scratch, or do nothing
// when the element updates
let calculatedKey: string = "";
Expand Down Expand Up @@ -233,6 +242,10 @@ function createState<T>(
trackingParams.renderedElements = children;
trackingParams.wrapperComponent = wrapperComponent;

if (options.selector) {
trackingParams.selector = options.selector;
}

return wrapperComponent;

// 1. build a lookup table with existing values
Expand Down Expand Up @@ -374,8 +387,14 @@ function createState<T>(
});

trackingIterators.forEach((trackingIterator) => {
const { cb, key, renderedElements, elementsByKey, wrapperComponent } =
trackingIterator;
const {
cb,
key,
renderedElements,
elementsByKey,
wrapperComponent,
selector,
} = trackingIterator;
if (!wrapperComponent) {
console.error("there is no wrapper component for the iterator");
return;
Expand All @@ -392,10 +411,12 @@ function createState<T>(
return;
}

const elements = selector ? selector(value) : value;

// if we have any tracking iterators, it means the value is an array
// but I don't know how to have correct type inferring here
// so we check manually
if (Array.isArray(value)) {
if (Array.isArray(elements)) {
const newRenderedElements: [
VelesElement | VelesComponent,
string,
Expand All @@ -413,7 +434,7 @@ function createState<T>(
[calculatedKey: string]: boolean;
} = {};

value.forEach((element, index) => {
elements.forEach((element, index) => {
let calculatedKey: string = "";
if (
typeof key === "string" &&
Expand Down