Skip to content

Commit

Permalink
impl(lazy-initialize): set content for handlePrint() and handleClick(…
Browse files Browse the repository at this point in the history
…) via second optional argument using [content] (#679)
  • Loading branch information
isocroft authored Feb 9, 2024
1 parent 4b3ed1a commit 3cc32aa
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 15 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"typescript.tsdk": "./node_modules/typescript/lib"
"typescript.tsdk": "./node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ The component accepts the following props:
| Name | Type | Description |
| :-------------------: | :------- | :---------------------------------------------------------------------------------------------------------------------------------- |
| **`bodyClass?`** | `string` | One or more class names to pass to the print window, separated by spaces |
| **`content`** | `function` | A function that returns a component reference value. The content of this reference value is then used for print |
| **`content?`** | `function` | A function that returns a component reference value. The content of this reference value is then used for print |
| **`copyStyles?`** | `boolean` | Copy all `<style>` and `<link type="stylesheet" />` tags from `<head>` inside the parent window into the print window. (default: `true`) |
| **`documentTitle?`** | `string` | Set the title for printing when saving as a file |
| **`fonts?`** | `{ family: string, source: string; weight?: string; style?: string; }[]` | You may optionally provide a list of fonts which will be loaded into the printing iframe. This is useful if you are using custom fonts |
Expand Down Expand Up @@ -163,6 +163,34 @@ const Example = () => {
};
```

### Calling from functional components with [useReactToPrint](https://reactjs.org/docs/hooks-intro.html) using `content` passed to `handlePrint()`

```jsx
import React, { useRef } from 'react';
import { useReactToPrint } from 'react-to-print';

export const AnotherExample = () => {
const contentToPrint = useRef(null);
const handlePrint = useReactToPrint({
documentTitle: "Print This Document",
onBeforePrint: () => console.log("before printing..."),
onAfterPrint: () => console.log("after printing..."),
removeAfterPrint: true,
});

return (
<>
<div ref={contentToPrint}>Hello Again</div>
<button onClick={() => {
handlePrint(null, () => contentToPrint.current);
}}>
PRINT
</button>
</>
)
}
```

**Note ([401](https://github.com/gregnb/react-to-print/issues/401)):** In TypeScript, if you encounter `componentRef.current` error such as: `Type 'undefined' is not assignable to type 'ReactInstance | null'.`, add `null` inside the `useRef()`:

```ts
Expand Down
67 changes: 67 additions & 0 deletions examples/FunctionalComponentWithHookAndLazyContent/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as React from "react";

import { ComponentToPrint } from "../ComponentToPrint";
import { useReactToPrint } from "../../src/index";
import { CUSTOM_FONTS } from "../fonts";

export const FunctionalComponentWithHookAndLazyContent = () => {
const componentRef = React.useRef(null);

const onBeforeGetContentResolve = React.useRef<(() => void) | null>(null);

const [loading, setLoading] = React.useState(false);
const [text, setText] = React.useState("Some cool text from the parent");

const handleAfterPrint = React.useCallback(() => {
console.log("`onAfterPrint` called"); // tslint:disable-line no-console
}, []);

const handleBeforePrint = React.useCallback(() => {
console.log("`onBeforePrint` called"); // tslint:disable-line no-console
}, []);

const handleOnBeforeGetContent = React.useCallback(() => {
console.log("`onBeforeGetContent` called"); // tslint:disable-line no-console
setLoading(true);
setText("Loading new text...");

return new Promise<void>((resolve) => {
onBeforeGetContentResolve.current = resolve;

setTimeout(() => {
setLoading(false);
setText("New, Updated Text!");
resolve();
}, 2000);
});
}, [setLoading, setText]);

const reactToPrintContent = React.useCallback(() => {
return componentRef.current;
}, [componentRef.current]);

const handlePrint = useReactToPrint({
documentTitle: "SuperFileName",
onBeforeGetContent: handleOnBeforeGetContent,
onBeforePrint: handleBeforePrint,
onAfterPrint: handleAfterPrint,
removeAfterPrint: true,
fonts: CUSTOM_FONTS
});

React.useEffect(() => {
if (text === "New, Updated Text!" && typeof onBeforeGetContentResolve.current === "function") {
onBeforeGetContentResolve.current();
}
}, [onBeforeGetContentResolve.current, text]);

return (
<div>
{loading && <p className="indicator">onBeforeGetContent: Loading...</p>}
<button onClick={() => handlePrint(null, reactToPrintContent)}>
Print using a Functional Component with the useReactToPrint hook without `content` option
</button>
<ComponentToPrint ref={componentRef} text={text} />
</div>
);
};
3 changes: 3 additions & 0 deletions examples/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ClassComponentText } from "./ClassComponentText";
import { CustomPrint } from "./CustomPrint";
import { FunctionalComponent } from "./FunctionalComponent";
import { FunctionalComponentWithHook } from "./FunctionalComponentWithHook";
import { FunctionalComponentWithHookAndLazyContent } from "./FunctionalComponentWithHookAndLazyContent";
import { FunctionalComponentWithFunctionalComponentToPrint } from './FunctionalComponentWithFunctionalComponentToPrint';
import "./styles/index.css";

