wpengine / faust-scaffold-ts

Faust Scaffold Blueprint in TypeScript
13 stars 4 forks source link

Request: show examples of using GraphQL interfaces and (typed) fragment data #9

Open justlevine opened 1 year ago

justlevine commented 1 year ago

WordPress-flavored Headless is simplest when we design our queries using GraphQL interfaces and reusable fragments. We see this pattern in faust already with content blocks.

GraphQL-Codegen handles this with the useFragment() decorator, a recent pattern that that's not the most obvious to implement when using Apollo.

It would be extremely helpful if this scaffold included examples that show users how to effectively employ these patterns with Faust.

For example: a wp-templates/singular that uses nodeByUri to get data for either a post or page, a primaryMenuItems that passes when strict ts is enabled, or a ts-valid setup for Faust Blocks that doesnt throw linting errors against the ContentBlock prop type, since the attribute fragments are only stored as refs until decorated.

blakewilson commented 11 months ago

Thanks for this feedback @justlevine!

We will be addressing this as soon as we get consensus on our RFC for multiple queries and implement this within the blueprints.

mariusorani commented 8 months ago

When i try to add the editorBlocks inside the Component.query of the page.tsx i got this error: Type '{}' is missing the following properties from type 'DocumentNode': kind, definitions

this is the whole page.tsx:

import { gql } from "../__generated__";
import { WordPressBlock } from "@faustwp/blocks";
import Head from "next/head";
import EntryHeader from "../components/entry-header";
import Footer from "../components/footer";
import Header from "../components/header";
import { GetPageQuery } from "../__generated__/graphql";
import { DocumentNode } from 'graphql';
import { FaustTemplate } from "@faustwp/core";
import { flatListToHierarchical } from '@faustwp/core';
import { WordPressBlocksViewer } from '@faustwp/blocks';
import blocks from '../wp-blocks';

const Component: FaustTemplate<GetPageQuery> = ({loading, data}) => {
  // Loading state for previews
  if (loading) {
    return <>Loading...</>;
  }

  const { title: siteTitle, description: siteDescription } =
    data.generalSettings;
  const menuItems = data.primaryMenuItems.nodes;
  const { title, content, editorBlocks} = data.page;
  const blockList = flatListToHierarchical(editorBlocks, { childrenKey: 'innerBlocks' });

  return (
    <>
      <Head>
        <title>{`${title} - ${siteTitle}`}</title>
      </Head>

      <Header
        siteTitle={siteTitle}
        siteDescription={siteDescription}
        menuItems={menuItems}
      />

      <main className="container">
        <EntryHeader title={title} />
        <div dangerouslySetInnerHTML={{ __html: content }} />
        <WordPressBlocksViewer blocks={blockList} />
      </main>

      <Footer />
    </>
  );
};

Component.variables = ({ databaseId }, ctx) => {
  return {
    databaseId,
    asPreview: ctx?.asPreview,
  };
};

Component.query = gql(`
  ${blocks.CoreParagraph.fragments.entry}
  ${blocks.CoreColumns.fragments.entry}
  ${blocks.CoreColumn.fragments.entry}
  ${blocks.CoreCode.fragments.entry}
  ${blocks.CoreButtons.fragments.entry}
  ${blocks.CoreButton.fragments.entry}
  ${blocks.CoreQuote.fragments.entry}
  ${blocks.CoreImage.fragments.entry}
  ${blocks.CoreSeparator.fragments.entry}
  ${blocks.CoreList.fragments.entry}
  ${blocks.CoreHeading.fragments.entry}
  query GetPage($databaseId: ID!, $asPreview: Boolean = false) {
    page(id: $databaseId, idType: DATABASE_ID, asPreview: $asPreview) {
      title
      content
      editorBlocks {
        name
        __typename
        renderedHtml
        id: clientId
        parentId: parentClientId
        ...${blocks.CoreParagraph.fragments.key}
        ...${blocks.CoreColumns.fragments.key}
        ...${blocks.CoreColumn.fragments.key}
        ...${blocks.CoreCode.fragments.key}
        ...${blocks.CoreButtons.fragments.key}
        ...${blocks.CoreButton.fragments.key}
        ...${blocks.CoreQuote.fragments.key}
        ...${blocks.CoreImage.fragments.key}
        ...${blocks.CoreSeparator.fragments.key}
        ...${blocks.CoreList.fragments.key}
        ...${blocks.CoreHeading.fragments.key}
      }
    }
    generalSettings {
      title
      description
    }
    primaryMenuItems: menuItems(where: { location: PRIMARY }) {
      nodes {
        id
        uri
        path
        label
        parentId
        cssClasses
        menu {
          node {
            name
          }
        }
      }
    }
  }
`);

