---
title: 'Creating Detail Views'
---

# Creating Detail Views

The two most common type of components you'll be creating in your UI extensions are list components and detail components.

In Vendure, we have standardized the way you write these components so that your ui extensions can be made to fit seamlessly into the rest of the app.

:::note
The specific pattern described here is for Angular-based components. It is also possible to create detail views using React components, but 
in that case you won't be able to use the built-in Angular-specific components.
:::

## Example: Creating a Product Detail View

Let's say you have a plugin which adds a new entity to the database called `ProductReview`. You have already created a [list view](/guides/extending-the-admin-ui/creating-list-views/), and
now you need a detail view which can be used to view and edit individual reviews.

### Extend the TypedBaseDetailComponent class

The detail component itself is an Angular component which extends the [BaseDetailComponent](/reference/admin-ui-api/list-detail-views/base-detail-component/) or [TypedBaseDetailComponent](/reference/admin-ui-api/list-detail-views/typed-base-detail-component) class.

This example assumes you have set up your project to use code generation as described in the [GraphQL code generation guide](/guides/how-to/codegen/#codegen-for-admin-ui-extensions).

```ts title="src/plugins/reviews/ui/components/review-detail/review-detail.component.ts"
import { ResultOf } from '@graphql-typed-document-node/core';
import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { TypedBaseDetailComponent, LanguageCode, NotificationService, SharedModule } from '@vendure/admin-ui/core';

// This is the TypedDocumentNode & type generated by GraphQL Code Generator
import { graphql } from '../../gql';

export const reviewDetailFragment = graphql(`
  fragment ReviewDetail on ProductReview {
    id
    createdAt
    updatedAt
    title
    rating
    text
    authorName
    productId
  }
`);

export const getReviewDetailDocument = graphql(`
  query GetReviewDetail($id: ID!) {
    review(id: $id) {
      ...ReviewDetail
    }
  }
`);

export const createReviewDocument = graphql(`
  mutation CreateReview($input: CreateProductReviewInput!) {
    createProductReview(input: $input) {
      ...ReviewDetail
    }
  }
`);

export const updateReviewDocument = graphql(`
  mutation UpdateReview($input: UpdateProductReviewInput!) {
    updateProductReview(input: $input) {
      ...ReviewDetail
    }
  }
`);

@Component({
    selector: 'review-detail',
    templateUrl: './review-detail.component.html',
    styleUrls: ['./review-detail.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    standalone: true,
    imports: [SharedModule],
})
export class ReviewDetailComponent extends TypedBaseDetailComponent<typeof getReviewDetailDocument, 'review'> implements OnInit, OnDestroy {
    detailForm = this.formBuilder.group({
        title: [''],
        rating: [1],
        authorName: [''],
    });

    constructor(private formBuilder: FormBuilder, private notificationService: NotificationService) {
        super();
    }

    ngOnInit() {
        this.init();
    }

    ngOnDestroy() {
        this.destroy();
    }

    create() {
        const { title, rating, authorName } = this.detailForm.value;
        if (!title || rating == null || !authorName) {
            return;
        }
        this.dataService
            .mutate(createReviewDocument, {
                input: { title, rating, authorName },
            })
            .subscribe(({ createProductReview }) => {
                if (createProductReview.id) {
                    this.notificationService.success('Review created');
                    this.router.navigate(['extensions', 'reviews', createProductReview.id]);
                }
            });
    }

    update() {
        const { title, rating, authorName } = this.detailForm.value;
        this.dataService
            .mutate(updateReviewDocument, {
                input: { id: this.id, title, rating, authorName },
            })
            .subscribe(() => {
                this.notificationService.success('Review updated');
            });
    }

    protected setFormValues(entity: NonNullable<ResultOf<typeof getReviewDetailDocument>['review']>, languageCode: LanguageCode): void {
        this.detailForm.patchValue({
            title: entity.name,
            rating: entity.rating,
            authorName: entity.authorName,
            productId: entity.productId,
        });
    }
}
```

### Create the template

Here is the standard layout for detail views:

```html
<vdr-page-block>
    <vdr-action-bar>
        <vdr-ab-left></vdr-ab-left>
        <vdr-ab-right>
            <button
                class="button primary"
                *ngIf="isNew$ | async; else updateButton"
                (click)="create()"
                [disabled]="detailForm.pristine || detailForm.invalid"
            >
                {{ 'common.create' | translate }}
            </button>
            <ng-template #updateButton>
                <button
                    class="btn btn-primary"
                    (click)="update()"
                    [disabled]="detailForm.pristine || detailForm.invalid"
                >
                    {{ 'common.update' | translate }}
                </button>
            </ng-template>
        </vdr-ab-right>
    </vdr-action-bar>
</vdr-page-block>

<form class="form" [formGroup]="detailForm">
    <vdr-page-detail-layout>
        <!-- The sidebar is used for displaying "metadata" type information about the entity -->
        <vdr-page-detail-sidebar>
            <vdr-card *ngIf="entity$ | async as entity">
                <vdr-page-entity-info [entity]="entity" />
            </vdr-card>
        </vdr-page-detail-sidebar>

        <!-- The main content area is used for displaying the entity's fields -->
        <vdr-page-block>
            <!-- The vdr-card is the container for grouping items together on a page -->
            <!-- it can also take an optional [title] property to display a title -->
            <vdr-card>
                <!-- the form-grid class is used to lay out the form fields -->
                <div class="form-grid">
                    <vdr-form-field label="Title" for="title">
                        <input id="title" type="text" formControlName="title" />
                    </vdr-form-field>
                    <vdr-form-field label="Rating" for="rating">
                        <input id="rating" type="number" min="1" max="5" formControlName="rating" />
                    </vdr-form-field>

                    <!-- etc -->
                </div>
            </vdr-card>
        </vdr-page-block>
    </vdr-page-detail-layout>
</form>
```

### Route config

Here's how the routing would look for a typical list & detail view:

```ts title="src/plugins/reviews/ui/routes.ts"
import { registerRouteComponent } from '@vendure/admin-ui/core';

import { ReviewDetailComponent, getReviewDetailDocument } from './components/review-detail/review-detail.component';
import { ReviewListComponent } from './components/review-list/review-list.component';

export default [
    // List view
    registerRouteComponent({
        path: '',
        component: ReviewListComponent,
        breadcrumb: 'Product reviews',
    }),
    // highlight-start
    // Detail view
    registerRouteComponent({
        path: ':id',
        component: ReviewDetailComponent,
        query: getReviewDetailDocument,
        entityKey: 'productReview',
        getBreadcrumbs: entity => [
            {
                label: 'Product reviews',
                link: ['/extensions', 'product-reviews'],
            },
            {
                label: `#${entity?.id} (${entity?.product.name})`,
                link: [],
            },
        ],
    }),
    // highlight-end
]
```

## Supporting custom fields

From Vendure v2.2, it is possible for your [custom entities to support custom fields](/guides/developer-guide/database-entity/#supporting-custom-fields).

If you have set up your entity to support custom fields, and you want custom fields to be available in the Admin UI detail view,
you need to add the following to your detail component:

```ts title="src/plugins/reviews/ui/components/review-detail/review-detail.component.ts"
// highlight-next-line
import { getCustomFieldsDefaults } from '@vendure/admin-ui/core';

