Skip to content

Commit

Permalink
Add reset function to useWebViewState (#717)
Browse files Browse the repository at this point in the history
  • Loading branch information
rolfheij-sil authored Jan 18, 2024
2 parents d8b1c62 + b3addaa commit 054c2dd
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@ import papi from '@papi/frontend';
import { useEvent, Button } from 'platform-bible-react';
import { useCallback, useState } from 'react';
import type { HelloWorldEvent } from 'hello-world';
import { WebViewProps } from '@papi/core';

globalThis.webViewComponent = function HelloWorld2() {
const randomInt = () => {
return Math.floor(Math.random() * 100);
};

globalThis.webViewComponent = function HelloWorld2({ useWebViewState }: WebViewProps) {
const [clicks, setClicks] = useState(0);
const [defaultClicks, setDefaultClicks] = useState(randomInt());
const [webViewStateClicks, setWebViewStateClicks, resetWebViewStateClicks] = useWebViewState(
'webViewStateClicks',
defaultClicks,
);

// Update the clicks when we are informed helloWorld has been run
useEvent(
Expand All @@ -24,7 +34,21 @@ globalThis.webViewComponent = function HelloWorld2() {
setClicks(clicks + 1);
}}
>
Hello World {clicks}
use-Event Clicks: {clicks}
</Button>
</div>
<hr />
<div>
<Button
onClick={() => {
setWebViewStateClicks(webViewStateClicks + 1);
}}
>
use-Web-View-State Clicks: {webViewStateClicks}
</Button>
<Button onClick={() => resetWebViewStateClicks()}>Reset clicks counter</Button>
<Button onClick={() => setDefaultClicks(randomInt())}>
Randomize default value: {defaultClicks}
</Button>
</div>
</>
Expand Down
21 changes: 12 additions & 9 deletions lib/papi-dts/papi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
/// <reference types="node" />
/// <reference types="node" />
declare module 'shared/models/web-view.model' {
import { Dispatch, SetStateAction } from 'react';
/** The type of code that defines a webview's content */
export enum WebViewContentType {
/**
Expand Down Expand Up @@ -200,8 +199,8 @@ declare module 'shared/models/web-view.model' {
*/
export type UseWebViewStateHook = <T>(
stateKey: string,
defaultStateValue: NonNullable<T>,
) => [webViewState: NonNullable<T>, setWebViewState: Dispatch<SetStateAction<NonNullable<T>>>];
defaultStateValue: T,
) => [webViewState: T, setWebViewState: (stateValue: T) => void, resetWebViewState: () => void];
/**
*
* Gets the updatable properties on this WebView's WebView definition
Expand Down Expand Up @@ -368,10 +367,15 @@ declare module 'shared/global-this.model' {
* ```
*/
var useWebViewState: UseWebViewStateHook;
/** Retrieve the value from web view state with the given 'stateKey', if it exists. */
var getWebViewState: <T>(stateKey: string) => T | undefined;
/**
* Retrieve the value from web view state with the given 'stateKey', if it exists. Otherwise
* return default value
*/
var getWebViewState: <T>(stateKey: string, defaultValue: T) => T;
/** Set the value for a given key in the web view state. */
var setWebViewState: <T>(stateKey: string, stateValue: NonNullable<T>) => void;
var setWebViewState: <T>(stateKey: string, stateValue: T) => void;
/** Remove the value for a given key in the web view state */
var resetWebViewState: (stateKey: string) => void;
var getWebViewDefinitionUpdatablePropertiesById: (
webViewId: string,
) => WebViewDefinitionUpdatableProperties | undefined;
Expand Down Expand Up @@ -3772,9 +3776,8 @@ declare module 'shared/services/settings.service' {
*
* @param key The string id of the setting for which the value is being retrieved
* @param defaultSetting The default value used for the setting if no value is available for the key
* @returns The value of the specified setting, parsed to an object. Returns `undefined` if setting
* is not present or no value is available
* @throws When defaultSetting is required but not provided
* @returns The value of the specified setting, parsed to an object. Returns default setting if
* setting does not exist
*/
const getSetting: <SettingName extends keyof SettingTypes>(
key: SettingName,
Expand Down
9 changes: 6 additions & 3 deletions src/renderer/global-this.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getModuleSimilarApiMessage } from '@shared/utils/util';
import {
getWebViewStateById,
setWebViewStateById,
resetWebViewStateById,
} from '@renderer/services/web-view-state.service';
import useWebViewState from '@renderer/hooks/use-web-view-state.hook';
import * as papiReact from '@renderer/services/papi-frontend-react.service';
Expand Down Expand Up @@ -79,8 +80,9 @@ declare global {
var SillsdevScripture: SillsdevScriptureType;
var webViewRequire: WebViewRequire;
// Web view state functions are used in the default imports for each webview in web-view.service.ts
var getWebViewStateById: <T>(id: string, stateKey: string) => T | undefined;
var setWebViewStateById: <T>(id: string, stateKey: string, stateValue: NonNullable<T>) => void;
var getWebViewStateById: <T>(id: string, stateKey: string, defaultValue: T) => T;
var setWebViewStateById: <T>(id: string, stateKey: string, stateValue: T) => void;
var resetWebViewStateById: (id: string, stateKey: string) => void;
}
/* eslint-enable */

Expand All @@ -105,9 +107,10 @@ globalThis.ReactDOMClient = ReactDOMClient;
globalThis.createRoot = ReactDOMClient.createRoot;
globalThis.SillsdevScripture = SillsdevScripture;
globalThis.webViewRequire = webViewRequire;
// We don't expose get/setWebViewStateById in PAPI because web views don't have access to IDs
// We don't expose get/setWebViewStateById/resetWebViewStateById in PAPI because web views don't have access to IDs
globalThis.getWebViewStateById = getWebViewStateById;
globalThis.setWebViewStateById = setWebViewStateById;
globalThis.resetWebViewStateById = resetWebViewStateById;
// We store the hook reference because we need it to bind it to the webview's iframe 'window' context
globalThis.useWebViewState = useWebViewState;

Expand Down
39 changes: 29 additions & 10 deletions src/renderer/hooks/use-web-view-state.hook.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
import { useState, useEffect, Dispatch, SetStateAction } from 'react';
import { useState, useCallback, useEffect } from 'react';

// We don't add this to PAPI directly like other hooks because `this` has to be bound to a web view's iframe context
/** See `web-view.model.ts` for normal hook documentation */
export default function useWebViewState<T>(
this: {
getWebViewState: (stateKey: string) => T | undefined;
setWebViewState: (stateKey: string, stateValue: NonNullable<T>) => void;
getWebViewState: (stateKey: string, defaultValue: T) => T;
setWebViewState: (stateKey: string, stateValue: T) => void;
resetWebViewState: (stateKey: string) => void;
},
stateKey: string,
defaultStateValue: NonNullable<T>,
): [webViewState: NonNullable<T>, setWebViewState: Dispatch<SetStateAction<NonNullable<T>>>] {
const [state, setState] = useState(() => this.getWebViewState(stateKey) ?? defaultStateValue);
defaultStateValue: T,
): [webViewState: T, setWebViewState: (newStateValue: T) => void, resetWebViewState: () => void] {
const [state, setStateInternal] = useState(() =>
this.getWebViewState(stateKey, defaultStateValue),
);

// Whenever the state changes, save the updated value
useEffect(() => {
this.setWebViewState(stateKey, state);
}, [stateKey, state]);
if (
this.getWebViewState(stateKey, defaultStateValue) === defaultStateValue &&
state !== defaultStateValue
)
setStateInternal(defaultStateValue);
}, [defaultStateValue, state, stateKey]);

return [state, setState];
const setState = useCallback(
(newStateValue: T) => {
setStateInternal(newStateValue);
this.setWebViewState(stateKey, newStateValue);
},
[stateKey],
);

const resetState = useCallback(() => {
setStateInternal(defaultStateValue);
this.resetWebViewState(stateKey);
}, [defaultStateValue, stateKey]);

return [state, setState, resetState];
}
20 changes: 17 additions & 3 deletions src/renderer/services/web-view-state.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,15 @@ export function setFullWebViewStateById(id: string, state: Record<string, unknow
*
* @param id ID of the web view
* @param stateKey Key used to retrieve the state value
* @returns String (if it exists) containing the state for the given key of the given web view
* @returns String (if it exists) containing the state for the given key of the given web view.
* Otherwise default value is returned.
*/
export function getWebViewStateById<T>(id: string, stateKey: string): T | undefined {
export function getWebViewStateById<T>(id: string, stateKey: string, defaultValue: T): T {
if (!id || !stateKey) throw new Error('id and stateKey must be provided to get webview state');
const state = getRecord(id);
// We don't have any way to know what type this is, so just type assert for convenience
// eslint-disable-next-line no-type-assertion/no-type-assertion
return state[stateKey] as T | undefined;
return stateKey in state ? (state[stateKey] as T) : defaultValue;
}

/**
Expand All @@ -97,6 +98,19 @@ export function setWebViewStateById<T>(id: string, stateKey: string, stateValue:
save();
}

/**
* Remove the web view state object associated with the given ID
*
* @param id ID of the web view
* @param stateKey Key for the associated state
*/
export function resetWebViewStateById(id: string, stateKey: string): void {
if (!id || !stateKey) throw new Error('id and stateKey must be provided to remove webview state');
const state = getRecord(id);
delete state[stateKey];
save();
}

/**
* Purge any web view state that hasn't been touched since the process has been running. Only call
* this once all web views have been loaded.
Expand Down
6 changes: 4 additions & 2 deletions src/renderer/services/web-view.service-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,7 @@ export const getWebView = async (
// The web view provider might have updated the web view state, so save it
setFullWebViewStateById(webView.id, webView.state);

// `webViewRequire`, `getWebViewStateById`, and `setWebViewStateById` below are defined in `src\renderer\global-this.model.ts`
// `webViewRequire`, `getWebViewStateById`, `setWebViewStateById` and `resetWebViewStateById` below are defined in `src\renderer\global-this.model.ts`
// `useWebViewState` below is defined in `src\shared\global-this.model.ts`
// We have to bind `useWebViewState` to the current `window` context because calls within PAPI don't have access to a webview's `window` context
/**
Expand All @@ -862,8 +862,10 @@ export const getWebView = async (
var require = window.parent.webViewRequire;
var getWebViewStateById = window.parent.getWebViewStateById;
var setWebViewStateById = window.parent.setWebViewStateById;
window.getWebViewState = (stateKey) => { return getWebViewStateById('${webView.id}', stateKey) };
var resetWebViewStateById = window.parent.resetWebViewStateById;
window.getWebViewState = (stateKey, defaultValue) => { return getWebViewStateById('${webView.id}', stateKey, defaultValue) };
window.setWebViewState = (stateKey, stateValue) => { setWebViewStateById('${webView.id}', stateKey, stateValue) };
window.resetWebViewState = (stateKey) => { resetWebViewStateById('${webView.id}', stateKey) };
window.useWebViewState = window.parent.useWebViewState.bind(window);
var getWebViewDefinitionUpdatablePropertiesById = window.parent.getWebViewDefinitionUpdatablePropertiesById;
window.getWebViewDefinitionUpdatableProperties = () => { return getWebViewDefinitionUpdatablePropertiesById('${webView.id}')}
Expand Down
11 changes: 8 additions & 3 deletions src/shared/global-this.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,15 @@ declare global {
var webViewComponent: FunctionComponent<WebViewProps>;
/** JSDOC DESTINATION UseWebViewStateHook */
var useWebViewState: UseWebViewStateHook;
/** Retrieve the value from web view state with the given 'stateKey', if it exists. */
var getWebViewState: <T>(stateKey: string) => T | undefined;
/**
* Retrieve the value from web view state with the given 'stateKey', if it exists. Otherwise
* return default value
*/
var getWebViewState: <T>(stateKey: string, defaultValue: T) => T;
/** Set the value for a given key in the web view state. */
var setWebViewState: <T>(stateKey: string, stateValue: NonNullable<T>) => void;
var setWebViewState: <T>(stateKey: string, stateValue: T) => void;
/** Remove the value for a given key in the web view state */
var resetWebViewState: (stateKey: string) => void;
// Web view "by id" functions are used in the default imports for each webview in web-view.service.ts
// but probably wouldn't be used in a webview
// TODO: Find a way to move this to `@renderer/global-this.model.ts` without causing an error on
Expand Down
6 changes: 2 additions & 4 deletions src/shared/models/web-view.model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { Dispatch, SetStateAction } from 'react';

/** The type of code that defines a webview's content */
export enum WebViewContentType {
/**
Expand Down Expand Up @@ -220,8 +218,8 @@ export type WebViewDefinitionUpdateInfo = Partial<WebViewDefinitionUpdatableProp
*/
export type UseWebViewStateHook = <T>(
stateKey: string,
defaultStateValue: NonNullable<T>,
) => [webViewState: NonNullable<T>, setWebViewState: Dispatch<SetStateAction<NonNullable<T>>>];
defaultStateValue: T,
) => [webViewState: T, setWebViewState: (stateValue: T) => void, resetWebViewState: () => void];

// Note: the following comment uses @, not the actual @ character, to hackily provide @param and
// such on this type. It seem that, for some reason, JSDoc does not carry these annotations on
Expand Down
10 changes: 3 additions & 7 deletions src/shared/services/settings.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,8 @@ const onDidUpdateSettingEmitters = new Map<
*
* @param key The string id of the setting for which the value is being retrieved
* @param defaultSetting The default value used for the setting if no value is available for the key
* @returns The value of the specified setting, parsed to an object. Returns `undefined` if setting
* is not present or no value is available
* @throws When defaultSetting is required but not provided
* @returns The value of the specified setting, parsed to an object. Returns default setting if
* setting does not exist
*/
const getSetting = <SettingName extends SettingNames>(
key: SettingName,
Expand All @@ -42,10 +41,7 @@ const getSetting = <SettingName extends SettingNames>(
if (settingString !== null) {
return deserialize(settingString);
}
if (defaultSetting) {
return defaultSetting;
}
throw new Error(`No default value provided for setting '${key}'`);
return defaultSetting;
};

/**
Expand Down

0 comments on commit 054c2dd

Please sign in to comment.