vendure-ecommerce/storefront-angular-starter

Accessing customField properties?

Opened this issue · 1 comments

Hi, i'm rather new to node & angular. I'm trying to create custom fields for a Product, but having trouble accessing the custom fields i've created.

Custom fields are defined in my vendure-config.ts below.

customFields: {
        Product: [
          { 
              name: 'metaTitle', 
              type: 'string', 
              label: [ { languageCode: LanguageCode.en, value: 'Meta Title' } ] 
          },
          { 
              name: 'metaDescription', 
              type: 'string', 
              label: [ { languageCode: LanguageCode.en, value: 'Meta Description' } ] 
          },
          { 
              name: 'metaKeywords', 
              type: 'string', 
              label: [ { languageCode: LanguageCode.en, value: 'Meta Keywords' } ] 
          }
        ]
    },

product-detail.component.ts


import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { filter, map, switchMap, withLatestFrom } from 'rxjs/operators';

import { AddToCart, GetProductDetail } from '../../../common/generated-types';
import { notNullOrUndefined } from '../../../common/utils/not-null-or-undefined';
import { DataService } from '../../providers/data/data.service';
import { NotificationService } from '../../providers/notification/notification.service';
import { StateService } from '../../providers/state/state.service';

import { ADD_TO_CART, GET_PRODUCT_DETAIL } from './product-detail.graphql';
import { Title, Meta } from '@angular/platform-browser';

@Component({
    selector: 'vsf-product-detail',
    templateUrl: './product-detail.component.html',
    styleUrls: ['./product-detail.component.scss'],
})
export class ProductDetailComponent implements OnInit, OnDestroy {

    product: GetProductDetail.Product;

    selectedAsset: { id: string; preview: string; };
    selectedVariant: GetProductDetail.Variants;
    qty = 1;
    breadcrumbs: GetProductDetail.Breadcrumbs[] | null = null;

    @ViewChild('addedToCartTemplate', {static: true})
    private addToCartTemplate: TemplateRef<any>;
    private sub: Subscription;

    constructor(private dataService: DataService,
                private stateService: StateService,
                private notificationService: NotificationService,
                private route: ActivatedRoute,
                private titleService: Title, 
                private metaService: Meta) {
    }

    ngOnInit() {
        const lastCollectionSlug$ = this.stateService.select(state => state.lastCollectionSlug);
        const productSlug$ = this.route.paramMap.pipe(
            map(paramMap => paramMap.get('slug')),
            filter(notNullOrUndefined),
        );


        this.sub = productSlug$.pipe(
            switchMap(slug => {
                return this.dataService.query<GetProductDetail.Query, GetProductDetail.Variables>(GET_PRODUCT_DETAIL, {
                        slug,
                    },
                );
            }),
            map(data => data.product),
            filter(notNullOrUndefined),
            withLatestFrom(lastCollectionSlug$),
        ).subscribe(([product, lastCollectionSlug]) => {

            this.product = product;
            // trying to assign customFields here throws error
            let customFieldsData = this.product.customFields;

            if (this.product.featuredAsset) {
                this.selectedAsset = this.product.featuredAsset;
            }
            
            // return this.product.customFields.meta_title/; 
            // if(this.product.customFields){
            //     console.log('product', this.product.customFields.meta_title);
            // }
            // this.titleService.setTitle(custom.metaTitle);

            // this.metaService.addTags([
            //   {name: 'keywords', content: custom.metaKeywords},
            //   {name: 'description', content: custom.metaDescription},
            //   {name: 'robots', content: 'index, follow'}
            // ]);

            this.selectedVariant = product.variants[0];
            const collection = this.getMostRelevantCollection(product.collections, lastCollectionSlug);
            this.breadcrumbs = collection ? collection.breadcrumbs : [];
        });
    }

    ngOnDestroy() {
        if (this.sub) {
            this.sub.unsubscribe();
        }
    }

    addToCart(variant: GetProductDetail.Variants, qty: number) {
        this.dataService.mutate<AddToCart.Mutation, AddToCart.Variables>(ADD_TO_CART, {
            variantId: variant.id,
            qty,
        }).subscribe(({addItemToOrder}) => {
            switch (addItemToOrder.__typename) {
                case 'Order':
                    this.stateService.setState('activeOrderId', addItemToOrder ? addItemToOrder.id : null);
                    if (variant) {
                        this.notificationService.notify({
                            title: 'Added to cart',
                            type: 'info',
                            duration: 3000,
                            templateRef: this.addToCartTemplate,
                            templateContext: {
                                variant,
                                quantity: qty,
                            },
                        }).subscribe();
                    }
                    break;
                case 'OrderModificationError':
                case 'OrderLimitError':
                case 'NegativeQuantityError':
                case 'InsufficientStockError':
                    this.notificationService.error(addItemToOrder.message).subscribe();
                    break;
            }

        });
    }

    viewCartFromNotification(closeFn: () => void) {
        this.stateService.setState('cartDrawerOpen', true);
        closeFn();
    }

    /**
     * If there is a collection matching the `lastCollectionId`, return that. Otherwise return the collection
     * with the longest `breadcrumbs` array, which corresponds to the most specific collection.
     */
    private getMostRelevantCollection(collections: GetProductDetail.Collections[], lastCollectionSlug: string | null) {
        const lastCollection = collections.find(c => c.slug === lastCollectionSlug);
        if (lastCollection) {
            return lastCollection;
        }
        return collections.slice().sort((a, b) => {
            if (a.breadcrumbs.length < b.breadcrumbs.length) {
                return 1;
            }
            if (a.breadcrumbs.length > b.breadcrumbs.length) {
                return -1;
            }
            return 0;
        })[0];
    }

}