export default Component;

I would be grateful for any help.

justlevine commented 8 months ago

I use WordPressTemplate instead and it works:

import { WordPressTemplate } from '@faustwp/core/dist/mjs/getWordPressProps';
import dynamic from 'next/dynamic';
import { gql } from '@graphqlTypes/gql';

const Component = dynamic( () =>
    import( '../features/WpTemplates/Page' ).then( ( mod ) => mod.Page )
);

const Page: WordPressTemplate = ( props ) => {
    return <Component { ...props } />;
};

Page.variables = ( { uri } ) => {
    return {
        uri,
    };
};

Page.query = gql( `
    query GetPageNode($uri: ID!) {
        siteConfig {
            ...SiteIdentityFrag
            ...FooterSettingsFrag
        }
        ...PrimaryMenuItemsFrag
        currentPage: page(id: $uri, idType: URI) {
            id
            uri
            title
            editorBlocks {
                ...BlockContentEditorBlockFrag
            }
            ...FeaturedImageFrag
            ...NodeWithSeoFrag
            pageSettings {
                axewpPageHeader
            }
        }
    }
` );

export default Page;

The dynamic import is because Faust loads all the possible templates in app.ts 🤷‍♂️ In that case, it's the dynamic component that gets wrapped as a FaustTemplate:

import dynamic from 'next/dynamic';
import { Main } from '@/components/elements/Main/Main';
import { PageHeader } from '@/components/elements/PageHeader/PageHeader';
import { Header } from '@/components/layouts/Header/Header';
import { Layout } from '@/components/layouts/Layout/Layout';
import { BlockContent } from '@/features/WpBlocks/components/BlockContent';
import { getFragmentData } from '@graphqlTypes/fragment-masking';
import {
    MediaItemFragFragmentDoc,
    FeaturedImageFragFragmentDoc,
    FooterSettingsFragFragmentDoc,
    NodeWithSeoFragFragmentDoc,
    PrimaryMenuItemsFragFragmentDoc,
    SiteIdentityFragFragmentDoc,
    BlockContentEditorBlockFragFragment,
    GetPageNodeQuery,
    MenuItemNodeFragFragment,
    SeoFragFragment,
} from '@graphqlTypes/graphql';
import type { FaustTemplate } from '@faustwp/core/dist/mjs/getWordPressProps';

const Footer = dynamic( () =>
    import( '../../components/layouts/Footer/Footer' ).then( ( mod ) => mod.Footer )
);

export const Page: FaustTemplate< GetPageNodeQuery > = ( { data, loading } ) => {
    const identityFrag = getFragmentData( SiteIdentityFragFragmentDoc, data?.siteConfig );
    const logo = getFragmentData(
        MediaItemFragFragmentDoc,
        identityFrag?.identity?.axewpLogoLight?.node
    );

    const footerFrag = getFragmentData( FooterSettingsFragFragmentDoc, data?.siteConfig );
    const footer = footerFrag?.footerSettings?.axewpFooterContent;

    const primaryMenuFrag = getFragmentData( PrimaryMenuItemsFragFragmentDoc, data );
    const menu = primaryMenuFrag?.primaryMenuItems?.nodes;

    const seoFrag = getFragmentData( NodeWithSeoFragFragmentDoc, data?.currentPage );
    const seo = seoFrag?.seo;

    const editorBlocks = data?.currentPage?.editorBlocks;
    const pageTitle =
        data?.currentPage?.pageSettings?.axewpPageHeader || data?.currentPage?.title || '';

    const featuredImageFrag = getFragmentData( FeaturedImageFragFragmentDoc, data?.currentPage );
    const featuredImage = getFragmentData(
        MediaItemFragFragmentDoc,
        featuredImageFrag?.featuredImage?.node
    );

    return (
        <Layout loading={ !! loading } seo={ seo as SeoFragFragment }>
            <Header logo={ logo } menu={ menu as MenuItemNodeFragFragment[] } />
            <Main
                className="wp-block-group is-layout-constrained"
                style={ {
                    marginTop: 'var(--wp--preset--spacing--30)',
                } }
            >
                <PageHeader
                    title={ pageTitle }
                    image={ featuredImage }
                    className="wp-block-group alignwide has-global-padding"
                />
                { !! editorBlocks?.length && (
                    <BlockContent blocks={ editorBlocks as BlockContentEditorBlockFragFragment[] } />
                ) }
            </Main>
            <Footer content={ footer ?? '' } />
        </Layout>
    );
};
mariusorani commented 8 months ago

