How to use an Angular component in React

yashints

Yaser Adel Mehraban

Posted on June 2, 2019

How to use an Angular component in React

Angular Elements

Since Angular 6, the team released Angular Elements which allows you to bootstrap Angular components within an existing Angular application by registering them as Custom Components.

This opens a whole lot of opportunities to web developers who are used to argue about superiority of Angular vs React. Some people believe Angular is suitable for developing enterprise applications, while others prefer React because of it's speed and bundle size. Meanwhile, Angular team has been working hard to bring this feature into V6 release and let Angular lovers create Custom Components which not only can be used in other framework, but also give ability to create standalone widgets easily to be consumed even in a simple HTML page, think for example SharePoint.

The result component will be like a web component for the React application like any other HTML components like button or input. Plus, finally I had some time to play with the concepts and get a quick demo ready and it was pretty easy as expected. So let's get started.

For the purpose of this post, I will create a book list component in Angular which accepts a list of books and shows them in the page. This component is ported into a React application which will send the list of books as props to it. Once a book is added to the card, the changes will be propagated to React app.

At a high level, here is a picture describing the solution from documentation:

Angular Elements

Create your Angular component

First let's create a new Angular application using Angular CLI:

ng new book-list
Enter fullscreen mode Exit fullscreen mode

You can go ahead and delete the default app component and create a new component called book list:

ng g c book-list
Enter fullscreen mode Exit fullscreen mode

Note: I'm using Angular CLI's short hand commands here (g for generate and c for component), for more info on those please visit their documentation.

Since we want to evaluate the communication between our Angular and React applications, we will pass the book list as input. Just to test the other way around, we define an output as an EventEmitter that gets triggered whenever a book is selected from the list. So the component code will look like this:

import { Component, Input, Output, EventEmitter } from '@angular/core'

@Component({
  selector: 'book-list',
  templateUrl: './book-list.component.html',
  styleUrls: ['./book-list.component.css'],
})
export class BookListComponent {
  public bookList: any[]

  @Input('books')
  set books(books: string) {
    this.bookList = JSON.parse(books)
  }

  @Output('bookSelected') bookSelected = new EventEmitter<any>()

  constructor() {}

