Recently I took on the challenge of building a React app that utilizes Typescript & Material UI by using the Liferay-JS Yeoman generator. This approach was taken as an alternative to using Create React App & adapting it to Liferay. The benefit is there are no extra steps needed to gain access to the Liferay Params (configuration, contextPath, portletElementId, & portletNamespace) as there would be in an CRA adaptation.
Here are the steps I have found that, at the time of writing this blog, are working for me…
**NOTE – You must use NPM & not Yarn. Using Yarn causes errors when bundling MUI components. Also, this is written using DXP 7.3**
1. Create a React App using the yeoman liferay-js generator. If you have not globally installed yeoman & the generator-liferay-js npm packages, follow the links & instructions below.
2. Once the App has been created install the following dependencies:
- Dev dependencies
- @babel/preset-typescript
- liferay-npm-bundler-plugin-inject-imports-dependencies
npm i -D @babel/preset-typescript liferay-npm-bundler-plugin-inject-imports-dependencies
- Standard dependencies
- typescript
- @types/node
- @types/react
- @types/react-dom
- @types/jest (optional for testing)
- @types/material-ui
- @material-ui/core
- @material-ui/icons
- @material-ui/styles
- @material-ui/lab (optional)
npm i typescript @types/node @types/react @types/react-dom @types/jest @types/material-ui @material-ui/core @material-ui/icons @material-ui/styles @material-ui/lab
3. Run ‘tsc –init’ in your root folder to create a tsconfig.json. The following tsconfig.json works at the time of writing this:
{
"compilerOptions": {
"target": "ES5",
"module": "ESNext",
"lib": [
"es6",
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"checkJs": false,
"jsx": "react-jsx",
"declaration": true,
"noEmit": true,
"isolatedModules": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitThis": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
]
}
4. Update your .babelrc to the following:
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
[
"@babel/preset-typescript",
{
"isTSX": true,
"allExtensions": true
}
]
]
}
5. Update your .npmbundlerrc to the following. This is so Material UI components will work w/ the Liferay generated bundle:
{
"create-jar": {
"output-dir": "dist",
"features": {
"js-extender": true,
"web-context": "/exercise1.1",
"localization": "features/localization/Language",
"configuration": "features/configuration.json"
}
},
"dump-report": true,
"*": {
"plugins": ["inject-imports-dependencies"]
}
}
6. Update the build script & add the check-types script to your package.json. Adding ‘check types’ will make sure there are no TS errors before Babel complies. If there are errors the compile step will not happen:
"build": "npm run check-types && npx babel src -d build src --extensions '.ts,.tsx' && npm run copy-assets && liferay-npm-bundler",
"check-types": "tsc",
7. You should now be able to use Typescript & Material UI in your Liferay generated React App. Rename your index & AppComonent files from .js to .tsx. You will have to create types for the Liferay generated props. As a shortcut here is my index.tsx file:
import React from 'react';
import ReactDOM from 'react-dom';
import AppComponent from './AppComponent';
interface IProps {
portletNamespace: string;
contextPath: string;
portletElementId: string;
configuration: any;
}
/**
* This is the main entry point of the portlet.
*
* See https://tinyurl.com/js-ext-portlet-entry-point for the most recent
* information on the signature of this function.
*
* @param {Object} params a hash with values of interest to the portlet
* @return {void}
*/
export default function main({portletNamespace, contextPath, portletElementId, configuration}: IProps){
ReactDOM.render(
<App
portletNamespace={portletNamespace}
contextPath={contextPath}
portletElementId={portletElementId}
configuration={configuration}
/>,
document.getElementById(portletElementId)
);
}
And AppComponent.tsx:
import React from 'react';
declare global {
interface Window {
Liferay: any;
}
}
interface IProps {
portletNamespace: string;
contextPath: string;
portletElementId: string;
configuration: any;
}
const Liferay: any = window.Liferay;
const AppComponent: React.FC<IProps> = (props: IProps) => {
return (
<div>
<div>
<span className="tag">{Liferay.Language.get('portlet-namespace')}:</span>
<span className="value">{props.portletNamespace}</span>
</div>
<div>
<span className="tag">{Liferay.Language.get('context-path')}:</span>
<span className="value">{props.contextPath}</span>
</div>
<div>
<span className="tag">{Liferay.Language.get('portlet-element-id')}:</span>
<span className="value">{props.portletElementId}</span>
</div>
<div>
<span className="tag">{Liferay.Language.get('configuration')}:</span>
<span className="value pre">{JSON.stringify(props.configuration, null, 2)}</span>
</div>
</div>
);
}
export default AppComponent;
8. Now deploy the app to your local Liferay & if everything was setup right you should see something akin to the following (names will be different) after adding your new app to a content page:
9. At this point, you can start building your app & adding Material UI components. As stated in the note at the top make sure you are using npm & not yarn. Somewhere in your node_modules yarn installs a second instance of React which causes an Invariant Violation: Invalid hook call error after compiling the code & running in Liferay.
References:
Babel preset typescript:
https://babeljs.io/docs/en/babel-preset-typescript
Babelrc config for TS:
https://stackoverflow.com/questions/63143214/compiling-tsx-to-js-with-babel-not-correct
Creating a ‘check-types’ script & other Babel/TS info:
https://iamturns.com/typescript-babel
How to implement material-ui on Liferay:
https://cnpmjs.org/package/liferay-npm-bundler-plugin-inject-imports-dependencies
Material UI w/ TS:
https://material-ui.com/guides/typescript
Use NPM & not Yarn:
https://github.com/mui-org/material-ui/issues/12934#issuecomment-435016750