Thank you for your reply @justlevine! I tried the approach you mentioned above but still no luck, and the same error. :/

import { gql } from "../__generated__";
import { WordPressTemplate } from '@faustwp/core/dist/mjs/getWordPressProps';
import dynamic from 'next/dynamic';
import blocks from '../wp-blocks';

const Component = dynamic( () =>
    import( '../features/WpTemplates/Page' ).then( ( mod ) => mod.Page )
);

const Page: WordPressTemplate = ( props ) => {
    return <Component { ...props } />;
};

Page.variables = ({ databaseId }, ctx) => {
  return {
    databaseId,
    asPreview: ctx?.asPreview,
  };
};

Page.query = gql(`
  query GetPage($databaseId: ID!, $asPreview: Boolean = false) {
    generalSettings {
      title
      description
    }
    primaryMenuItems: menuItems(where: { location: PRIMARY }) {
      nodes {
        id
        uri
        path
        label
        parentId
        cssClasses
        menu {
          node {
            name
          }
        }
      }
    }
    currentPage: page(id: $databaseId, idType: DATABASE_ID, asPreview: $asPreview) {
      title
      content
      editorBlocks {
        name
        __typename
        renderedHtml
        id: clientId
        parentId: parentClientId
        ...${blocks.CoreParagraph.fragments.key}
        ...${blocks.CoreColumns.fragments.key}
        ...${blocks.CoreColumn.fragments.key}
        ...${blocks.CoreCode.fragments.key}
        ...${blocks.CoreButtons.fragments.key}
        ...${blocks.CoreButton.fragments.key}
        ...${blocks.CoreQuote.fragments.key}
        ...${blocks.CoreImage.fragments.key}
        ...${blocks.CoreSeparator.fragments.key}
        ...${blocks.CoreList.fragments.key}
        ...${blocks.CoreHeading.fragments.key}
      }
    }
  }
`);

export default Page;

and the WpTemplates/Page.tsx:

import Head from "next/head";
import EntryHeader from "../../components/entry-header";
import Footer from "../../components/footer";
import Header from "../../components/header";
import { GetPageQuery } from "../../__generated__/graphql";
import type { FaustTemplate } from '@faustwp/core/dist/mjs/getWordPressProps';
import { flatListToHierarchical } from '@faustwp/core';
import { WordPressBlocksViewer } from '@faustwp/blocks';

export const Page: FaustTemplate<GetPageQuery> = ({ data, loading }) => {
    // Loading state for previews
    if (loading) {
      return <>Loading...</>;
    }

    const { title: siteTitle, description: siteDescription } =
      data.generalSettings;
    const menuItems = data.primaryMenuItems.nodes;
    const { title, content, editorBlocks} = data.currentPage;
    const blockList = flatListToHierarchical(editorBlocks, { childrenKey: 'innerBlocks' });

    return (
      <>
        <Head>
          <title>{`${title} - ${siteTitle}`}</title>
        </Head>

        <Header
          siteTitle={siteTitle}
          siteDescription={siteDescription}
          menuItems={menuItems}
        />

        <main className="container">
          <EntryHeader title={title} />
          <div dangerouslySetInnerHTML={{ __html: content }} />
          <WordPressBlocksViewer blocks={blockList} />
        </main>

        <Footer />
      </>
    );
  };

can't figure it out what i'm doing wrong

blakewilson commented 7 months ago

Hi @mariusorani, if you are still experiencing this, could you open a new issue on the faustjs repo?

https://github.com/wpengine/faustjs/issues/new/choose

blakewilson commented 7 months ago

@justlevine Sorry for the lack of traction on this. we've been in the transition from using an internal Jira board to now using GitHub projects. I've added this item to our backlog, but not sure when we can prioritize it.

When we pull this into progress we'll update you here!