diff --git a/integration-tests/assign-attributes.test.ts b/integration-tests/assign-attributes.test.ts index 9bd3fc1..177838c 100644 --- a/integration-tests/assign-attributes.test.ts +++ b/integration-tests/assign-attributes.test.ts @@ -3,14 +3,7 @@ import userEvent from "@testing-library/user-event"; import { attachComponent, createElement, createState, createRef } from "../src"; -function shallow(obj1: Record, obj2: Record) { - return ( - Object.keys(obj1).length === Object.keys(obj2).length && - Object.keys(obj1).every((key) => obj1[key] === obj2[key]) - ); -} - -describe("createState", () => { +describe("assign-attributes", () => { let cleanup: Function | undefined; afterEach(() => { diff --git a/integration-tests/context.test.ts b/integration-tests/context.test.ts new file mode 100644 index 0000000..1601c80 --- /dev/null +++ b/integration-tests/context.test.ts @@ -0,0 +1,85 @@ +import { screen } from "@testing-library/dom"; +import userEvent from "@testing-library/user-event"; + +import { + attachComponent, + createElement, + createState, + createRef, + createContext, +} from "../src"; + +describe("Context", () => { + let cleanup: Function | undefined; + + afterEach(() => { + cleanup?.(); + cleanup = undefined; + }); + + test("nested components have access to the Context", () => { + const exampleContext = createContext(); + + function App() { + return createElement("div", { + children: [ + createElement("h1", { children: "Application" }), + createElement(NestedComponent), + ], + }); + } + + function NestedComponent() { + const exampleValue = exampleContext.readContext(); + + return createElement("div", { + "data-testid": "contextContent", + children: `context value is ${exampleValue}`, + }); + } + + cleanup = attachComponent({ + htmlElement: document.body, + component: createElement(exampleContext.Provider, { + value: 5, + children: createElement(App), + }), + }); + + expect(screen.getByTestId("contextContent").textContent).toBe( + "context value is 5" + ); + }); + + test("nested components have access to the Context if added with Context.addContext()", () => { + const exampleContext = createContext(); + + function App() { + exampleContext.addContext(5); + return createElement("div", { + children: [ + createElement("h1", { children: "Application" }), + createElement(NestedComponent), + ], + }); + } + + function NestedComponent() { + const exampleValue = exampleContext.readContext(); + + return createElement("div", { + "data-testid": "contextContent", + children: `context value is ${exampleValue}`, + }); + } + + cleanup = attachComponent({ + htmlElement: document.body, + component: createElement(App), + }); + + expect(screen.getByTestId("contextContent").textContent).toBe( + "context value is 5" + ); + }); +}); diff --git a/integration-tests/create-state/track-value.test.ts b/integration-tests/create-state/track-value.test.ts index 1983178..026665d 100644 --- a/integration-tests/create-state/track-value.test.ts +++ b/integration-tests/create-state/track-value.test.ts @@ -9,14 +9,7 @@ import { createRef, } from "../../src"; -function shallow(obj1: Record, obj2: Record) { - return ( - Object.keys(obj1).length === Object.keys(obj2).length && - Object.keys(obj1).every((key) => obj1[key] === obj2[key]) - ); -} - -describe("createState", () => { +describe("track-value", () => { let cleanup: Function | undefined; afterEach(() => { diff --git a/integration-tests/create-state/use-attribute.test.ts b/integration-tests/create-state/use-attribute.test.ts index ecb3453..2fac970 100644 --- a/integration-tests/create-state/use-attribute.test.ts +++ b/integration-tests/create-state/use-attribute.test.ts @@ -8,9 +8,7 @@ import { onUnmount, } from "../../src"; -import type { State } from "../../src"; - -describe("createState", () => { +describe("state.useAttribute", () => { let cleanup: Function | undefined; afterEach(() => { diff --git a/integration-tests/create-state/use-value-iterator.test.ts b/integration-tests/create-state/use-value-iterator.test.ts index 58ca759..9c99e2e 100644 --- a/integration-tests/create-state/use-value-iterator.test.ts +++ b/integration-tests/create-state/use-value-iterator.test.ts @@ -10,7 +10,7 @@ import { import type { State } from "../../src"; -describe("createState", () => { +describe("state.useValueIterator", () => { let cleanup: Function | undefined; afterEach(() => { diff --git a/src/_utils.ts b/src/_utils.ts index 3a3b9d3..d4ddbeb 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -1,4 +1,5 @@ import { executeComponent } from "./create-element/parse-component"; +import { addPublicContext, popPublicContext } from "./context"; import type { VelesComponentObject, @@ -51,10 +52,12 @@ function renderTree( } return executedString; } else if ("velesComponentObject" in component) { + addPublicContext(); const componentTree = executeComponent(component); const executedComponent = {} as ExecutedVelesComponent; executedComponent.executedVelesComponent = true; executedComponent.tree = renderTree(componentTree.tree); + popPublicContext(); executedComponent._privateMethods = { ...componentTree._privateMethods, _callMountHandlers: () => { diff --git a/src/context/index.ts b/src/context/index.ts new file mode 100644 index 0000000..d278509 --- /dev/null +++ b/src/context/index.ts @@ -0,0 +1,72 @@ +import { Fragment } from "../fragment"; +import { createElement } from "../create-element"; + +import type { VelesChildren } from "../types"; +import type { ComponentContext } from "./types.d.ts"; + +// the name is so convoluted because we also +// use context stack for lifecycle +const publicContextStack: ComponentContext[] = []; + +let contextIdCounter = 1; +function createContext() { + // unique context id + const contextId = contextIdCounter++; + function addContext(value: T) { + const currentContextObject = + publicContextStack[publicContextStack.length - 1]; + + if (!currentContextObject) { + // either executed outside of the rendering framework + // or some bug + console.error("cannot add Context due to missing stack value"); + } else { + publicContextStack[publicContextStack.length - 1] = { + ...currentContextObject, + [contextId]: value, + }; + } + } + return { + Provider: ({ value, children }: { value: T; children: VelesChildren }) => { + addContext(value); + return createElement(Fragment, { children }); + }, + addContext, + readContext: (): T => { + const currentContext = publicContextStack[publicContextStack.length - 1]; + + if (!currentContext) { + // we are outside the context somehow + console.error("no Context currently available"); + } else { + return currentContext[contextId]; + } + }, + }; +} + +function addPublicContext(specificContext?: ComponentContext) { + if (specificContext) { + publicContextStack.push(specificContext); + } else { + if (publicContextStack.length === 0) { + publicContextStack.push({}); + } else { + const currentContext = publicContextStack[publicContextStack.length - 1]; + publicContextStack.push(currentContext); + } + } +} + +function popPublicContext() { + publicContextStack.pop(); +} + +// this function is needed to save current context to re-execute components +// which are mounted conditionally +function getCurrentContext() { + return publicContextStack[publicContextStack.length - 1]; +} + +export { createContext, addPublicContext, popPublicContext, getCurrentContext }; diff --git a/src/context/types.d.ts b/src/context/types.d.ts new file mode 100644 index 0000000..c6bd3b4 --- /dev/null +++ b/src/context/types.d.ts @@ -0,0 +1,3 @@ +export type ComponentContext = { + [id: number]: any; +}; diff --git a/src/create-state/index.ts b/src/create-state/index.ts index b008d0d..2ff24ce 100644 --- a/src/create-state/index.ts +++ b/src/create-state/index.ts @@ -5,7 +5,7 @@ import { createTextElement } from "../create-element/create-text-element"; import { triggerUpdates } from "./trigger-updates"; import { addUseValueMountHandler } from "./update-usevalue-selector-value"; import { updateUseAttributeValue } from "./update-useattribute-value"; -import { updateUseValueIteratorValue } from "./update-usevalueiterator-value"; +import { getCurrentContext } from "../context"; import type { VelesElement, @@ -13,7 +13,12 @@ import type { VelesStringElement, } from "../types"; -import type { State, TrackingIterator, StateTrackers } from "./types"; +import type { + State, + TrackingIterator, + StateTrackers, + TrackingSelectorElement, +} from "./types"; function createState( initialValue: T, @@ -93,14 +98,17 @@ function createState( ? createTextElement(returnedNode as string) : returnedNode; + const currentContext = getCurrentContext(); + node.needExecutedVersion = true; - const trackingSelectorElement = { + const trackingSelectorElement: TrackingSelectorElement = { selector, selectedValue, cb, node, comparator, + savedContext: currentContext, }; addUseValueMountHandler({ diff --git a/src/create-state/types.d.ts b/src/create-state/types.d.ts index 97ebd76..f0a4e21 100644 --- a/src/create-state/types.d.ts +++ b/src/create-state/types.d.ts @@ -3,9 +3,8 @@ import type { VelesComponentObject, VelesStringElement, AttributeHelper, - ExecutedVelesElement, - ExecutedVelesComponent, } from "../types"; +import type { ComponentContext } from "../context/types"; export type State = { trackValue( @@ -84,6 +83,7 @@ export type TrackingSelectorElement = { selectedValue: any; comparator: (value1: any, value2: any) => boolean; node: VelesElement | VelesComponentObject | VelesStringElement; + savedContext: ComponentContext; }; export type TrackingAttribute = { diff --git a/src/create-state/update-usevalue-selector-value.ts b/src/create-state/update-usevalue-selector-value.ts index a2abd32..fe19374 100644 --- a/src/create-state/update-usevalue-selector-value.ts +++ b/src/create-state/update-usevalue-selector-value.ts @@ -5,6 +5,7 @@ import { getExecutedComponentVelesNode, } from "../_utils"; import { createTextElement } from "../create-element/create-text-element"; +import { addPublicContext, popPublicContext } from "../context"; import type { ExecutedVelesElement, @@ -25,7 +26,7 @@ function updateUseValueSelector({ trackers: StateTrackers; getValue: () => T; }) { - const { selectedValue, selector, cb, node, comparator } = + const { selectedValue, selector, cb, node, comparator, savedContext } = selectorTrackingElement; const newSelectedValue = selector ? selector(value) : value; @@ -34,11 +35,13 @@ function updateUseValueSelector({ return; } + addPublicContext(savedContext); const returnednewNode = cb ? cb(newSelectedValue) : newSelectedValue == undefined ? "" : String(newSelectedValue); + popPublicContext(); const newNode = !returnednewNode || typeof returnednewNode === "string" ? createTextElement(returnednewNode as string) @@ -70,6 +73,7 @@ function updateUseValueSelector({ cb, node: newNode, comparator, + savedContext, }; if (parentVelesElementRendered) { diff --git a/src/index.ts b/src/index.ts index c177227..c185c57 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,5 +4,6 @@ export { onMount, onUnmount } from "./hooks"; export { createState } from "./create-state"; export { createRef } from "./create-ref"; export { Fragment } from "./fragment"; +export { createContext } from "./context"; export type { State } from "./create-state/types";