DevGang
Авторизоваться

Получение данных маршрута с помощью функции преобразователя в Angular

В этом сообщении блога я демонстрирую технику использования функции разрешения данных для получения данных во время изменения маршрута. Когда маршрут завершает активацию, компонент получает доступ к данным маршрута и может отображать их в шаблоне или манипулировать ими для получения новых состояний.

Я работал над фейковой демо-версией магазина, где на первой странице отображаются все продукты в списке. Когда пользователь щелкает название продукта, он перенаправляется на страницу сведений о продукте. Этот вариант использования очень прост, но на самом деле я потратил некоторое время на реализацию окончательного решения.

Первоначальное решение заключалось в том, чтобы сделать HTTP-запрос для получения Observable продукта, а затем использовать NgIf и AsyncPipe для разрешения Observable в шаблоне. Angular представил Signal, и я решил сохранить продукт в Signal и визуализировать шаблон со значением Signal. Решение не было элегантным, и мне пришлось его поцарапать. Наконец, я реализовал функцию разрешения данных для получения продукта по идентификатору. Когда страница сведений о продукте полностью маршрутизирована, я извлек продукт из данных маршрута и использовал его в компоненте.

Вариант использования демо-версии

В демонстрации поддельного магазина я вызываю API продукта, чтобы получить все продукты и отобразить их в ProductListComponent. Когда пользователь нажимает на название продукта, я вызываю другой API, чтобы получить сведения по идентификатору и отобразить данные в ProductDetailsComponent.

// routes.ts

export const routes: Routes = [
  {
    path: 'products',
    loadComponent: () => import('./products/product-list/product-list.component').then((m) => m.ProductListComponent),
    title: 'Product list',
  },
  {
    path: 'products/:id',
    loadComponent: () => import('./products/product-details/product-details.component').then((m) => m.ProductDetailsComponent),
    title: 'Product',
  },
];

Этот вариант использования типичен для приложения CRUD, но для разработки решения, которое получает продукт, фактически потребовалось 3 итерации.

Попытка 1. Получение Observable продукта по идентификатору и разрешение Observable в шаблоне HTML с помощью NgIf и AsyncPipe.

Попытка 2. Применение toSignal для преобразования Observable в Signal и отображения значения Signal в шаблоне. Это решение на самом деле было слишком сложным и усугубляло ситуацию.

Попытка 3. Использование функции преобразователя данных для получения наблюдаемого объекта или продукта. С помощью withComponentInputBinding продукт доступен в ProductDetailsComponent в качестве входных данных. Затем я применил входные данные во встроенном шаблоне для отображения значений продукта.

В следующих нескольких разделах я собираюсь показать, как я итеративно реализовал функцию разрешения, чтобы вернуть продукт и использовать его в компоненте.

Решение 1.

Получение Observable из API и его разрешение во встроенном шаблоне.

С помощью withComponentInputBinding параметр пути (id) является входными данными ProductDetailsComponent. Я использовал идентификатор для получения продукта и разрешил Observable во встроенном шаблоне с помощью NgIf и AsyncPipe.

// main.ts

bootstrapApplication(App, {
  providers: [provideHttpClient(), provideRouter(routes, withComponentInputBinding())]
});
// product.service.ts

const PRODUCTS_URL = 'https://fakestoreapi.com/products';

@Injectable({
  providedIn: 'root'
})
export class ProductService {
  private readonly httpClient = inject(HttpClient);

  getProduct(id: number): Observable<Product | undefined> {
    return this.httpClient.get<Product>(`${PRODUCTS_URL}/${id}`)
      .pipe(catchError((err) => of(undefined)));
  }
}
// product-details.component.ts

