A complementary tutorial on how to work with Nx.dev using web components and not die trying.
Code available in GitHub
Find me on Twitter as @Fabian0007
Angular App and web components libraries created with React— Part #2 of 9
In this second part of the tutorial, we will work with the Angular application
created in the first part and this time we will create a web component library
using React. The first part of this tutorial is here.
Creating a web component library using React
We will start by adding the React capabilities, then we will create the first
web component library using React in our project:
npm i @nrwl/react
npm run nx g @nrwl/react:lib header-lib
Adding to the Angular app
The first thing that comes to mind is to add the import in
/apps/angularapp/src/main.ts:
import '@tutorial/header-lib';
Now we will run:
npm start angular app --watch
An error will appear on our console:
ERROR in libs/header-lib/src/index.ts:1:15 — error TS6142: Module ‘./lib/header-lib’ was resolved to ‘C:/Users/FCano/Documents/personal-tutorials/test-continuing/tutorial/libs/header-lib/src/lib/header-lib.tsx’, but ‘ — jsx’ is not set.
1 export * from ‘./lib/header-lib’;
We need to enable the jsx option. TypeScript ships with three JSX modes:
preserve
, react
, and react-native
. These modes only affect the emit stage - type checking is unaffected. The preserve
mode will keep the JSX as part of the output to be further consumed by another transform step (e.g.
Babel). Additionally the output will have a .jsx
file
extension. The react
mode will emit React.createElement
, does not need to go through a JSX transformation before use, and the output will have a .js
file extension. The react-native
mode is the equivalent of preserve
in that it keeps all JSX, but the output will instead have a .js
file extension[1]. For our case we will use the react
option. you must add the code marked in bold to /apps/angularapp/src/tsconfig.json:
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"types": ["node", "jest"],
}
}
An new error will appear on our console:
ERROR in libs/header-lib/src/lib/header-lib.tsx:1:19 — error TS2307: Cannot find module ‘react’.
1 import React from ‘react’;
This is very simple, without having React installed, how are we going to work
with a React application? An alternative to not having to install it would be to
compile our library from the outside and add it as a resource, but that would
complicate updating this library and that is not the idea of a monorepo. We
will install React and react-dom(Since we are working with a web application, we need to install the glue between react and the DOM) by executing the following command:
npm install react react-dom
And of course we need to install the types for these packages since we are using Typecript.
npm install — save-dev @types/react @types/react-dom
The error will disapper and we will replace the file
/apps/angularapp/src/app/app.component.html with:
<header-lib></header-lib>
<main></main>
<template id="template-test" [innerHTML]="template"> </template>
<footer-lib [attr.creator]="creator"></footer-lib>
Our header library will not be shown, and it is obvious, we are calling a web
component that does not exist, what we have for now is a React library, to make it a web component we have two options each with its own advantages and disadvantages.
Do it yourself
React and Web
Components are built to solve different problems. Web Components provide strong encapsulation for reusable components, while React provides a declarative library that keeps the DOM in sync with your data. The two goals are complementary. As a developer, you are free to use React in your Web Components, or to use Web Components in React, or both[2]. Our need at the moment is to encapsulate the react library that we already have in a web component.
First we will change the name of /libs/footer-lib /src/lib/header-lib.tsx to
ReactHeader.tsx and we will replace the content with:
import * as React from ‘react’;
import ‘./header-lib.css’;
/* eslint-disable-next-line */
export interface ReactHeaderProps {}
export const ReactHeader = (props: ReactHeaderProps) => {
return (
<div>
<h1>Welcome to header-lib component!</h1>
</div>
);
};
export default ReactHeader;
Now we will create on the same folder the file header-lib.tsx and will replace
with:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { ReactHeader } from './ReactHeader';
export class HeaderLib extends HTMLElement {
public mountPoint: HTMLDivElement;
connectedCallback() {
this.mountReactApp();
}
disconnectedCallback() {
ReactDOM.unmountComponentAtNode(this.mountPoint);
}
mountReactApp() {
if (!this.mountPoint) {
this.mountPoint = document.createElement('div');
this.appendChild(this.mountPoint);
}
ReactDOM.render(<ReactHeader />, this.mountPoint);
}
}
customElements.define('header-lib', HeaderLib);
Here we are using the life cycle of the web components (connectedCallback and disconnectedCallback) and ReactDOM for render a React component over a web component.
Our component not is using the ShadowDOM(see the first part of this tutorial), so the h1 label is centered. To use it we must add the constructor and we will attach a shadow DOM tree to the web component “this.attachShadow({ mode: ‘open’})”, it must be open because we need to access to shadowRoot property of the web component, then we must to append the div element to the shadowRoot property instead of over the web component.
constructor() {
super();
this.attachShadow({ mode: 'open' })
}
this.appendChild(this.mountPoint); -> this.shadowRoot.appendChild(this.mountPoint);
Now if we want to add the style, we need to add it as a inline style, because
the shadowDOM doesnt we permit to use the external css, we can to use
style-it for insert the css directly
in the DOM, but we need to be able to export the css as a module, so I invite
you to review this on your own.
import * as React from 'react';
import './header-lib.css';
/* eslint-disable-next-line */
export interface ReactHeaderProps {}
export const ReactHeader = (props: ReactHeaderProps) => {
return (
</div>
);
};
export default ReactHeader;
Now if we want to pass a parameter, the first is update the React component:
import * as React from 'react';
import './header-lib.css';
/* eslint-disable-next-line */
export interface ReactHeaderProps {
creator: string
}
export const ReactHeader = ({ creator }: ReactHeaderProps) => {
return (
<div style={display: 'flex', alignItems: 'center', justifyContent: 'center'}>
<img
alt="Nx logo"
width="75"
src="https://nx.dev/assets/images/nx-logo-white.svg"
/>
<h1>Header {creator}</h1>
</div>
);
};
export default ReactHeader;
In header-lib.tsx we must add the attributeChangedCallback lifecycle to get the attribute “creator” and pass it to the mountReactApp function where we will pass that value to the React component.
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { ReactHeader } from './ReactHeader';
export class HeaderLib extends HTMLElement {
public mountPoint: HTMLDivElement;
public static observedAttributes = ['creator'];
constructor() {
super();
this.attachShadow({ mode: 'open' })
}
connectedCallback() {
this.mountReactApp('');
}
attributeChangedCallback() {
const creator: string = this.getAttribute('creator');
this.mountReactApp(creator);
}
disconnectedCallback() {
ReactDOM.unmountComponentAtNode(this.mountPoint);
}
mountReactApp(creator: string) {
if (!this.mountPoint) {
this.mountPoint = document.createElement('div');
this.shadowRoot.appendChild(this.mountPoint);
}
ReactDOM.render(<ReactHeader creator={creator}/>, this.mountPoint);
}
}
customElements.define('header-lib', HeaderLib);
Finally we will need to update the app.component.html in angularApp:
<header-lib [attr.creator]="creator"></header-lib>
<main></main>
<template id="template-test" [innerHTML]="template"> </template>
<footer-lib [attr.creator]="creator"></footer-lib>
The fast way
We will create a new library for test this way:
npm run nx g @nrwl/react:lib header-fast-lib
We will install these dependencies:
npm i prop-types
npm i react-to-webcomponent
We will create ReactHeader en lib of header-fast-lib:
import * as React from 'react';
import * as PropTypes from 'prop-types';
/* eslint-disable-next-line */
export interface ReactHeaderProps {
creator: string
}
export const ReactHeader = ({ creator }: ReactHeaderProps) => {
return (
<div style={display: 'flex', alignItems: 'center', justifyContent: 'center'}>
<img
alt="Nx logo"
width="75"
src="https://nx.dev/assets/images/nx-logo-white.svg"
/>
<h1>Header {creator}</h1>
</div>
);
};
ReactHeader.propTypes = { creator: PropTypes.string };
export default ReactHeader;
The only change here regarding header-lib is “ReactHeader.propTypes = { creator: PropTypes.string };” because react-to-webcomponent need it. Now inside header-fast-lib.tsx we will write:
import * as ReactDOM from 'react-dom';
import * as React from 'react';
import reactToWebComponent from 'react-to-webcomponent';
import { ReactHeader } from './ReactHeader';
export const webcomponent = reactToWebComponent(ReactHeader, React, ReactDOM);
customElements.define('header-fast-lib', webcomponent);
Inside main.ts of angularApp we will include the library and in
app.component.html we will use instead of :
import '@tutorial/header-fast-lib';
<header-fast-lib [attr.creator]="creator"></header-fast-lib>
As we have seen working web components with React is not complicated, and most of the things we knew from the previous tutorial where we work with native web components are applied, in the next part of this tutorial we will work with web components built with Angular, see you soon.
Top comments (0)