Comparing version 0.0.5 to 0.0.6
@@ -18,4 +18,3 @@ export declare type ForgoRef<T> = { | ||
render: (props: TProps, args: ForgoRenderArgs) => ForgoElement<ForgoComponentCtor<TProps>, TProps>; | ||
load?: () => void; | ||
unload?: () => void; | ||
unmount?: () => void; | ||
}; | ||
@@ -39,3 +38,8 @@ export declare type ForgoElement<TType extends string | ForgoComponentCtor<TProps>, TProps extends ForgoElementProps> = { | ||
}; | ||
export declare type EnvType = { | ||
window: Window | typeof globalThis; | ||
document: HTMLDocument; | ||
}; | ||
export declare function setCustomEnv(value: any): void; | ||
export declare function mount(forgoNode: ForgoNode, parentElement: HTMLElement | null): void; | ||
export declare function rerender(element: ForgoElementArg | undefined): void; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.rerender = exports.mount = void 0; | ||
exports.rerender = exports.mount = exports.setCustomEnv = void 0; | ||
/* | ||
@@ -11,2 +11,12 @@ The element types we care about. | ||
const TEXT_NODE_TYPE = 3; | ||
const documentObject = globalThis ? globalThis.document : document; | ||
const windowObject = globalThis ? globalThis : window; | ||
let env = { | ||
window: windowObject, | ||
document: documentObject, | ||
}; | ||
function setCustomEnv(value) { | ||
env = value; | ||
} | ||
exports.setCustomEnv = setCustomEnv; | ||
/* | ||
@@ -49,6 +59,13 @@ This is the main render function. | ||
function renderString(text, node, pendingAttachStates) { | ||
var _a; | ||
// Text nodes will always be recreated | ||
const textNode = document.createTextNode(text); | ||
const textNode = env.document.createTextNode(text); | ||
attachProps(text, textNode, pendingAttachStates); | ||
if (node) { | ||
// If there are old component states, we might need to unmount some of em. | ||
// After comparing with the new states. | ||
const oldComponentStates = (_a = getForgoState(node)) === null || _a === void 0 ? void 0 : _a.components; | ||
if (oldComponentStates) { | ||
unloadIncompatibleStates(pendingAttachStates, oldComponentStates); | ||
} | ||
node.replaceWith(textNode); | ||
@@ -71,4 +88,11 @@ } | ||
function renderDOMElement(forgoElement, node, pendingAttachStates) { | ||
var _a; | ||
if (node) { | ||
let nodeToBindTo; | ||
// If there are old component states, we might need to unmount some of em. | ||
// After comparing with the new states. | ||
const oldComponentStates = (_a = getForgoState(node)) === null || _a === void 0 ? void 0 : _a.components; | ||
if (oldComponentStates) { | ||
unloadIncompatibleStates(pendingAttachStates, oldComponentStates); | ||
} | ||
// if the nodes are not of the same of the same type, we need to replace. | ||
@@ -78,3 +102,3 @@ if (node.nodeType === TEXT_NODE_TYPE || | ||
node.tagName.toLowerCase() !== forgoElement.type)) { | ||
const newElement = document.createElement(forgoElement.type); | ||
const newElement = env.document.createElement(forgoElement.type); | ||
node.replaceWith(newElement); | ||
@@ -92,3 +116,3 @@ nodeToBindTo = newElement; | ||
// There was no node passed in, so create a new element. | ||
const newElement = document.createElement(forgoElement.type); | ||
const newElement = env.document.createElement(forgoElement.type); | ||
if (forgoElement.props.ref) { | ||
@@ -113,11 +137,2 @@ forgoElement.props.ref.value = newElement; | ||
if (!hasCompatibleState) { | ||
// If we don't have compatible state, | ||
// unload all components from index upwards | ||
const unloaded = state.components.slice(componentIndex); | ||
state.components = state.components.slice(0, componentIndex); | ||
for (const item of unloaded) { | ||
if (item.component.unload) { | ||
item.component.unload(); | ||
} | ||
} | ||
// We have to create a new component | ||
@@ -147,3 +162,3 @@ const args = { element: { componentIndex } }; | ||
// we'll push the savedComponentState into pending states for later attachment. | ||
const statesToAttach = pendingAttachStates.concat(savedComponentState); | ||
const statesToAttach = pendingAttachStates.concat(Object.assign(Object.assign({}, savedComponentState), { props: forgoElement.props })); | ||
// Get a new element by calling render on existing component. | ||
@@ -216,23 +231,12 @@ const newForgoElement = savedComponentState.component.render(forgoElement.props, args); | ||
else { | ||
if (typeof forgoChild.type === "string") { | ||
const findResult = findReplacementCandidateForDOMElement(forgoChild, childNodes, forgoChildIndex); | ||
if (findResult.found) { | ||
unloadNodes(parentElement, childNodes, forgoChildIndex, findResult.index); | ||
render(forgoChild, childNodes[findResult.index], []); | ||
} | ||
else { | ||
const { node } = render(forgoChild, undefined, []); | ||
parentElement.insertBefore(node, childNodes[forgoChildIndex]); | ||
} | ||
const findResult = typeof forgoChild.type === "string" | ||
? findReplacementCandidateForDOMElement(forgoChild, childNodes, forgoChildIndex) | ||
: findReplacementCandidateForCustomComponent(forgoChild, childNodes, forgoChildIndex); | ||
if (findResult.found) { | ||
unloadNodes(parentElement, childNodes, forgoChildIndex, findResult.index); | ||
render(forgoChild, childNodes[findResult.index], []); | ||
} | ||
else { | ||
const findResult = findReplacementCandidateForCustomComponent(forgoChild, childNodes, forgoChildIndex); | ||
if (findResult.found) { | ||
unloadNodes(parentElement, childNodes, forgoChildIndex, findResult.index); | ||
render(forgoChild, childNodes[findResult.index], []); | ||
} | ||
else { | ||
const { node } = render(forgoChild, undefined, []); | ||
parentElement.insertBefore(node, childNodes[forgoChildIndex]); | ||
} | ||
const { node } = render(forgoChild, undefined, []); | ||
parentElement.insertBefore(node, childNodes[forgoChildIndex]); | ||
} | ||
@@ -247,3 +251,3 @@ } | ||
/* | ||
Unloads nodes from a node list | ||
Unloads components from a node list | ||
This does: | ||
@@ -260,4 +264,4 @@ a) Remove the node | ||
for (const componentState of state.components) { | ||
if (componentState.component.unload) { | ||
componentState.component.unload(); | ||
if (componentState.component.unmount) { | ||
componentState.component.unmount(); | ||
} | ||
@@ -269,2 +273,32 @@ } | ||
/* | ||
When states is attached to a new node, | ||
or when states are reattached, some of the old component states need to go away. | ||
The corresponding components will need to be unmounted. | ||
While rendering, the component gets reused if the ctor is the same. | ||
If the ctor is different, the component is discarded. And hence needs to be unmounted. | ||
So we check the ctor type in old and new. | ||
*/ | ||
function unloadIncompatibleStates(newStates, oldStates) { | ||
let i = 0; | ||
for (const newState of newStates) { | ||
if (oldStates.length > i) { | ||
const oldState = oldStates[i]; | ||
if (oldState.ctor !== newState.ctor) { | ||
break; | ||
} | ||
i++; | ||
} | ||
else { | ||
break; | ||
} | ||
} | ||
for (let j = i; j < oldStates.length; j++) { | ||
const oldState = oldStates[j]; | ||
if (oldState.component.unmount) { | ||
oldState.component.unmount(); | ||
} | ||
} | ||
} | ||
/* | ||
When we try to find replacement candidates for DOM nodes, | ||
@@ -271,0 +305,0 @@ we try to: |
{ | ||
"name": "forgo", | ||
"version": "0.0.5", | ||
"main": "./dist" | ||
"version": "0.0.6", | ||
"main": "./dist", | ||
"devDependencies": { | ||
"@types/jsdom": "^16.2.6", | ||
"@types/mocha": "^8.2.0", | ||
"@types/should": "^13.0.0", | ||
"esm": "^3.2.25", | ||
"jsdom": "^16.4.0", | ||
"mocha": "^8.2.1", | ||
"should": "^13.2.3", | ||
"typescript": "^4.1.3" | ||
}, | ||
"scripts": { | ||
"build-test": "(cd test && ./build.sh)", | ||
"test": "mocha -r esm test/dist/test.js" | ||
}, | ||
"license": "MIT" | ||
} |
167
README.md
# forgo | ||
Forgo is a 4KB library that makes it super easy to create modern web apps using JSX (like React). | ||
Forgo is a 4KB library that makes it super easy to create modern web apps using JSX (like React). | ||
Unlike React, apps are plain JS with very little framework specific code. Everything you already know about DOM APIs and JavaScript will easily carry over. | ||
- Use HTML DOM APIs for accessing elements | ||
- There are no synthetic events, use standard DOM APIs | ||
- There are no synthetic events | ||
- Use closures for maintaining component state | ||
- Use any singleton pattern for managing app-wide state | ||
- There's no vDOM, DOM diffing etc. Renders are manually triggered. | ||
- There's no vDOM or DOM diffing. Renders are manually triggered | ||
Forgo is basically just one small JS file (actually TypeScript). It's somewhat decently documented, but I could use some help here. A stated goal of the project is to always remain within that single file. | ||
Forgo is basically just one small JS file (actually TypeScript). It's somewhat decently documented, but I could use some help here. A stated goal of the project is to always remain within that single file. | ||
@@ -57,2 +56,156 @@ ## Installation | ||
## Child Components and Passing Props | ||
That works just as you'd have seen in React. | ||
```jsx | ||
function Parent(props) { | ||
return { | ||
render(props, args) { | ||
return ( | ||
<div> | ||
<Greeter firstName="Jeswin" /> | ||
<Greeter firstName="Kai" /> | ||
</div> | ||
); | ||
}, | ||
}; | ||
} | ||
function Greeter(props) { | ||
return { | ||
render(props, args) { | ||
return <div>Hello {props.firstName}</div>; | ||
}, | ||
}; | ||
} | ||
``` | ||
## Reading Form Input Elements | ||
You're expected to read form input control values with regular DOM APIs. | ||
There's a small hurdle though - how do you we get a reference to these nodes? Well, that's where the ref attribute comes in. An object bound to the ref attribute in the markup will have its value property set to the DOM element. | ||
See the usage below: | ||
```jsx | ||
function Component(props) { | ||
const myInputRef = {}; | ||
return { | ||
render(props, args) { | ||
function onClick() { | ||
const inputElement = myInputRef.value; | ||
alert(inputElement.value); // Read the text input! | ||
} | ||
return ( | ||
<div> | ||
<input type="text" ref={myInputRef} /> | ||
<button onclick={onClick}>Click me!</button> | ||
</div> | ||
); | ||
}, | ||
}; | ||
} | ||
``` | ||
## Multiple Components, passing props etc. | ||
Finally, let's do a recap with a more complete example. Let's make a Todo List app in TypeScript. | ||
There will be three components: | ||
1. TodoList (the main component) | ||
2. TodoListItem | ||
3. AddTodo | ||
Here's the TodoList, which hosts the other two components. | ||
```tsx | ||
type TodoListProps = {}; | ||
function TodoList(props: TodoListProps) { | ||
let todos: string[] = []; | ||
return { | ||
render(props: TodoListProps, args: ForgoRenderArgs) { | ||
function onTodoAdd(text: string) { | ||
todos.push(text); | ||
rerender(args.element); | ||
} | ||
return ( | ||
<div> | ||
<h1>Forgo Todos</h1> | ||
<ul> | ||
{todos.map((t) => ( | ||
<TodoListItem text={t} /> | ||
))} | ||
</ul> | ||
<AddTodo onAdd={onTodoAdd} /> | ||
</div> | ||
); | ||
}, | ||
}; | ||
} | ||
``` | ||
Here's the TodoList item, which simply displays a Todo. | ||
```tsx | ||
type TodoListItemProps = { | ||
text: string; | ||
}; | ||
function TodoListItem(props: TodoListItemProps) { | ||
return { | ||
render() { | ||
return <li>{props.text}</li>; | ||
}, | ||
}; | ||
} | ||
``` | ||
And here's the AddTodo component. It takes an onAdd function from the parent, which gets called whenever a new todo is added. | ||
```tsx | ||
type AddTodoProps = { | ||
onAdd: (text: string) => void; | ||
}; | ||
function AddTodo(props: AddTodoProps) { | ||
const input: { value?: HTMLInputElement } = {}; | ||
function onClick() { | ||
const inputEl = input.value; | ||
if (inputEl) { | ||
props.onAdd(inputEl.value); | ||
inputEl.value = ""; | ||
inputEl.focus(); | ||
} | ||
} | ||
return { | ||
render() { | ||
return ( | ||
<div> | ||
<input type="text" ref={input} /> | ||
<button onclick={onClick}>Add me!</button> | ||
</div> | ||
); | ||
}, | ||
}; | ||
} | ||
``` | ||
That's all. Mount it, and we're ready to go. | ||
```ts | ||
window.addEventListener("load", () => { | ||
mount(<TodoList />, document.getElementById("root")); | ||
}); | ||
``` | ||
## Building | ||
@@ -86,4 +239,4 @@ | ||
"jsxImportSource": "forgo" | ||
}, | ||
} | ||
} | ||
``` | ||
``` |
157
src/index.ts
@@ -36,4 +36,3 @@ /* | ||
1. render() returns the actual DOM to render. | ||
2. load() is optional. And gets called just before the component gets mounted. | ||
3. unload() is optional. Opposite of load, gets called just before unmount. | ||
2. unmount() is optional. Gets called just before unmount. | ||
*/ | ||
@@ -45,4 +44,3 @@ export type ForgoComponent<TProps extends ForgoElementProps> = { | ||
) => ForgoElement<ForgoComponentCtor<TProps>, TProps>; | ||
load?: () => void; | ||
unload?: () => void; | ||
unmount?: () => void; | ||
}; | ||
@@ -118,2 +116,22 @@ | ||
/* | ||
The following adds support for injecting test environment objects. | ||
Such as JSDOM. | ||
*/ | ||
export type EnvType = { | ||
window: Window | typeof globalThis; | ||
document: HTMLDocument; | ||
}; | ||
const documentObject = globalThis ? globalThis.document : document; | ||
const windowObject = globalThis ? globalThis : window; | ||
let env: EnvType = { | ||
window: windowObject, | ||
document: documentObject, | ||
}; | ||
export function setCustomEnv(value: any) { | ||
env = value; | ||
} | ||
/* | ||
This is the main render function. | ||
@@ -173,5 +191,12 @@ forgoNode is the node to render. | ||
// Text nodes will always be recreated | ||
const textNode = document.createTextNode(text); | ||
const textNode = env.document.createTextNode(text); | ||
attachProps(text, textNode, pendingAttachStates); | ||
if (node) { | ||
// If there are old component states, we might need to unmount some of em. | ||
// After comparing with the new states. | ||
const oldComponentStates = getForgoState(node)?.components; | ||
if (oldComponentStates) { | ||
unloadIncompatibleStates(pendingAttachStates, oldComponentStates); | ||
} | ||
node.replaceWith(textNode); | ||
@@ -202,2 +227,9 @@ } | ||
// If there are old component states, we might need to unmount some of em. | ||
// After comparing with the new states. | ||
const oldComponentStates = getForgoState(node)?.components; | ||
if (oldComponentStates) { | ||
unloadIncompatibleStates(pendingAttachStates, oldComponentStates); | ||
} | ||
// if the nodes are not of the same of the same type, we need to replace. | ||
@@ -209,3 +241,3 @@ if ( | ||
) { | ||
const newElement = document.createElement(forgoElement.type); | ||
const newElement = env.document.createElement(forgoElement.type); | ||
node.replaceWith(newElement); | ||
@@ -217,2 +249,3 @@ nodeToBindTo = newElement; | ||
attachProps(forgoElement, nodeToBindTo, pendingAttachStates); | ||
renderChildNodes(forgoElement, nodeToBindTo as HTMLElement); | ||
@@ -222,3 +255,3 @@ return { node: nodeToBindTo }; | ||
// There was no node passed in, so create a new element. | ||
const newElement = document.createElement(forgoElement.type); | ||
const newElement = env.document.createElement(forgoElement.type); | ||
if (forgoElement.props.ref) { | ||
@@ -251,12 +284,2 @@ forgoElement.props.ref.value = newElement; | ||
if (!hasCompatibleState) { | ||
// If we don't have compatible state, | ||
// unload all components from index upwards | ||
const unloaded = state.components.slice(componentIndex); | ||
state.components = state.components.slice(0, componentIndex); | ||
for (const item of unloaded) { | ||
if (item.component.unload) { | ||
item.component.unload(); | ||
} | ||
} | ||
// We have to create a new component | ||
@@ -290,3 +313,6 @@ const args: ForgoRenderArgs = { element: { componentIndex } }; | ||
// we'll push the savedComponentState into pending states for later attachment. | ||
const statesToAttach = pendingAttachStates.concat(savedComponentState); | ||
const statesToAttach = pendingAttachStates.concat({ | ||
...savedComponentState, | ||
props: forgoElement.props, | ||
}); | ||
@@ -380,38 +406,26 @@ // Get a new element by calling render on existing component. | ||
} else { | ||
if (typeof forgoChild.type === "string") { | ||
const findResult = findReplacementCandidateForDOMElement( | ||
forgoChild as ForgoElement<string, any>, | ||
const findResult = | ||
typeof forgoChild.type === "string" | ||
? findReplacementCandidateForDOMElement( | ||
forgoChild as ForgoElement<string, any>, | ||
childNodes, | ||
forgoChildIndex | ||
) | ||
: findReplacementCandidateForCustomComponent( | ||
forgoChild as ForgoElement<ForgoComponentCtor<any>, any>, | ||
childNodes, | ||
forgoChildIndex | ||
); | ||
if (findResult.found) { | ||
unloadNodes( | ||
parentElement, | ||
childNodes, | ||
forgoChildIndex | ||
forgoChildIndex, | ||
findResult.index | ||
); | ||
if (findResult.found) { | ||
unloadNodes( | ||
parentElement, | ||
childNodes, | ||
forgoChildIndex, | ||
findResult.index | ||
); | ||
render(forgoChild, childNodes[findResult.index], []); | ||
} else { | ||
const { node } = render(forgoChild, undefined, []); | ||
parentElement.insertBefore(node, childNodes[forgoChildIndex]); | ||
} | ||
render(forgoChild, childNodes[findResult.index], []); | ||
} else { | ||
const findResult = findReplacementCandidateForCustomComponent( | ||
forgoChild as ForgoElement<ForgoComponentCtor<any>, any>, | ||
childNodes, | ||
forgoChildIndex | ||
); | ||
if (findResult.found) { | ||
unloadNodes( | ||
parentElement, | ||
childNodes, | ||
forgoChildIndex, | ||
findResult.index | ||
); | ||
render(forgoChild, childNodes[findResult.index], []); | ||
} else { | ||
const { node } = render(forgoChild, undefined, []); | ||
parentElement.insertBefore(node, childNodes[forgoChildIndex]); | ||
} | ||
const { node } = render(forgoChild, undefined, []); | ||
parentElement.insertBefore(node, childNodes[forgoChildIndex]); | ||
} | ||
@@ -432,3 +446,3 @@ } | ||
/* | ||
Unloads nodes from a node list | ||
Unloads components from a node list | ||
This does: | ||
@@ -450,4 +464,4 @@ a) Remove the node | ||
for (const componentState of state.components) { | ||
if (componentState.component.unload) { | ||
componentState.component.unload(); | ||
if (componentState.component.unmount) { | ||
componentState.component.unmount(); | ||
} | ||
@@ -459,2 +473,37 @@ } | ||
/* | ||
When states is attached to a new node, | ||
or when states are reattached, some of the old component states need to go away. | ||
The corresponding components will need to be unmounted. | ||
While rendering, the component gets reused if the ctor is the same. | ||
If the ctor is different, the component is discarded. And hence needs to be unmounted. | ||
So we check the ctor type in old and new. | ||
*/ | ||
function unloadIncompatibleStates( | ||
newStates: NodeAttachedComponentState<any>[], | ||
oldStates: NodeAttachedComponentState<any>[] | ||
) { | ||
let i = 0; | ||
for (const newState of newStates) { | ||
if (oldStates.length > i) { | ||
const oldState = oldStates[i]; | ||
if (oldState.ctor !== newState.ctor) { | ||
break; | ||
} | ||
i++; | ||
} else { | ||
break; | ||
} | ||
} | ||
for (let j = i; j < oldStates.length; j++) { | ||
const oldState = oldStates[j]; | ||
if (oldState.component.unmount) { | ||
oldState.component.unmount(); | ||
} | ||
} | ||
} | ||
type CandidateSearchResult = | ||
@@ -461,0 +510,0 @@ | { |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
65828
13
1110
241
8