Skip to content

React Custom Element Implementation Guide for AIV

This guide provides a complete implementation for creating React components that can be deployed as custom elements and used within AIV applications.

Overview

The goal is to create a React component that can be:

  1. Built as a custom element (Web Component)
  2. Deployed independently
  3. Used within Angular applications
  4. Communicate with Angular through attributes and events

Project Structure

react-custom-element/
├── src/
│   ├── components/
│   │   ├── ReactComponent.jsx    # Main React component
│   │   └── ReactComponent.css    # Component styles
│   ├── CustomElement.js          # Custom element wrapper
│   └── index.js                  # Entry point
├── public/
│   └── index.html               # Demo HTML
├── dist/                        # Built files
├── webpack.config.js            # Webpack configuration
├── package.json                 # Dependencies and scripts
├── .babelrc                     # Babel configuration
├── demo.html                    # Standalone demo
└── README.md                    # Documentation

Step 1: Setup the React Custom Element Project

1.1 Create the React Component

// src/components/ReactComponent.jsx
import React, { useState, useEffect } from 'react';
import './ReactComponent.css';

const ReactComponent = ({ 
  title = 'React Component', 
  message = 'This is a React component wrapped as a custom element.',
  theme = 'primary'
}) => {
  const [count, setCount] = useState(0);
  const [currentTime, setCurrentTime] = useState(new Date());

  useEffect(() => {
    const timer = setInterval(() => {
      setCurrentTime(new Date());
    }, 1000);

    return () => clearInterval(timer);
  }, []);

  const handleIncrement = () => {
    setCount(prev => prev + 1);
  };

  const handleReset = () => {
    setCount(0);
  };

  return (
    <div className={`react-component react-component--${theme}`}>
      <div className="react-component__header">
        <h2 className="react-component__title">{title}</h2>
        <div className="react-component__time">
          {currentTime.toLocaleTimeString()}
        </div>
      </div>
      
      <div className="react-component__content">
        <p className="react-component__message">{message}</p>
        
        <div className="react-component__counter">
          <h3>Counter: {count}</h3>
          <div className="react-component__buttons">
            <button 
              className="react-component__button react-component__button--primary"
              onClick={handleIncrement}
            >
              Increment
            </button>
            <button 
              className="react-component__button react-component__button--secondary"
              onClick={handleReset}
            >
              Reset
            </button>
          </div>
        </div>
        
        <div className="react-component__features">
          <h4>Features:</h4>
          <ul>
            <li>React 18 with hooks</li>
            <li>Custom element wrapper</li>
            <li>Theme support</li>
            <li>Real-time updates</li>
            <li>Interactive components</li>
            <li>Shadow DOM encapsulation</li>
          </ul>
        </div>
      </div>
    </div>
  );
};

export default ReactComponent;

1.2 Create the Custom Element Wrapper

// src/CustomElement.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import ReactComponent from './components/ReactComponent';

class ReactCustomElement extends HTMLElement {
  constructor() {
    super();
    this.shadow = this.attachShadow({ mode: 'open' });
    this.root = null;
  }

  connectedCallback() {
    this.render();
  }

  disconnectedCallback() {
    if (this.root) {
      this.root.unmount();
      this.root = null;
    }
  }

  static get observedAttributes() {
    return ['title', 'message', 'theme'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this.render();
    }
  }

  getProps() {
    return {
      title: this.getAttribute('title') || undefined,
      message: this.getAttribute('message') || undefined,
      theme: this.getAttribute('theme') || 'primary'
    };
  }

  render() {
    if (!this.root) {
      const container = document.createElement('div');
      this.shadow.appendChild(container);
      this.root = createRoot(container);
    }

    const props = this.getProps();
    this.root.render(React.createElement(ReactComponent, props));
  }
}

// Define the custom element
customElements.define('react-custom-element', ReactCustomElement);

export default ReactCustomElement;

1.3 Create the Entry Point

// src/index.js
import ReactCustomElement from './CustomElement';

// Export the custom element class for manual registration
export { ReactCustomElement };

// Export a function to register the custom element
export function registerReactCustomElement() {
  if (!customElements.get('react-custom-element')) {
    customElements.define('react-custom-element', ReactCustomElement);
  }
}

// Auto-register if this module is loaded
registerReactCustomElement();

// Export as default for UMD builds
export default ReactCustomElement;

Step 2: Build Configuration

2.1 Webpack Configuration

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'react-custom-element.js',
    library: {
      name: 'ReactCustomElement',
      type: 'umd',
      export: 'default'
    },
    globalObject: 'this'
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              '@babel/preset-env',
              ['@babel/preset-react', { runtime: 'automatic' }]
            ]
          }
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.jsx']
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      filename: 'index.html'
    })
  ],
  devServer: {
    static: {
      directory: path.join(__dirname, 'public')
    },
    port: 3000,
    hot: true
  }
};

2.2 Package.json

{
  "name": "react-custom-element",
  "version": "1.0.0",
  "description": "React custom element for Angular integration",
  "main": "dist/index.js",
  "scripts": {
    "build": "webpack --mode production",
    "dev": "webpack --mode development --watch",
    "start": "webpack serve --mode development"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@babel/core": "^7.22.0",
    "@babel/preset-env": "^7.22.0",
    "@babel/preset-react": "^7.22.0",
    "babel-loader": "^9.1.0",
    "css-loader": "^6.8.0",
    "html-webpack-plugin": "^5.5.0",
    "style-loader": "^3.3.0",
    "webpack": "^5.88.0",
    "webpack-cli": "^5.1.0",
    "webpack-dev-server": "^4.15.0"
  }
}

Step 3: Build and Deploy

3.1 Build the Custom Element

npm install
npm run build

This creates dist/react-custom-element.js which contains the custom element.

3.2 Deploy Options

Here’s the improved version of your documentation for section 3.2 Deploy Options, with clearer structure, grammar corrections, and more professional language:


3.2 Deploy Options

To deploy your React custom app, follow the steps below carefully:

1. Create app.json

Within the same folder where your project build is located, create a file named **app.json**.

Use the following structure for the file. Do not modify the structure or include comments in the JSON:

{
  "outputs": [],
  "appJs": [
    "react-custom-element.js"
  ],
  "framework": "react",
  "inputs": [],
  "appCss": [],
  "name": "React Custom App",
  "selector": [
    "react-custom-element"
  ],
  "id": 1,
  "tag": "react-custom-element",
  "datasets": [{}]
}

Note:

  • Replace "name" with your actual app name.
  • Ensure the "id" is unique.
  • The "tag" and "selector" should match your custom element tag.

2. Prepare ZIP File

  • Rename the main folder to match the actual app name if needed.
  • Ensure the folder structure inside the ZIP matches the sample project structure (refer to the provided sample).
  • Include your app.json file inside this folder.
  • Create a .zip archive of the folder.

3. Upload to AIV

  1. Login to AIV. If you are already logged in, continue to the next step.

  2. Click on the hamburger menu (☰) on the left sidebar and select External App.

  3. On the External App page, scroll down to the bottom and click the Upload button.

  4. In the upload dialog:

    • Click the Upload icon to open the file selector.
    • Locate and select your ZIP file (e.g., react.zip). The app name will be auto-detected from app.json.
  5. (Optional) Add up to 5 datasets to your app. These datasets will be injected into your React app and accessible via data1, data2, data3, data4, and data5.

  6. Once done, click Upload to complete the process. Your app will now be available within AIV.