  selected(book: any) {
    this.bookSelected.emit(JSON.stringify(book))
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: Currently passing a complex object like array into a web component is a debatable (you can set a property value using JavaScript or just use attributes). For simplicity, we just pass a JSON string for the purpose of this post.

And the HTML is pretty simple:

<h1>List of recent books</h1>

<div *ngFor="let book of bookList">
  <strong>{{book.name}}</strong>
  <p>{{book.description}}</p>
  <label class="container"
    >Add to card <input type="checkbox" (change)="selected(book)" />
    <span class="checkmark"></span>
  </label>
</div>
Enter fullscreen mode Exit fullscreen mode

Bootstrapping the component as an element

Now that we have created our component, it's time to bootstrap our component as an Angular element:

import { BrowserModule } from '@angular/platform-browser'
import { NgModule, Injector } from '@angular/core'
import { createCustomElement } from '@angular/elements'
import { BookListComponent } from './book-list/book-list.component'

@NgModule({
  declarations: [BookListComponent],
  imports: [BrowserModule],
  providers: [],
  bootstrap: [],
  entryComponents: [BookListComponent],
})
export class AppModule {
  constructor(private injector: Injector) {
    const customElement = createCustomElement(BookListComponent, { injector })
    customElements.define('book-list', customElement)
  }

  ngDoBootstrap() {}
}
Enter fullscreen mode Exit fullscreen mode

To create a custom element from the book list component, we should invoke the new createCustomElement method which doesn't insert the newly created component inside CustomElementRegistry, hence using AppModule constructor to do it manually. Maybe in future that will happen automatically but until then it is our job to do it.

At this point the element is ready. It's time to build our element:

ng build --prod --output-hashing none
Enter fullscreen mode Exit fullscreen mode

If you have a look at your dist/book-list folder now, you should see three JS files generated for you, main.js, polyfills.js, runtime.js.

Folder structure

Hosting our book list inside a React app

It's time to create our React app. We can start by creating one using React CLI:

npx create-react-app react-host
Enter fullscreen mode Exit fullscreen mode

When the command finished you should have the initial React app template setup. If you run npm start from inside the react-host folder, you should see the default app:

react default app

We can copy the three files created by Angular CLI into the public folder of our React app and reference them inside index.html (You can setup the the projects in a way to export the Angular Element as npm package and consume it in React app, I am just taking it easy here 🙃).

<body>
  <noscript> You need to enable JavaScript to run this app. </noscript>
  <div id="root"></div>

  <script type="text/javascript" src="./book-list/runtime.js"></script>
  <script type="text/javascript" src="./book-list/polyfills.js"></script>
  <script type="text/javascript" src="./book-list/main.js"></script>
</body>
Enter fullscreen mode Exit fullscreen mode

Using the book list component

Now that we have everything in place, let's start by modifying our app component and add a list of books as it's state so we can pass it down to our book list:

constructor(props){
  super(props)
  this.state = {
    books: [
      {
        name: '10% Happier',
        description: `Practicing meditation and mindfulness
          will make you at least 10 percent happier.`
      },
      {
        name: 'The 10X Rule',
        description: `The biggest mistake most people
          make in life is not setting goals high enough.`
      },
      {
        name: 'A Short Guide to a Happy Life',
        description: `The only thing you have that nobody
          else has is control of your life.`
      }
    ]
  };
}
Enter fullscreen mode Exit fullscreen mode

Now we can use our book list component and pass the books down as property:

render() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <h1 className="App-title">Welcome to React</h1>
      </header>

      <div className="book-list">
        <book-list ref={elem => this.nv = elem}
          books={JSON.stringify(this.state.books)}></book-list>
      </div>
      <div className="selected-books">
        <h1>Shopping card</h1>
        {this.renderSelectedBooks()}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Note: As I mentioned above, we need to pass a string to our custom component or it won't receive the correct data.

We used a method to render the selected books, so let's define it:

renderSelectedBooks() {
    return(
      <div>
        {
          this.state.selectedBooks.map(function(book, index){
            return <div><strong key={ index }>{book.name}</strong></div>;
          })
        }
      </div>
    )
  }
Enter fullscreen mode Exit fullscreen mode

I am using internal state here, but note that this is not a React post and I am not following any best practices here.

Also we used a variable called nv to have a reference to the component. We will add an event listener to it which listens to bookSelected event and called a method to handle the event.

componentDidMount() {
  this.nv.addEventListener("bookSelected", this.handleBookSelected);
}
Enter fullscreen mode Exit fullscreen mode

Warning: The name of the event should match the name of event that you used when defining the Angular element.

Let's initialise our state in our event handler:

handleBookSelected = event => {
  const book = JSON.parse(event.detail)
  let selectedBookList = []

  if (this.state.selectedBooks.find(x => x.name === book.name)) {
    selectedBookList = this.state.selectedBooks.filter(
      x => x.name !== book.name
    )
  } else {
    selectedBookList = [...this.state.selectedBooks, book]
  }

  this.setState({
    ...this.state,
    selectedBooks: [...selectedBookList],
  })
}
Enter fullscreen mode Exit fullscreen mode

The above code looks a bit busy, but it is very simple. We first check if the book is already selected and remove it if it is. If the book is not in selected list we add it and update the state. Once the state is updated React will reload the page and shows the updated selected books.

If you run the app now you should see a screen like this:

And that's it you're officially running an Angular Element inside a React app and they get along really well 😁🔥💯.

You can find the source code on my GitHub repository.

Now go and create awesome stuff 👊🏻.

💖 💪 🙅 🚩
yashints
Yaser Adel Mehraban

Posted on June 2, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related