@Component({
  selector: 'app-product-details',
  standalone: true,
  imports: [AsyncPipe, NgIf],
  template: `
    <div>
      <div class="product" *ngIf="product$ | async as product">
        <div class="row">
          <img [src]="product?.image" [attr.alt]="product?.title" width="200" height="200" />
        </div>
        <div class="row">
          <span>id:</span><span>{{ id }}</span>
        </div>
        <div class="row">
          <span>Category: </span><span>{{ (product?.category '' }}</span>
        </div>
        <div class="row">
          <span>Description: </span><span>{{ product?.description || '' }}</span>
        </div>
        <div class="row">
          <span>Price: </span><span>{{ product?.price || '' }}</span>
        </div> 
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductDetailsComponent implements OnInit {

    @Input({ required: true, transform: numberAttribute })
    id!: number;

    productService = inject(ProductService);
    product$!: Observable<Product | undefined>;

    ngOnInit() {
          this.product$ = this.productService.getProduct(this.id);
    }
}

Однако я предпочел не иметь дело с Observable, NgIf и AsyncPipe в компоненте и HTML-шаблоне. Если я не буду использовать Observable, я буду использовать Signal, который отслеживает реактивность приложения.

Позвольте мне провести рефакторинг решения для использования Signal и отображения значения Signal во встроенном шаблоне.

Решение 2.

Преобразование наблюдаемого в сигнал и отображение значения сигнала во встроенном шаблоне.

toSignal() — это функция, которая преобразует Observable в Signal, и я подумал, что это единственное, что мне нужно в ngOnInit. Однако компилятор выдал ошибку, поскольку toSignal() не вызывался в контексте внедрения. Чтобы исправить эту ошибку, я выполнил логику функции обратного вызова runInInjectionContext.

// product-details.component.ts

injector = inject(Injector);
product: Signal<Product | undefined> = signal(undefined); 

ngOnInit() {
    runInInjectionContext(this.injector, () => {
      this.product = toSignal(this.productService.getProduct(this.id),
            { initialValue: undefined });
    });
}

Благодаря Signal мне не нужно импортировать NgIf и AsyncPipe, и каждый раз, когда продукт изменяется в product().

<div>
      <div class="product">
        <div class="row">
          <img [src]="product()?.image" [attr.alt]="product()?.title" width="200" height="200" />
        </div>
        <div class="row">
          <span>id:</span><span>{{ id }}</span>
        </div>
        <div class="row">
          <span>Category: </span><span>{{ product()?.category || '' }}</span>
        </div>
        <div class="row">
          <span>Description: </span><span>{{ product()?.description || '' }}</span>
        </div>
        <div class="row">
          <span>Price: </span><span>{{ product()?.price || '' }}</span>
        </div> 
      </div>
</div>

При сравнении решений Observable и Signal последнее добавило ненужные сложности, такие как inject(Injector) и runInInjectionContext(...). Я бы предпочел вернуться к исходному решению, чем использовать toSignal ради использования Signal.

Затем я сформулировал другое решение, которое использует функцию преобразователя данных для получения продукта при изменении маршрута. Аналогично, withComponentInputBinding должен предоставить мне входные данные о продукте в ProductDetailsComponent.

Решение 3.

Использование функции преобразователя данных для возврата продукта в ProductDetailsComponent.

Функция разрешения данных принимает маршрут и возвращает либо Observable, либо Promise. Поэтому я извлек идентификатор из URL-адреса и передал его в службу продуктов для получения наблюдаемого продукта.

// product.resolver.ts

export const productResolver = (route: ActivatedRouteSnapshot) => {
  const productId = route.paramMap.get('id');

  if (!productId) {
    return of(undefined);
  }

  return inject(ProductService).getProduct(+productId);
}

В route.ts я изменил конфигурацию маршрутов, чтобы назначить productResolver свойству разрешения пути Products/:id.

// route.ts

export const routes: Routes = [
  {
    path: 'products/:id',
    loadComponent: () => import('./products/product-details/product-details.component').then((m) => m.ProductDetailsComponent),
    title: 'Product',
    resolve: {
      product: productResolver,
    }
  },
];

Далее я мог бы очистить ProductDetailsComponents, поскольку преобразователь продукта устранил предыдущую логику в ngOnInit.

// product-details.component

@Component({
  selector: 'app-product-details',
  standalone: true,
  template: `
    <div>
      <div class="product">
       <div class="row">
          <img [src]="product?.image" [attr.alt]="product?.title || 'product image'"
            width="200" height="200"
           />
        </div>
        <div class="row">
          <span>id:</span>
          <span>{{ product?.id || '' }}</span>
        </div>
        <div class="row">
          <span>Category: </span>
          <span>{{ product?.category || '' }}</span>
        </div>
        <div class="row">
          <span>Description: </span>
          <span>{{ product?.description || '' }}</span>
        </div>
        <div class="row">
          <span>Price: </span>
          <span>{{ product?.price || '' }}</span>
        </div> 
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductDetailsComponent {
  @Input()
  product: Product | undefined = undefined; 
}

Наконец, я заменил все вхождения Product() на Product во встроенном шаблоне.

Заключение

Это конец статьи в блоге, и я надеюсь, что вам понравится ее содержание, и вы продолжите следить за моим опытом изучения Angular и других технологий.

Источник:

#Angular
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу