Skip to content

Commit

Permalink
add selector option to useValueIterator
Browse files Browse the repository at this point in the history
  • Loading branch information
Bloomca committed May 27, 2024
1 parent 41967bf commit ec379d7
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 7 deletions.
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

0 comments on commit ec379d7

Please sign in to comment.