`
I have also added the customFields to the graphql query in product-detail.graphql.ts


export const GET_PRODUCT_DETAIL = gql`
    query GetProductDetail($slug: String!) {
        product(slug: $slug) {
            id
            name
            description
            variants {
                id
                name
                options {
                    code
                    name
                }
                price
                priceWithTax
                sku
            }
            featuredAsset {
                ...Asset
            }
            assets {
                ...Asset
            }
            collections {
                id
                slug
                breadcrumbs {
                    id
                    name
                    slug
                }
            }
            customFields {
                metaTitle
                metaDescription
                metaKeywords
            }
        }
    }
    ${ASSET_FRAGMENT}
`;

However it keeps throwing the error:
error TS2339: Property 'customFields' does not exist on type '{ __typename?: "Product" | undefined; } & Pick<Product, "id" | "name" | "description"> & { variants: ({ __typename?: "ProductVariant" | undefined; } & Pick<ProductVariant, "id" | ... 3 more ... | "sku"> & { ...; })[]; featuredAsset?: ({ ...; } & ... 2 more ... & { ...; }) | undefined; assets: ({ ...; } & ... 2 more ...'.

I have referred to the docs on the customFields, i have tried adding

declare module '@vendure/core' {
  interface CustomProductFields {
    metaTitle: string;
    metaDescription: string;
    metaKeywords: string;
  }
}

to vendure-config.ts file before the export. Maybe this is incorrect, i'm not to sure as i said i'm rather new and trying to get my head around this. It feels like i need to add something else to my angular frontend config to get it to work, or define something, but i'm not sure.

The full vendure config, only thing different from the original github version is the custom fields,

import {
    dummyPaymentHandler,
    DefaultJobQueuePlugin,
    DefaultSearchPlugin,
    VendureConfig,
    LanguageCode
} from '@vendure/core'; 


import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
import { AssetServerPlugin } from '@vendure/asset-server-plugin';
import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
import path from 'path';

export const config: VendureConfig = {
    apiOptions: {
        port: 3000,
        adminApiPath: 'admin-api',
        adminApiPlayground: {
            settings: {
                'request.credentials': 'include',
            } as any,
        },// turn this off for production
        adminApiDebug: true, // turn this off for production
        shopApiPath: 'shop-api',
        shopApiPlayground: {
            settings: {
                'request.credentials': 'include',
            } as any,
        },// turn this off for production
        shopApiDebug: true,// turn this off for production
    },
    authOptions: {
        superadminCredentials: {
            identifier: 'superadmin',
            password: 'superadmin',
        },
    },
    dbConnectionOptions: {
        type: 'mysql',
        synchronize: true, // turn this off for production
        logging: false,
        database: 'vendure',
        host: 'localhost',
        port: 3306,
        username: 'root',
        password: '',
        migrations: [path.join(__dirname, '../migrations/*.ts')],
    },
    paymentOptions: {
        paymentMethodHandlers: [dummyPaymentHandler],
    },
    customFields: {
        Product: [
          { 
              name: 'metaTitle', 
              type: 'string', 
              label: [ { languageCode: LanguageCode.en, value: 'Meta Title' } ] 
          },
          { 
              name: 'metaDescription', 
              type: 'string', 
              label: [ { languageCode: LanguageCode.en, value: 'Meta Description' } ] 
          },
          { 
              name: 'metaKeywords', 
              type: 'string', 
              label: [ { languageCode: LanguageCode.en, value: 'Meta Keywords' } ] 
          }
        ]
    },
    plugins: [
        AssetServerPlugin.init({
            route: 'assets',
            assetUploadDir: path.join(__dirname, '../static/assets'),
        }),
        DefaultJobQueuePlugin,
        DefaultSearchPlugin,
        EmailPlugin.init({
            devMode: true,
            outputPath: path.join(__dirname, '../static/email/test-emails'),
            route: 'mailbox',
            handlers: defaultEmailHandlers,
            templatePath: path.join(__dirname, '../static/email/templates'),
            globalTemplateVars: {
                // The following variables will change depending on your storefront implementation
                fromAddress: '"example" <noreply@example.com>',
                verifyEmailAddressUrl: 'http://localhost:8080/verify',
                passwordResetUrl: 'http://localhost:8080/password-reset',
                changeEmailAddressUrl: 'http://localhost:8080/verify-email-address-change'
            },
        }),
        AdminUiPlugin.init({
            route: 'admin',
            port: 3002,
        }),
    ],
};

any help would be appreciated, using mac Mojave, node v14.17.0 if that makes any difference.

Hi,

There are 2 aspects to the typings of the custom fields:

  1. The typings of the server-side entities. This is covered by the declare module '@vendure/core' { ... code above. This is only needed when you are developing plugins for your Vendure server and need to access custom fields in a type-safe way.
  2. The typings for the storefront. In this repo, these typings are generated by graphql-code-generator. There is some documentation about it here: https://github.com/vendure-ecommerce/storefront#code-generation

So based on what you wrote above, you are interested in number 2 above. Since you have already added the custom field field selection to your graphql document, you should be able to npm run generate-types and then have the generated typings automatically updated with your custom fields.