Remix-ing Routing in Angular 💿
Tags: remix, react, angular, opensource
Published: 2022-06-06 17:26:15

The team at [Remix](https://remix.run) are building a web framework that is built on the priniciples of React Router and uses React for rendering. They also have bigger plans to bring that same routing functionality to other frameworks. Recently, the team put out a blog post that they are [Remixing React Router](https://remix.run/blog/remixing-react-router) by taking the underlying pieces of React Router and making them framework agnostic for **any** framework to use. This post shows the steps I took to get Remix Routing working with Angular.

---

TL;DR

You can view the demo app here: https://remix-router-angular.netlify.app/

GitHub Repo: https://github.com/brandonroberts/remix-router-angular

---

It all started with a tweet...

![Kent C. Dodds Remix Angular](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2wkvkhi0t664d7bhn8sm.png)
 
Some time passes, and we get more breadcrumbs.

![Ryan Florence](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fvp77jr7pzanajdl6wnp.png)
 
So what does this mean? I went digging and sure enough, `@remix-run/router` exists as a standalone package to handle the underlying logic for managing router state, browser history, and more.

## Building the Router Service

```ts
import { createBrowserRouter } from '@remix-run/router';

  const router = createBrowserRouter({
    routes,
  });

  router.initialize();
```

A few lines and we have enough to integrate a routing solution in Angular? 🤔 Not so fast. Remix Router does handle setting up routing, providing a router state, imperative navigation, and more but does not handle rendering components. With Angular v14, rendering dynamic components has become much easier, so at most we need a router service, and an outlet component to get going.

```ts
export const ROUTES = new InjectionToken<RouteObject[]>('ROUTES');

export const REMIX_ROUTER = new InjectionToken('Remix Router', {
  providedIn: 'root',
  factory() {
    const routes = inject(ROUTES);
    const router = createBrowserRouter({
      routes,
    });
    router.initialize();
    return router;
  },
});
```

The Remix Router does need all the routes to be defined upfront. The code above defines some injection tokens so we can inject provided routes, and create the browser router for the router service.

```ts
@Injectable({
  providedIn: 'root',
})
export class Router {
  private _remixRouter = inject(REMIX_ROUTER);
  routerState$ = new BehaviorSubject<RouterState>(this._remixRouter.state);

  constructor() {
    this._remixRouter.subscribe((rs) => this.routerState$.next(rs));
  }

  get state() {
    return this._remixRouter.state;
  }

  navigate(path: string, opts?: NavigateOptions) {
    this._remixRouter.navigate(path, opts);
  }
}

export function provideRoutes(routes: RouteObject[]) {
  return [{ provide: ROUTES, useValue: routes }];
}
```

The router service is pretty thin. No `PlatformLocation` or `Location` services from Angular, as that's handled by remix router. The remix router has a `subscribe` method to listen to when the router state changes, so we wrap that in a nice observable for everyone to listen to.

Next up is the outlet for rendering components.

## Building a Router Outlet

```ts
@Directive({
  selector: 'outlet',
  standalone: true,
})
export class Outlet {
  private destroy$ = new Subject();
  private cmp!: Type<any>;
  private context? = getRouteContext();
  private router = getRouter();
  private vcr = inject(ViewContainerRef);

  ngOnInit() {
    this.setUpListener();
  }

  setUpListener() {
    this.router.routerState$
      .pipe(
        tap((rs) => {
          const matchesToRender = this.getMatch(rs);
          const currentCmp = matchesToRender.route.element;

          if (this.cmp !== currentCmp) {
            this.vcr.clear();
            this.vcr.createComponent(currentCmp, {
              injector: this.getInjector(matchesToRender),
            });
            this.cmp = currentCmp;
          }
        }),
        takeUntil(this.destroy$)
      )
      .subscribe();
  }

  getInjector(matchesToRender: DataRouteMatch) {
    const injector = Injector.create({
      providers: [
        {
          provide: ROUTE_CONTEXT,
          useValue: {
            id: matchesToRender.route.id,
            index: matchesToRender.route.index === true,
            params: matchesToRender.params,
          },
        },
      ],
      parent: this.vcr.injector,
    });

    return injector;
  }

  getMatch(routerState: RouterState) {
    const { matches } = routerState;
    const idx = matches.findIndex(
      (match) => match.route.id === this.context?.id
    );
    const matchesToRender = matches[idx + 1];

    return matchesToRender;
  }

  ngOnDestroy() {
    this.destroy$.next(true);
  }
}
```

The outlet is a placeholder directive that listens to the router state changes, and renders the component that matches the route by id. Remix Router knows all the paths, so it provides an array of matches to render. This allows us to handle nested routing without too much effort.

A parent component can be defined with the `outlet` directive to render child routes

```ts
@Component({
  selector: 'home',
  standalone: true,
  imports: [Outlet],
  template: `
    Parent - 
    <a (click)="child('child')">Child</a>

    <outlet></outlet>
  `
})
export class ParentComponent {
  router = getRouter();
}
```

## Defining Routes

Now that I have a router and an outlet, I can register some routes. 

```ts
import { RouteObject } from 'remix-router-angular';

import {
  AboutComponent,
  loader as aboutLoader,
  action as aboutAction,
} from './about.component';
import { HomeComponent } from './home.component';
import { ParentComponent } from './parent.component';
import { ChildComponent } from './child.component';

export const routes: RouteObject[] = [
  { path: '/', element: HomeComponent },
  {
    path: '/parent',
    element: ParentComponent,
    children: [
      {
        path: ':child',
        element: ChildComponent,
      },
    ],
  },
  {
    path: '/about',
    element: AboutComponent,
    action: aboutAction,
    loader: aboutLoader,
  },
];
```

The `about` route uses a `loader` for loading data, and an `action` for processing form data. These work just the same as in Remix today.

```ts
export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData();
  const name = formData.get('name');

  if (!name) {
    return {
      name: 'Name is required',
    };
  }

  return redirect(`/?name=${name}`);
};

export const loader: LoaderFunction = async () => {
  const res = await fetch('https://jsonplaceholder.typicode.com/todos/1');
  const todos = await res.json();

  return json({ todos });
};
```

Actions and loaders allow you prefetch data for the route, handle form validation, redirects, and more.

## Providing Routes

Because I'm using Angular v14 with standalone features, I used the `bootstrapApplication` function, and pass some providers through the `provideRoutes` function.

```ts
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRoutes } from 'remix-router-angular';

import { AppComponent } from './app/app.component';
import { routes } from './app/routes';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

bootstrapApplication(AppComponent, {
  providers: [provideRoutes(routes)],
});
```

I added some polish for using the new `inject()` function to provide access to get action/loader data from the Remix Router, and other pieces, but for early stage developments, everything works reasonably well. 

I'm exciting to see this develop more! Kudos to the Remix team for putting in the hard work on this.

You can view the demo app here: https://remix-router-angular.netlify.app/

GitHub Repo: https://github.com/brandonroberts/remix-router-angular

## Learn more

[Remix](https://remix.run)
[Angular](https://angular.io)
[Angular v14 Release Post](https://blog.angular.io/angular-v14-is-now-available-391a6db736af)
[remix-router-vue](https://github.com/brophdawg11/remix-routers/tree/main/packages/vue#readme)

If you liked this, click the :heart: so other people will see it. Follow [me on Twitter](https://twitter.com/brandontroberts) and subscribe to my [YouTube Channel](https://youtube.com/brandonrobertsdev?sub_confirmation=1) for more content on [Angular](https://angular.io), [NgRx](https://ngrx.io), and more!