Skip to content

Commit 857f56c

Browse files
committed
feat(jsx-email): add Conditional (mso) component (#119)
1 parent 4051b94 commit 857f56c

File tree

11 files changed

+206
-12
lines changed

11 files changed

+206
-12
lines changed

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
12

23
# npm options
34
auth-type=legacy

apps/test/fixtures/base.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
Button,
44
ColorScheme,
55
Column,
6+
Conditional,
67
Container,
78
Font,
89
Head,
@@ -22,7 +23,11 @@ import {
2223
export const Template = () => (
2324
<Html>
2425
<ColorScheme />
25-
<Head />
26+
<Head>
27+
<Conditional mso={true}>
28+
<meta content="batman" />
29+
</Conditional>
30+
</Head>
2631
<Preview>Preview Content</Preview>
2732
<Body>
2833
<Container>

apps/test/tests/.snapshots/smoke.spec.ts-page-Base-1-chromium.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
</style>
3737
</head>
3838
<body>
39+
<!--[if mso]><meta content="batman"/><![endif]-->
3940
<div style="display:none;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden">
4041
Preview Content
4142
<div>

docs/components/conditional.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
title: 'Conditional'
3+
description: Use HTML conditional comments effortlessly
4+
slug: conditional
5+
type: component
6+
---
7+
8+
<!--@include: @/include/header.md-->
9+
10+
<!--@include: @/include/install.md-->
11+
12+
## Usage
13+
14+
Add the component to your email template. Include styles where needed.
15+
16+
```jsx
17+
import { Conditional, Head } from 'jsx-email';
18+
19+
const Email = () => {
20+
return (
21+
<Head>
22+
<Conditional mso={true}>
23+
<meta content="batman" />
24+
</Conditional>
25+
</Head>
26+
);
27+
};
28+
```
29+
30+
## Component Props
31+
32+
```ts
33+
interface ConditionalProps {
34+
children?: React.ReactNode;
35+
expression?: string;
36+
mso?: boolean;
37+
}
38+
```
39+
40+
::: info
41+
The `expression` prop or the `mso` prop must be defined, but not both.
42+
:::
43+
44+
### Props
45+
46+
```ts
47+
expression?: string;
48+
```
49+
50+
If provided, the string will be used as the conditional expression within the HTML comment. e.g. a value of `lt ie 10` would result in a conditional comment block starting with `<!--[if lt ie 10]>`.
51+
52+
```ts
53+
mso?: boolean;
54+
```
55+
56+
If `true`, the conditional comment begins with `<!--[if mso]>`. If `false`, the conditional comment block uses a common hack and appears as `<!--[if !mso]><!--> ... <!--<![endif]-->`.

packages/jsx-email/src/components/body.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import type { BaseProps, JsxEmailComponent } from '../types';
22

3-
type RootProps = BaseProps<'body'>;
4-
5-
export interface BodyProps extends RootProps {}
3+
export interface BodyProps extends BaseProps<'body'> {}
64

75
export const Body: JsxEmailComponent<BodyProps> = ({ children, style, ...props }) => (
86
<body {...props} data-id="jsx-email/body" style={style}>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React, { Suspense } from 'react';
2+
3+
import { jsxToString, useData } from '../render/jsx-to-string';
4+
import type { JsxEmailComponent } from '../types';
5+
6+
export interface ConditionalProps {
7+
children?: React.ReactNode;
8+
expression?: string;
9+
mso?: boolean;
10+
}
11+
12+
const notMso = (html: string) => `<!--[if !mso]><!-->${html}<!--<![endif]-->`;
13+
14+
const comment = (expression: string, html: string) => `<!--[if ${expression}]>${html}<![endif]-->`;
15+
16+
const Renderer = (props: ConditionalProps) => {
17+
const { children, mso } = props;
18+
let { expression } = props;
19+
const html = useData(props, () => jsxToString(<>{children}</>));
20+
let innerHtml = '';
21+
22+
if (mso === false) innerHtml = notMso(html);
23+
else if (mso === true && !expression) expression = 'mso';
24+
if (expression) innerHtml = comment(expression, html);
25+
26+
// @ts-ignore
27+
// Note: This is perfectly valid. TS just expects lowercase tag names to match a specific type
28+
return <jsx-email-cond dangerouslySetInnerHTML={{ __html: innerHtml }} />;
29+
};
30+
31+
export const Conditional: JsxEmailComponent<ConditionalProps> = (props) => {
32+
const { children, expression, mso } = props;
33+
34+
if (typeof expression === 'undefined' && typeof mso === 'undefined')
35+
throw new RangeError(
36+
'jsx-email: Conditional expects the `expression` or `mso` prop to be defined'
37+
);
38+
39+
if (typeof expression !== 'undefined' && typeof mso !== 'undefined')
40+
throw new RangeError(
41+
'jsx-email: Conditional expects the `expression` or `mso` prop to be defined, not both'
42+
);
43+
44+
return (
45+
<>
46+
<Suspense fallback={<div>waiting</div>}>
47+
<Renderer {...props}>{children}</Renderer>
48+
</Suspense>
49+
</>
50+
);
51+
};
52+
53+
Conditional.displayName = 'Conditional';

packages/jsx-email/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from './components/button/button';
33
export * from './components/code';
44
export * from './components/color-scheme';
55
export * from './components/column';
6+
export * from './components/conditional';
67
export * from './components/container';
78
export * from './components/font';
89
export * from './components/head';

packages/jsx-email/src/render/process.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import type { ProcessOptions } from '../types';
88

99
export { prettyHtml };
1010

11+
export const jsxEmailTags = ['jsx-email-cond'];
12+
1113
export const processHtml = async ({ html, minify, pretty, strip }: ProcessOptions) => {
1214
const { rehype } = await import('rehype');
1315
const { default: stringify } = await import('rehype-stringify');
@@ -18,6 +20,7 @@ export const processHtml = async ({ html, minify, pretty, strip }: ProcessOption
1820
emitParseErrors: true
1921
// fragment: true
2022
};
23+
const reJsxTags = new RegExp(`<[/]?(${jsxEmailTags.join('|')})>`, 'g');
2124

2225
function rehypeMoveStyle() {
2326
return function (tree: Root) {
@@ -81,8 +84,11 @@ export const processHtml = async ({ html, minify, pretty, strip }: ProcessOption
8184
collapseEmptyAttributes: true
8285
})
8386
.process(html);
87+
8488
let result = docType + String(doc).replace('<!doctype html>', '').replace('<head></head>', '');
8589

90+
result = result.replace(reJsxTags, '');
91+
8692
if (pretty) result = prettyHtml(result);
8793

8894
return result;

packages/jsx-email/src/types.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,8 @@ export type BaseProps<TElement extends React.ElementType> =
66
disableDefaultStyle?: boolean;
77
};
88

9-
// export type BaseParentProps<TElement extends React.ElementType> = BaseProps<TElement> & {
10-
// children?: React.ReactNode | undefined;
11-
// };
12-
139
export type JsxEmailComponent<TProps extends BaseProps<any>> = React.FC<Readonly<TProps>>;
1410

15-
// export type JsxEmailParentComponent<TProps extends BaseParentProps<any> = {}> = React.FC<
16-
// Readonly<TProps>
17-
// >;
18-
1911
export type PlainTextOptions = HtmlToTextOptions;
2012

2113
export interface RenderOptions {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`<Conditional> component > renders expression 1`] = `"<!DOCTYPE html PUBLIC \\"-//W3C//DTD XHTML 1.0 Transitional//EN\\" \\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\\"><html><body><!--[if lt batman]><h1>joker</h1><![endif]--></body></html>"`;
4+
5+
exports[`<Conditional> component > renders mso: false 1`] = `"<!DOCTYPE html PUBLIC \\"-//W3C//DTD XHTML 1.0 Transitional//EN\\" \\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\\"><html><body><!--[if !mso]><!--><h1>batman</h1><!--<![endif]--></body></html>"`;
6+
7+
exports[`<Conditional> component > renders mso: true 1`] = `"<!DOCTYPE html PUBLIC \\"-//W3C//DTD XHTML 1.0 Transitional//EN\\" \\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\\"><html><body><!--[if mso]><h1>batman</h1><![endif]--></body></html>"`;
8+
9+
exports[`<Conditional> component > renders with jsxToString 1`] = `"<jsx-email-cond><!--[if mso]><h1>batman</h1><![endif]--></jsx-email-cond>"`;
10+
11+
exports[`<Conditional> component > throws on bad props 1`] = `[RangeError: jsx-email: Conditional expects the \`expression\` or \`mso\` prop to be defined]`;
12+
13+
exports[`<Conditional> component > throws on bad props 2`] = `[RangeError: jsx-email: Conditional expects the \`expression\` or \`mso\` prop to be defined, not both]`;

0 commit comments

Comments
 (0)