Expand Down Expand Up @@ -44,10 +45,12 @@ class Example extends React.Component<Props, State> {
<TabList>
<Tab>Standard</Tab>
<Tab>With Hook</Tab>
<Tab>With Hook And Lazy Content</Tab>
<Tab>With a functional ComponentToPrint</Tab>
</TabList>
<TabPanel><FunctionalComponent /></TabPanel>
<TabPanel><FunctionalComponentWithHook /></TabPanel>
<TabPanel><FunctionalComponentWithHookAndLazyContent /></TabPanel>
<TabPanel><FunctionalComponentWithFunctionalComponentToPrint /></TabPanel>
</Tabs>
</TabPanel>
Expand Down
68 changes: 55 additions & 13 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ const contextEnabled = Object.prototype.hasOwnProperty.call(React, "createContex
const hooksEnabled = Object.prototype.hasOwnProperty.call(React, "useMemo") && Object.prototype.hasOwnProperty.call(React, "useCallback");

export interface IPrintContextProps {
handlePrint: () => void,
handlePrint: (event: unknown, content?: (() => React.ReactInstance | null)) => void,
}
const PrintContext = contextEnabled ? React.createContext({} as IPrintContextProps) : null;
export const PrintContextConsumer = PrintContext ? PrintContext.Consumer : () => null;

export interface ITriggerProps<T> {
onClick: () => void;
onClick: (event: unknown) => void;
ref: (v: T) => void;
}

Expand All @@ -25,6 +25,26 @@ type Font = {

type PropertyFunction<T> = () => T;

/**
* This function helps to curry arguments to a bound function
* and partially apply them at the end of the argument list.
*
* @param {Object} thisArg
* @param {Function} callback
* @param {Array.<*>} predefinedArgs
*
* @returns {*}
*/
function wrapCallbackWithArgs<CallbackReturnValue, BoundObject>(
thisArg: BoundObject,
callback: (...curryArgs: any[]) => CallbackReturnValue,
...predefinedArgs: unknown[]
) {
return function (...args: unknown[]) {
return callback.apply(thisArg, [...args, ...predefinedArgs]);
};
}

// NOTE: https://github.com/Microsoft/TypeScript/issues/23812
const defaultProps = {
copyStyles: true,
Expand All @@ -50,7 +70,7 @@ export interface IReactToPrintProps {
bodyClass?: string;
children?: React.ReactNode;
/** Content to be printed */
content: () => React.ReactInstance | null;
content?: () => React.ReactInstance | null;
/** Copy styles over into print window. default: true */
copyStyles?: boolean;
/**
Expand Down Expand Up @@ -186,7 +206,13 @@ export default class ReactToPrint extends React.Component<IReactToPrintProps> {
}
}

public handleClick = () => {

public handleClick (
/* eslint-disable-next-line no-unused-vars */
// @ts-expect-error variable is unused
event: unknown,
content?: (() => React.ReactInstance | null)
) {
const {
onBeforeGetContent,
onPrintError,
Expand All @@ -196,21 +222,21 @@ export default class ReactToPrint extends React.Component<IReactToPrintProps> {
const onBeforeGetContentOutput = onBeforeGetContent();
if (onBeforeGetContentOutput && typeof onBeforeGetContentOutput.then === "function") {
onBeforeGetContentOutput
.then(this.handlePrint)
.then(() => this.handlePrint(content))
.catch((error: Error) => {
if (onPrintError) {
onPrintError("onBeforeGetContent", error);
}
});
} else {
this.handlePrint();
this.handlePrint(content);
}
} else {
this.handlePrint();
this.handlePrint(content);
}
}

public handlePrint = () => {
public handlePrint = (optionalContent?: (() => React.ReactInstance | null)) => {
const {
bodyClass,
content,
Expand All @@ -220,7 +246,11 @@ export default class ReactToPrint extends React.Component<IReactToPrintProps> {
nonce,
} = this.props;

const contentEl = content();
let contentEl = typeof optionalContent === "function" ? optionalContent() : null;

if (!contentEl && typeof content === "function") {
contentEl = content();
}

if (contentEl === undefined) {
this.logMessages(['To print a functional component ensure it is wrapped with `React.forwardRef`, and ensure the forwarded ref is used. See the README for an example: https://github.com/gregnb/react-to-print#examples']);
Expand Down Expand Up @@ -566,7 +596,7 @@ export default class ReactToPrint extends React.Component<IReactToPrintProps> {

if (trigger) {
return React.cloneElement(trigger(), {
onClick: this.handleClick,
onClick: this.handleClick.bind(this),
});
} else {
if (!PrintContext) {
Expand All @@ -575,7 +605,9 @@ export default class ReactToPrint extends React.Component<IReactToPrintProps> {
return null;
}

const value = { handlePrint: this.handleClick };
const value = {
handlePrint: this.handleClick.bind(this)
};

return (
<PrintContext.Provider value={value as IPrintContextProps}>
Expand All @@ -586,7 +618,10 @@ export default class ReactToPrint extends React.Component<IReactToPrintProps> {
}
}

type UseReactToPrintHookReturn = () => void;
type UseReactToPrintHookReturn = (
event: unknown,
content?: (() => React.ReactInstance | null)
) => void;

export const useReactToPrint = (props: IReactToPrintProps): UseReactToPrintHookReturn => {
if (!hooksEnabled) {
Expand All @@ -605,5 +640,12 @@ export const useReactToPrint = (props: IReactToPrintProps): UseReactToPrintHookR
[props]
);

return React.useCallback(() => reactToPrint.handleClick(), [reactToPrint]);
return React.useCallback(
(event: unknown, content?: (() => React.ReactInstance | null)) => {
/* eslint-disable-next-line @typescript-eslint/unbound-method */
const triggerPrint = wrapCallbackWithArgs(reactToPrint, reactToPrint.handleClick, content);
// NOTE: `event` is a no-use argument
// (necessary for backward compatibility with older versions)
return triggerPrint(event);
}, [reactToPrint]);
};

0 comments on commit 3cc32aa

Please sign in to comment.