How to configure storybook story for module with RouterLink
Asked Answered
P

5

11

Cannot configure story for module where routerLink is used due to error

ERROR NullInjectorError: StaticInjectorError(DynamicModule)[RouterLinkActive -> Router]

To Reproduce Steps to reproduce the behavior: demo-app Run app and you can test that there is no possible solution to add RouterModule to work with story. It cannot be configured with RouterTestingModule, RouterModule, RouterModule.forRoot with iframe.html path. There is always the same error about missing provider.

Expected behavior To run application and story with routerLink

Additional context Latest version of storybook 5.3.3 and angular ~8.2.14 I was working with different configuration 5.2.8 and this issue does not appear.

How to configure this module, is there an issue in storybook?

Storybook issue

Perky answered 15/1, 2020 at 8:27 Comment(2)
Are you still looking for an answer to this question? Does the answer proposed by Ling Vu work for you?Ecumenicism
Any feedback? Does it work?Mathia
S
2

In case you are working with angular 15 and also want to render the router-outlet, I was able to solve it like this:

in preview.js:

const globalModuleImports = moduleMetadata({
  imports: [RouterTestingModule, RouterModule, HttpClientModule],
  providers: [Router],
});


const setRoutesMetadata = (fn, c) => {
  const story = fn();
  story.moduleMetadata.providers.push(
    {
      provide: ENVIRONMENT_INITIALIZER, multi: true, useValue() {
        inject(Router).resetConfig(c.parameters?.routes || [])
      }
    }
  )
  return story;
}

export const decorators = [
  globalModuleImports,
  setRoutesMetadata
];

and then in your story you can set routes inside a story parameters:

  SomeStory.parameters = {
    routes: [
      {
        path: "second",
        loadChildren: () => import('./second/second-routes'),
      },
    ]
  }
Shalon answered 10/1, 2023 at 12:53 Comment(0)
P
14

Storybook works different than angular so I dont need to inject FeatureModule into story, Component is just fine. When only NavbarComponent is injected with RouterTestingModule

story configuration looks like this

storiesOf('Navbar', module)
  .addDecorator(
    moduleMetadata({
      imports: [BrowserAnimationsModule, RouterTestingModule],
      declarations: [NavbarComponent],
    }),
  )

And you don't need routes configuration xD

Perky answered 12/2, 2020 at 8:58 Comment(2)
I still don't see the point in injecting RouterTestingModule which is supposed to be used in Unit tests and mock the routes, but it's okay if it works for you.Mathia
This provides mocks for needed directives, and as storybook works it is only for presentation so full RouterModule is not requiredPerky
M
7

I made the following changes:

  1. Adding the RouterModule including the the routes array (used to define your routes)
  2. Provide the required APP_BASE_HREF.

navbar.module.ts

const routes: Routes = [];


@NgModule({
  declarations: [NavbarComponent],
  imports: [
    CommonModule,
    RouterModule.forRoot(routes)
  ],
  providers: [{provide: APP_BASE_HREF, useValue: '/'}],
  exports: [NavbarComponent]
})
export class NavbarModule {
}

After that you just need to add the <router-outlet></router-outlet> into your NavbarComponent

Mathia answered 24/1, 2020 at 8:11 Comment(1)
This does not solve problem, there is the same problemPerky
T
7

With Angular v15 and Storybook v7

The only way that worked for me for the routerLink to work and also render the router-outlet was by using applicationConfig for providing the router stuff like this:

const meta: Meta<RoutingTabsComponentExt> = {
    title: "UI Bricks/Web Only/Routing Tabs",
    tags: ["autodocs"],
    component: RoutingTabsComponent,
    decorators: [
        moduleMetadata({
            imports: [RoutingTabsModule],
        }),
        applicationConfig({
            providers: [provideRouter(routes)],
        }),
        componentWrapperDecorator((story) => `${story}`),
    ],
};

I just prepared some dummy data for it and the storybook was able to route and render without issues

@Component({
    standalone: true,
    template: `
        <h1>First Tab</h1>
        <p>This is the content of the first tab</p>
    `,
})
class FirstStoryContentComponent {}

@Component({
    standalone: true,
    template: `
        <h1>Second Tab</h1>
        <p>This is the content of the second tab</p>
    `,
})
class SecondStoryContentComponent {}

const routes: Routes = [
    {
        path: "tab1",
        loadComponent: () => FirstStoryContentComponent,
    },
    {
        path: "tab2",
        loadComponent: () => SecondStoryContentComponent,
    },
    {
        path: "**",
        redirectTo: "tab1",
        pathMatch: "full",
    },
];

Hope that it helps someone, since it took me hours to figure out and none of the solutions that are out there were working for me :).

Torrance answered 3/10, 2023 at 11:35 Comment(0)
S
2

In case you are working with angular 15 and also want to render the router-outlet, I was able to solve it like this:

in preview.js:

const globalModuleImports = moduleMetadata({
  imports: [RouterTestingModule, RouterModule, HttpClientModule],
  providers: [Router],
});


const setRoutesMetadata = (fn, c) => {
  const story = fn();
  story.moduleMetadata.providers.push(
    {
      provide: ENVIRONMENT_INITIALIZER, multi: true, useValue() {
        inject(Router).resetConfig(c.parameters?.routes || [])
      }
    }
  )
  return story;
}

export const decorators = [
  globalModuleImports,
  setRoutesMetadata
];

and then in your story you can set routes inside a story parameters:

  SomeStory.parameters = {
    routes: [
      {
        path: "second",
        loadChildren: () => import('./second/second-routes'),
      },
    ]
  }
Shalon answered 10/1, 2023 at 12:53 Comment(0)
C
0

I use the following code with Angular 18 and Storybook 8. This code also intercepts and logs all link navigations.

preview.ts:

const routerDecorator: DecoratorFunction<AngularRenderer> = (
  storyFn,
  context,
) => {
  return applicationConfig({
    providers: [
      {
        provide: Router,
        useFactory: () => {
          return new ActionLoggingRouter();
        },
      },
      provideRouter([
        {
          path: 'iframe.html',
          children: [],
        },
      ]),
    ],
  })(storyFn, context);
};

// usage
const preview: Preview = {
  decorators: [routerDecorator]
};

ActionLoggingRouter.ts:

import { action } from '@storybook/addon-actions';
import { Router } from '@angular/router';

export class ActionLoggingRouter extends Router {
  override navigate(commands: unknown, extras: unknown) {
    action('[router] navigate')({ commands, extras });
    return Promise.resolve(true);
  }

  override navigateByUrl(url: unknown, extras: unknown) {
    action('[router] navigateByUrl')({ url, extras });
    return Promise.resolve(true);
  }
}

Cello answered 2/9, 2024 at 12:28 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.