Skip to content

Commit

Permalink
add initial Context implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Bloomca committed Jun 11, 2024
1 parent edc6903 commit b1bce58
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 26 deletions.
9 changes: 1 addition & 8 deletions integration-tests/assign-attributes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,7 @@ import userEvent from "@testing-library/user-event";

import { attachComponent, createElement, createState, createRef } from "../src";

function shallow(obj1: Record<string, any>, obj2: Record<string, any>) {
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(() => {
Expand Down
85 changes: 85 additions & 0 deletions integration-tests/context.test.ts
Original file line number Diff line number Diff line change
@@ -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<number>();

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<number>();

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"
);
});
});
9 changes: 1 addition & 8 deletions integration-tests/create-state/track-value.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,7 @@ import {
createRef,
} from "../../src";

function shallow(obj1: Record<string, any>, obj2: Record<string, any>) {
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(() => {
Expand Down
4 changes: 1 addition & 3 deletions integration-tests/create-state/use-attribute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import {
onUnmount,
} from "../../src";

import type { State } from "../../src";

describe("createState", () => {
describe("state.useAttribute", () => {
let cleanup: Function | undefined;

afterEach(() => {
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/create-state/use-value-iterator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {

import type { State } from "../../src";

describe("createState", () => {
describe("state.useValueIterator", () => {
let cleanup: Function | undefined;

afterEach(() => {
Expand Down
3 changes: 3 additions & 0 deletions src/_utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { executeComponent } from "./create-element/parse-component";
import { addPublicContext, popPublicContext } from "./context";

import type {
VelesComponentObject,
Expand Down Expand Up @@ -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: () => {
Expand Down
72 changes: 72 additions & 0 deletions src/context/index.ts
Original file line number Diff line number Diff line change
@@ -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<T>() {
// 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 };
3 changes: 3 additions & 0 deletions src/context/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type ComponentContext = {
[id: number]: any;
};
14 changes: 11 additions & 3 deletions src/create-state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@ 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,
VelesComponentObject,
VelesStringElement,
} from "../types";

import type { State, TrackingIterator, StateTrackers } from "./types";
import type {
State,
TrackingIterator,
StateTrackers,
TrackingSelectorElement,
} from "./types";

function createState<T>(
initialValue: T,
Expand Down Expand Up @@ -93,14 +98,17 @@ function createState<T>(
? createTextElement(returnedNode as string)
: returnedNode;

const currentContext = getCurrentContext();

node.needExecutedVersion = true;

const trackingSelectorElement = {
const trackingSelectorElement: TrackingSelectorElement = {
selector,
selectedValue,
cb,
node,
comparator,
savedContext: currentContext,
};

addUseValueMountHandler({
Expand Down
4 changes: 2 additions & 2 deletions src/create-state/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import type {
VelesComponentObject,
VelesStringElement,
AttributeHelper,
ExecutedVelesElement,
ExecutedVelesComponent,
} from "../types";
import type { ComponentContext } from "../context/types";

export type State<ValueType> = {
trackValue(
Expand Down Expand Up @@ -84,6 +83,7 @@ export type TrackingSelectorElement = {
selectedValue: any;
comparator: (value1: any, value2: any) => boolean;
node: VelesElement | VelesComponentObject | VelesStringElement;
savedContext: ComponentContext;
};

export type TrackingAttribute = {
Expand Down
6 changes: 5 additions & 1 deletion src/create-state/update-usevalue-selector-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getExecutedComponentVelesNode,
} from "../_utils";
import { createTextElement } from "../create-element/create-text-element";
import { addPublicContext, popPublicContext } from "../context";

import type {
ExecutedVelesElement,
Expand All @@ -25,7 +26,7 @@ function updateUseValueSelector<T>({
trackers: StateTrackers;
getValue: () => T;
}) {
const { selectedValue, selector, cb, node, comparator } =
const { selectedValue, selector, cb, node, comparator, savedContext } =
selectorTrackingElement;
const newSelectedValue = selector ? selector(value) : value;

Expand All @@ -34,11 +35,13 @@ function updateUseValueSelector<T>({
return;
}

addPublicContext(savedContext);
const returnednewNode = cb
? cb(newSelectedValue)
: newSelectedValue == undefined
? ""
: String(newSelectedValue);
popPublicContext();
const newNode =
!returnednewNode || typeof returnednewNode === "string"
? createTextElement(returnednewNode as string)
Expand Down Expand Up @@ -70,6 +73,7 @@ function updateUseValueSelector<T>({
cb,
node: newNode,
comparator,
savedContext,
};

if (parentVelesElementRendered) {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

0 comments on commit b1bce58

Please sign in to comment.