@Component({
    selector: 'review-detail',
    templateUrl: './review-detail.component.html',
    styleUrls: ['./review-detail.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    standalone: true,
    imports: [SharedModule],
})
export class ReviewDetailComponent extends TypedBaseDetailComponent<typeof getReviewDetailDocument, 'review'> implements OnInit, OnDestroy {

    // highlight-next-line
    customFields = this.getCustomFieldConfig('ProductReview');

    detailForm = this.formBuilder.group({
        title: [''],
        rating: [1],
        authorName: [''],
        // highlight-next-line
        customFields: this.formBuilder.group(getCustomFieldsDefaults(this.customFields)),
    });

    protected setFormValues(entity: NonNullable<ResultOf<typeof getReviewDetailDocument>['review']>, languageCode: LanguageCode): void {
        this.detailForm.patchValue({
            title: entity.name,
            rating: entity.rating,
            authorName: entity.authorName,
            productId: entity.productId,
        });
        // highlight-start
        if (this.customFields.length) {
            this.setCustomFieldFormValues(this.customFields, this.detailForm.get('customFields'), entity);
        }
        // highlight-end
    }
}
```

Then add a card for your custom fields to the template:

```html title="src/plugins/reviews/ui/components/review-detail/review-detail.component.html"
<form class="form" [formGroup]="detailForm">
    <vdr-page-detail-layout>
        <!-- The sidebar is used for displaying "metadata" type information about the entity -->
        <vdr-page-detail-sidebar>
            <vdr-card *ngIf="entity$ | async as entity">
                <vdr-page-entity-info [entity]="entity" />
            </vdr-card>
        </vdr-page-detail-sidebar>

        <!-- The main content area is used for displaying the entity's fields -->
        <vdr-page-block>
            <!-- The vdr-card is the container for grouping items together on a page -->
            <!-- it can also take an optional [title] property to display a title -->
            <vdr-card>
                <!-- the form-grid class is used to lay out the form fields -->
                <div class="form-grid">
                    <vdr-form-field label="Title" for="title">
                        <input id="title" type="text" formControlName="title" />
                    </vdr-form-field>
                    <vdr-form-field label="Rating" for="rating">
                        <input id="rating" type="number" min="1" max="5" formControlName="rating" />
                    </vdr-form-field>

                    <!-- etc -->
                </div>
            </vdr-card>
            // highlight-start
            <vdr-card
                    formGroupName="customFields"
                    *ngIf="customFields.length"
                    [title]="'common.custom-fields' | translate"
            >
                <vdr-tabbed-custom-fields
                        entityName="ProductReview"
                        [customFields]="customFields"
                        [customFieldsFormGroup]="detailForm.get('customFields')"
                ></vdr-tabbed-custom-fields>
            </vdr-card>
            // highlight-end
        </vdr-page-block>
    </vdr-page-detail-layout>
</form>
```
