@blockprotocol/hook
Advanced tools
Comparing version 0.0.1-canary.10 to 0.0.1
@@ -15,4 +15,4 @@ import { RefObject } from "react"; | ||
/** | ||
* Create a HookBlockHandler instance, using a reference to an element in the | ||
* block. | ||
* Create a HookEmbedderHandler instance, using a reference to an element | ||
* around the block. | ||
* | ||
@@ -26,3 +26,16 @@ * The hookService will only be reconstructed if the element reference changes. | ||
}; | ||
/** | ||
* Pass a node by ref to the embedding application's hook service. | ||
* | ||
* @param service The hook service returned by {@link useHookBlockService} | ||
* @param ref A React ref containing the DOM node. This hook will ensure the | ||
* embedding application is notified when the underlying DOM node | ||
* inside the ref changes | ||
* @param type The type of hook – i.e, "text" | ||
* @param path The path to the data associated with the hook | ||
* @param fallback A fallback to be called if the embedding application doesn't | ||
* implement the hook service, or doesn't implement this | ||
* specific type of hook. Return a function to "teardown" your | ||
* fallback (i.e, remove any event listeners). | ||
*/ | ||
export declare const useHook: <T extends HTMLElement>(service: HookBlockHandler | null, ref: RefObject<void | T | null>, type: string, path: string, fallback: (node: T) => void | (() => void)) => void; | ||
export declare const useHookRef: <T extends HTMLElement>(service: HookBlockHandler | null, type: string, path: string, fallback: (node: T | null) => void) => (node: T | null) => void; |
@@ -10,3 +10,3 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
}; | ||
import { useCallback, useEffect, useLayoutEffect, useRef, useState, } from "react"; | ||
import { useEffect, useLayoutEffect, useRef, useState } from "react"; | ||
import { HookBlockHandler, HookEmbedderHandler } from "./index.js"; | ||
@@ -47,4 +47,4 @@ const useHookServiceConstructor = ({ Handler, constructorArgs, ref, }) => { | ||
/** | ||
* Create a HookBlockHandler instance, using a reference to an element in the | ||
* block. | ||
* Create a HookEmbedderHandler instance, using a reference to an element | ||
* around the block. | ||
* | ||
@@ -62,5 +62,29 @@ * The hookService will only be reconstructed if the element reference changes. | ||
}; | ||
/** | ||
* Pass a node by ref to the embedding application's hook service. | ||
* | ||
* @param service The hook service returned by {@link useHookBlockService} | ||
* @param ref A React ref containing the DOM node. This hook will ensure the | ||
* embedding application is notified when the underlying DOM node | ||
* inside the ref changes | ||
* @param type The type of hook – i.e, "text" | ||
* @param path The path to the data associated with the hook | ||
* @param fallback A fallback to be called if the embedding application doesn't | ||
* implement the hook service, or doesn't implement this | ||
* specific type of hook. Return a function to "teardown" your | ||
* fallback (i.e, remove any event listeners). | ||
*/ | ||
export const useHook = (service, ref, type, path, fallback) => { | ||
const hookRef = useRef(null); | ||
const [, setError] = useState(); | ||
/** | ||
* React can't catch async errors to handle them within ErrorBoundary's, etc, | ||
* but if you throw it inside the callback for a setState function, it can. | ||
* | ||
* @see https://github.com/facebook/react/issues/14981#issuecomment-468460187 | ||
*/ | ||
const [, catchError] = useState(); | ||
/** | ||
* The fallback may change in between the hook message being sent, and the | ||
* not implemented error being received. This allows to ensure we call the | ||
* latest fallback, with no chance of calling a stale closure | ||
*/ | ||
const fallbackRef = useRef(fallback); | ||
@@ -70,7 +94,14 @@ useLayoutEffect(() => { | ||
}); | ||
const existingHookRef = useRef(null); | ||
/** | ||
* We can't use the normal effect teardown to trigger the hook teardown, as | ||
* in order to detect changes to the node underlying the ref, we run our main | ||
* effect on every render. Therefore, we create a "mount" effect and trigger | ||
* the teardown in the mount effect teardown. | ||
*/ | ||
useLayoutEffect(() => { | ||
return () => { | ||
var _a, _b; | ||
(_b = (_a = hookRef.current) === null || _a === void 0 ? void 0 : _a.teardown) === null || _b === void 0 ? void 0 : _b.call(_a).catch((err) => { | ||
setError(() => { | ||
(_b = (_a = existingHookRef.current) === null || _a === void 0 ? void 0 : _a.teardown) === null || _b === void 0 ? void 0 : _b.call(_a).catch((err) => { | ||
catchError(() => { | ||
throw err; | ||
@@ -83,4 +114,13 @@ }); | ||
var _a, _b, _c, _d, _e, _f; | ||
const existingHook = (_a = hookRef.current) === null || _a === void 0 ? void 0 : _a.params; | ||
const existingHook = (_a = existingHookRef.current) === null || _a === void 0 ? void 0 : _a.params; | ||
const node = ref.current; | ||
/** | ||
* We cannot use the dependency array for the effect, as refs aren't updated | ||
* during render, so the value passed into the dependency array for the ref | ||
* won't have updated and therefore updates to the underlying node wouldn't | ||
* trigger this effect, and embedding applications wouldn't be notified. | ||
* | ||
* Instead, we run the effect on every render and do our own change | ||
* detection. | ||
*/ | ||
if (existingHook && | ||
@@ -93,5 +133,10 @@ existingHook.service === service && | ||
} | ||
const teardownPromise = (_d = (_c = (_b = hookRef.current) === null || _b === void 0 ? void 0 : _b.teardown) === null || _c === void 0 ? void 0 : _c.call(_b).catch()) !== null && _d !== void 0 ? _d : Promise.resolve(); | ||
const teardownPromise = (_d = (_c = (_b = existingHookRef.current) === null || _b === void 0 ? void 0 : _b.teardown) === null || _c === void 0 ? void 0 : _c.call(_b).catch()) !== null && _d !== void 0 ? _d : Promise.resolve(); | ||
if (node && service) { | ||
const controller = new AbortController(); | ||
/** | ||
* Is this an update to the existing hook, or is it a whole new hook? The | ||
* only param to the hook which can change without creating a new hook is | ||
* the node. Any other change will result in a new hook being created | ||
*/ | ||
const reuseId = existingHook && | ||
@@ -102,3 +147,3 @@ existingHook.service === service && | ||
const hook = { | ||
id: reuseId ? (_f = (_e = hookRef.current) === null || _e === void 0 ? void 0 : _e.id) !== null && _f !== void 0 ? _f : null : null, | ||
id: reuseId ? (_f = (_e = existingHookRef.current) === null || _e === void 0 ? void 0 : _e.id) !== null && _f !== void 0 ? _f : null : null, | ||
params: { | ||
@@ -110,33 +155,39 @@ service, | ||
}, | ||
teardown: () => __awaiter(void 0, void 0, void 0, function* () { | ||
controller.abort(); | ||
if (hook.id) { | ||
try { | ||
hook.id = null; | ||
if (hookRef.current === hook) { | ||
hookRef.current = null; | ||
teardown() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (controller.signal.aborted) { | ||
return; | ||
} | ||
controller.abort(); | ||
const hookId = hook.id; | ||
if (hookId) { | ||
try { | ||
hook.id = null; | ||
if (existingHookRef.current === hook) { | ||
existingHookRef.current = null; | ||
} | ||
if (!service.destroyed) { | ||
yield service.hook({ | ||
data: { | ||
hookId, | ||
path, | ||
type, | ||
node: null, | ||
}, | ||
}); | ||
} | ||
} | ||
if (!service.destroyed) { | ||
yield service.hook({ | ||
data: { | ||
hookId: hook.id, | ||
path, | ||
type, | ||
node: null, | ||
}, | ||
catch (err) { | ||
catchError(() => { | ||
throw err; | ||
}); | ||
} | ||
} | ||
catch (err) { | ||
setError(() => { | ||
throw err; | ||
}); | ||
} | ||
} | ||
}), | ||
}); | ||
}, | ||
}; | ||
hookRef.current = hook; | ||
existingHookRef.current = hook; | ||
teardownPromise | ||
.then(() => { | ||
if (service.destroyed) { | ||
if (service.destroyed || controller.signal.aborted) { | ||
return; | ||
@@ -178,3 +229,3 @@ } | ||
.catch((err) => { | ||
setError(() => { | ||
catchError(() => { | ||
throw err; | ||
@@ -185,50 +236,6 @@ }); | ||
else { | ||
hookRef.current = null; | ||
existingHookRef.current = null; | ||
} | ||
}); | ||
}; | ||
export const useHookRef = (service, type, path, fallback) => { | ||
const hookId = useRef(null); | ||
const controllerRef = useRef(null); | ||
const fallbackRef = useRef(fallback); | ||
const [, setError] = useState(); | ||
useLayoutEffect(() => { | ||
fallbackRef.current = fallback; | ||
}); | ||
return useCallback((node) => { | ||
var _a; | ||
(_a = controllerRef.current) === null || _a === void 0 ? void 0 : _a.abort(); | ||
const controller = new AbortController(); | ||
controllerRef.current = controller; | ||
service === null || service === void 0 ? void 0 : service.hook({ | ||
data: { | ||
hookId: hookId.current, | ||
node, | ||
type, | ||
path, | ||
}, | ||
}).then((response) => { | ||
if (!controller.signal.aborted) { | ||
if (response.errors) { | ||
if (response.errors.length === 1 && | ||
response.errors[0].code === "NOT_IMPLEMENTED") { | ||
fallbackRef.current(node); | ||
} | ||
else { | ||
// eslint-disable-next-line no-console | ||
console.error(response.errors); | ||
throw new Error("Unknown error in hook"); | ||
} | ||
} | ||
else if (response.data) { | ||
hookId.current = response.data.hookId; | ||
} | ||
} | ||
}).catch((err) => { | ||
setError(() => { | ||
throw err; | ||
}); | ||
}); | ||
}, [path, service, type]); | ||
}; | ||
//# sourceMappingURL=react.js.map |
@@ -9,3 +9,3 @@ import { MessageCallback } from "@blockprotocol/core"; | ||
path: string; | ||
hookId?: string | null; | ||
hookId: string | null; | ||
}; | ||
@@ -12,0 +12,0 @@ export declare type BlockHookMessageCallbacks = {}; |
{ | ||
"name": "@blockprotocol/hook", | ||
"version": "0.0.1-canary.10", | ||
"version": "0.0.1", | ||
"description": "Implementation of the Block Protocol Hook service specification for blocks and embedding applications", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
119
README.md
## Block Protocol – Hook Service | ||
This package implements the Block Protocol Hook service for blocks and embedding applications. | ||
This package implements the Block Protocol Hraph service for blocks and embedding applications. | ||
### Todo | ||
To get started: | ||
- [ ] Write docs | ||
1. `yarn add @blockprotocol/hook` or `npm install @blockprotocol/hook` | ||
1. Follow the instructions to use the hook service as a [block](#blocks) or an [embedding application](#embedding-applications) | ||
## Blocks | ||
To create a `HookBlockHandler`, pass the constructor an element in your block, along with any callbacks you wish to register to handle incoming messages. | ||
To send a hook message, you call the `hook` function. | ||
```typescript | ||
import { HookBlockHandler } from "@blockprotocol/hook"; | ||
const handler = new HookBlockHandler({ element }); | ||
handler.hook({ | ||
data: { | ||
blockId: "hookId", | ||
node, | ||
type: "text", | ||
path: "$.text", | ||
}, | ||
}); | ||
``` | ||
### React example | ||
For React, we provide a `useHookBlockService` hook, which accepts a `ref` to an element. This will return an object with the shape of `{ hookService: HookBlockHandler | null }` which you can use to send hook messages. | ||
We also provide a `useHook` hook to make sending hook messages easier. | ||
```typescript | ||
import { useHook } from "@blockprotocol/hook/react"; | ||
useHook(hookService, nodeRef, "text", "$.text", (node) => { | ||
node.innerText = "hook fallback"; | ||
return () => { | ||
node.innerText = ""; | ||
}; | ||
}); | ||
``` | ||
Where `nodeRef` is a `RefObject` containing the DOM node you'd like to pass to the embedding application. | ||
### Custom elements | ||
There are no helpers for custom elements yet. | ||
## Embedding applications | ||
To create a `HookEmbedderHandler`, pass the constructor: | ||
1. An `element` wrapping your block | ||
1. `callbacks` to respond to messages from the block | ||
1. The starting values for any of the following messages you implement: | ||
- `hook` | ||
```typescript | ||
import { HookEmbedderHandler } from "@blockprotocol/hook"; | ||
const hookIds = new WeakMap<HTMLElement, string>(); | ||
const nodes = new Map<string, HTMLElement>(); | ||
const generateId = () => (Math.random() + 1).toString(36).substring(7); | ||
const hookService = new HookEmbedderHandler({ | ||
callbacks: { | ||
hook({ data }) { | ||
if (data.hookId) { | ||
const node = nodes.get(data.hookId); | ||
if (node) node.innerText = ""; | ||
nodes.delete(data.hookId); | ||
} | ||
const hookId = data.hookId ?? generateId(); | ||
if (data.node) { | ||
nodes.set(hookId, data.node); | ||
data.node.innerText = `Hook of type ${data.type} for path ${data.path}`; | ||
} | ||
return { hookId }; | ||
}, | ||
}, | ||
element: elementWrappingTheBlock, | ||
}); | ||
``` | ||
### React | ||
For React embedding applications, we provide a `useHookEmbedderService` hook, which accepts a `ref` to an element, and optionally any additional constructor arguments you wish to pass. | ||
```tsx | ||
import { useHookEmbedderService } from "@blockprotocol/hook/react"; | ||
import { useRef } from "react"; | ||
export const App = () => { | ||
const wrappingRef = useRef<HTMLDivElement>(null); | ||
useHookEmbedderService(blockRef, { | ||
hook({ data }) { | ||
// As above | ||
}, | ||
}); | ||
return ( | ||
<div ref={wrappingRef}> | ||
<Block /> | ||
</div> | ||
); | ||
}; | ||
``` |
Sorry, the diff of this file is not supported yet
30623
439
121