Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into main-v4
Browse files Browse the repository at this point in the history
  • Loading branch information
IanEdington committed Feb 2, 2023
2 parents 98b575c + e8befa5 commit 5debbfc
Show file tree
Hide file tree
Showing 31 changed files with 408 additions and 389 deletions.
File renamed without changes.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,26 @@ There is an awesome library [mjml](https://mjml.io/) with github repo here [http
`MJML` is a markup language created by [Mailjet](https://www.mailjet.com/).
So in order to create emails on the fly we created a library with `React` components.

## Get started today with V2 - 100% backwards compatible with https://github.com/wix-incubator/mjml-react/

V2 is a drop in replacement for https://github.com/wix-incubator/mjml-react/, with some additional support. If there's a component missing you can make a PR against https://github.com/Faire/mjml-react/tree/main-v2 or submit an issue and we'll try to unblock you.

## What's new in V3?

We wanted V3 of mjml-react to be fairly easy to migrate to from V2. We will implement more advanced features in V4. The main updates in V3 include:

- Typescript: mjml-react is now written in typescript which helps ensure correct props are passed to mjml components
- Full mjml component support: We use an automated script for pulling mjml components and creating a corresponding mjml-react component. This means we get full support of all components available in the latest mjml version
- Other small changes: add dangerouslySetInnerHTML in mjml-react for mjml ending tags, update testing, add in-code documenation

## What's coming in V4?

In V4 we are exploring exciting features that will make mjml-react even more powerful. This includes:

- Improved prop type safety: help ensure correct formatting for props like padding, width, and height

If you want to be on the cutting edge of what is being released, we are publishing a [v4-main-alpha version](https://www.npmjs.com/package/@faire/mjml-react/v/main-alpha) to npm.

## How it works

Install the required dependencies first:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
export const ATTRIBUTES_TO_USE_CSSProperties_WITH = new Set([
"color",
"textDecoration",
"textTransform",

"border",
"borderRadius",
"borderColor",
"borderStyle",

"backgroundColor",
"backgroundPosition",
"backgroundSize",
]);

/**
* Converts an mjml type definition into a typescript type definition
* Handles boolean, integer, enum, and
* This is used to generate the types on the React <> mjml binding component.
*/
export function getPropTypeFromMjmlAttributeType(
attribute: string,
mjmlAttributeType: string
): string {
if (mjmlAttributeType === "boolean") {
return "boolean";
}
if (mjmlAttributeType === "integer") {
return "number";
}
// e.g. "vertical-align": "enum(top,bottom,middle)"
if (mjmlAttributeType.startsWith("enum(")) {
return transformEnumType(mjmlAttributeType);
}
if (ATTRIBUTES_TO_USE_CSSProperties_WITH.has(attribute)) {
// When possible prefer using the CSSProperties definitions over the
// less helpful "string" or "string | number" type definitions.
return `React.CSSProperties["${attribute}"]`;
}
if (
mjmlAttributeType.startsWith("unit") &&
mjmlAttributeType.includes("px")
) {
return "string | number";
}
return "string";
}

/**
* Converts an mjml enum type definition into a typescript string literal type.
* Strings like `"enum(a,b,c)"` become `"a" | "b" | "c"`.
* This is used to generate the types on the React <> mjml binding component.
*/
function transformEnumType(mjmlAttributeType: string): string {
return (
mjmlAttributeType
.match(/\(.*\)/)?.[0]
?.slice(1, -1)
.split(",")
.map((str) => '"' + str + '"')
.join(" | ") ?? "unknown"
);
}
76 changes: 12 additions & 64 deletions scripts/generate-mjml-react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import camelCase from "lodash.camelcase";
import upperFirst from "lodash.upperfirst";
import * as path from "path";

import { typeToUnit } from "../src/utils";
import { getPropTypeFromMjmlAttributeType } from "./generate-mjml-react-utils/getPropTypeFromMjmlAttributeType";

const MJML_REACT_DIR = "src";

Expand All @@ -25,7 +25,7 @@ const GENERATED_HEADER_TSX = `
*/
`;

interface IMjmlComponent {
export interface IMjmlComponent {
componentName: string;
allowedAttributes?: Record<string, string>;
defaultAttributes?: Record<string, string>;
Expand Down Expand Up @@ -54,30 +54,14 @@ const MJML_COMPONENT_NAMES = MJML_COMPONENTS_TO_GENERATE.map(
(component) => component.componentName
);

const ATTRIBUTES_TO_USE_CSSProperties_WITH = new Set([
"color",
"textAlign",
"verticalAlign",
"textDecoration",
"textTransform",

"border",
"borderRadius",
"borderColor",
"borderStyle",

"backgroundColor",
"backgroundPosition",
"backgroundRepeat",
"backgroundSize",
]);

const TYPE_OVERRIDE: { [componentName: string]: { [prop: string]: string } } = {
mjml: { owa: "string", lang: "string" },
"mj-style": { inline: "boolean" },
"mj-class": { name: "string" },
"mj-table": { cellspacing: "string", cellpadding: "string" },
"mj-selector": { path: "string" },
"mj-section": { fullWidth: "boolean" },
"mj-wrapper": { fullWidth: "boolean" },
"mj-html-attribute": { name: "string" },
"mj-include": { path: "string" },
"mj-breakpoint": { width: "string" },
Expand Down Expand Up @@ -119,45 +103,6 @@ const ALLOW_ANY_PROPERTY = new Set(
)
);

function getPropTypeFromMjmlAttributeType(
attribute: string,
mjmlAttributeType: string
): string {
if (attribute === "fullWidth") {
return "boolean";
}
if (ATTRIBUTES_TO_USE_CSSProperties_WITH.has(attribute)) {
return `React.CSSProperties["${attribute}"]`;
}
if (mjmlAttributeType.startsWith("unit")) {
const validUnitTypes: string[] = Object.keys(typeToUnit).filter((type) =>
mjmlAttributeType.includes(typeToUnit[type as keyof typeof typeToUnit])
);
if (mjmlAttributeType.endsWith("{1,4}")) {
return `Matrix<${validUnitTypes.join(" | ")}>`;
}
return validUnitTypes.join(" | ");
}
if (mjmlAttributeType === "boolean") {
return "boolean";
}
if (mjmlAttributeType === "integer") {
return "number";
}
// e.g. "vertical-align": "enum(top,bottom,middle)"
if (mjmlAttributeType.startsWith("enum")) {
return (
mjmlAttributeType
.match(/\(.*\)/)?.[0]
?.slice(1, -1)
.split(",")
.map((str) => "'" + str + "'")
.join(" | ") ?? "unknown"
);
}
return "string";
}

function buildTypesForComponent(mjmlComponent: IMjmlComponent): string {
const {
componentName,
Expand Down Expand Up @@ -228,17 +173,13 @@ function buildFileContents({
}) {
const { props, createElementProps } =
buildReactCreateElementProps(componentName);
const unitsUsed = ["Matrix", ...Object.keys(typeToUnit)]
.filter((unit) => types.includes(unit))
.join(", ");
const unitsImports = unitsUsed ? ", " + unitsUsed : "";

return `
${GENERATED_HEADER_TSX}
import React from "react";
import { convertPropsToMjmlAttributes${unitsImports} } from "../${UTILS_FILE}";
import { convertPropsToMjmlAttributes } from "../${UTILS_FILE}";
export interface I${reactName}Props {
${types}
Expand All @@ -257,6 +198,13 @@ function buildReactCreateElementProps(componentName: string): {
const withChildren = "{children, ...props}";
const withoutChildren = "props";

if (componentName === "mj-style") {
return {
props: withChildren,
createElementProps:
"{ ...convertPropsToMjmlAttributes(props), dangerouslySetInnerHTML: { __html: props.dangerouslySetInnerHTML ? props.dangerouslySetInnerHTML.__html : children } }",
};
}
if (HAS_CHILDREN.has(componentName)) {
return {
props: withChildren,
Expand Down
21 changes: 8 additions & 13 deletions src/mjml/MjmlAccordion.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions src/mjml/MjmlAccordionElement.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 9 additions & 15 deletions src/mjml/MjmlAccordionText.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 7 additions & 12 deletions src/mjml/MjmlAccordionTitle.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/mjml/MjmlBody.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 5debbfc

Please sign in to comment.