Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

impl(lazy-initialize): set content for handlePrint() and handleClick() via second optional argument using [content] #679

Merged
merged 14 commits into from
Feb 9, 2024
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={(event) => {
handlePrint(event, () => 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={(e) => handlePrint(e, 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
69 changes: 56 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: React.MouseEvent, content?: (() => React.ReactInstance | null)) => void,
MatthewHerbst marked this conversation as resolved.
Show resolved Hide resolved
}
const PrintContext = contextEnabled ? React.createContext({} as IPrintContextProps) : null;
export const PrintContextConsumer = PrintContext ? PrintContext.Consumer : () => null;

export interface ITriggerProps<T> {
onClick: () => void;
onClick: (event: React.MouseEvent) => 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,7 @@ export default class ReactToPrint extends React.Component<IReactToPrintProps> {
}
}

public handleClick = () => {
public handleClick (event: React.MouseEvent, content?: (() => React.ReactInstance | null)) {
const {
onBeforeGetContent,
onPrintError,
Expand All @@ -196,21 +216,23 @@ 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);
}

return Boolean(event);
}

public handlePrint = () => {
public handlePrint = (optionalContent?: (() => React.ReactInstance | null)) => {
const {
bodyClass,
content,
Expand All @@ -220,7 +242,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 +592,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 +601,14 @@ export default class ReactToPrint extends React.Component<IReactToPrintProps> {
return null;
}

const value = { handlePrint: this.handleClick };
const value = {
handlePrint: (
event: React.MouseEvent,
content: (() => React.ReactInstance | null)
) => {
return this.handleClick(event, content);
}
};

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

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

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

return React.useCallback(() => reactToPrint.handleClick(), [reactToPrint]);
return React.useCallback(
(event: React.MouseEvent, 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]);
};