Skip to content

Commit

Permalink
refactor: add typing to componment renderer, refactor imports and fra…
Browse files Browse the repository at this point in the history
…gments
  • Loading branch information
asgorobets committed Nov 13, 2024
1 parent 008be46 commit e78ee8c
Show file tree
Hide file tree
Showing 17 changed files with 347 additions and 324 deletions.
8 changes: 5 additions & 3 deletions app/[locale]/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ComponentDuplexFieldsFragment } from '#/components/duplex-ctf/duplex-ct
import { ComponentHeroBannerFieldsFragment } from '#/components/hero-banner-ctf/hero-banner-ctf';
import { LanguageDataSetter } from '#/components/language-data-provider/language-data-provider';
import { ComponentSEOFieldsFragment, getSeoMetadata } from '#/components/seo/seo-ctf';
import { ComponentTopicBusinessInfoFieldsFragment } from '#/components/topic-business-info/topic-business-info';
import { TopicBusinessInfoFieldsFragment } from '#/components/topic-business-info/topic-business-info';
import { addContentSourceMaps } from '#/lib/contentSourceMaps';
import { graphqlClient } from '#/lib/graphqlClient';
import { getLocaleFromPath } from '#/locales/get-locale-from-path';
Expand All @@ -29,20 +29,22 @@ const getPage = async (slug: string, locale: string, preview = false) => {
items {
topSectionCollection(limit: 10) {
items {
__typename
...ComponentHeroBannerFields
...ComponentDuplexFields
}
}
pageContent {
...ComponentTopicBusinessInfo
__typename
...TopicBusinessInfo
}
slugEn: slug(locale: "en-US")
slugDe: slug(locale: "de-DE")
}
}
}
`,
[ComponentHeroBannerFieldsFragment, ComponentDuplexFieldsFragment, ComponentTopicBusinessInfoFieldsFragment]
[ComponentHeroBannerFieldsFragment, ComponentDuplexFieldsFragment, TopicBusinessInfoFieldsFragment]
);

const response = await graphqlClient(preview).query(pageQuery, {
Expand Down
6 changes: 3 additions & 3 deletions cli/scaffolds/components/{{hyphenCase name}}-client.{{ext}}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

import { ResultOf } from 'gql.tada';

import { Component{{pascalCase name}}FieldsFragment } from '#/components/{{hyphenCase name}}/{{hyphenCase name}}';
import { {{pascalCase name}}Fragment } from '#/components/{{hyphenCase name}}/{{hyphenCase name}}';
import { {{pascalCase name}} } from '#/components/ui/{{hyphenCase name}}';

import { useComponentPreview } from '../hooks/use-component-preview';

export const {{pascalCase name}}Client: React.FC<{
data: ResultOf<typeof Component{{pascalCase name}}FieldsFragment>;
data: ResultOf<typeof {{pascalCase name}}Fragment>;
}> = (props) => {
const { data: originalData } = props;
const { data, addAttributes } = useComponentPreview<typeof originalData>(originalData);
const { data, addAttributes } = useComponentPreview(originalData);

return <{{pascalCase name}} id={data.sys.id} addAttributes={addAttributes} />;
};
8 changes: 4 additions & 4 deletions cli/scaffolds/components/{{hyphenCase name}}.{{ext}}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { FragmentOf, graphql, readFragment } from 'gql.tada';

import { {{pascalCase name}}Client } from './{{hyphenCase name}}-client';

export const Component{{pascalCase name}}FieldsFragment = graphql(`
fragment Component{{pascalCase name}} on {{pascalCase name}} {
export const {{pascalCase name}}Fragment = graphql(`
fragment {{pascalCase name}} on {{pascalCase name}} {
__typename
sys {
id
Expand All @@ -12,10 +12,10 @@ export const Component{{pascalCase name}}FieldsFragment = graphql(`
`);

export type {{pascalCase name}}Props = {
data: FragmentOf<typeof Component{{pascalCase name}}FieldsFragment>;
data: FragmentOf<typeof {{pascalCase name}}Fragment> & Record<string, any>;
};

export const {{pascalCase name}}: React.FC<{{pascalCase name}}Props> = (props) => {
const data = readFragment(Component{{pascalCase name}}FieldsFragment, props.data);
const data = readFragment({{pascalCase name}}Fragment, props.data);
return <{{pascalCase name}}Client data={data} />;
};
11 changes: 6 additions & 5 deletions cli/utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const scaffoldComponentFiles = (contentType, updateMappings) => {
const config = {
name: contentType,
data: {
ext: 'tsx'
ext: 'tsx',
},
templates: [path.join(__dirname, 'scaffolds', 'components'), path.join(__dirname, 'scaffolds', 'ui')],
createSubFolder: true,
Expand All @@ -82,11 +82,12 @@ export const scaffoldComponentFiles = (contentType, updateMappings) => {
.then(() => {
const mappingFilePath = path.join(__dirname, '../components/component-renderer/mappings.ts');
const mappings = fs.readFileSync(mappingFilePath, 'utf-8').split('\n');
mappings.splice(
-2,
0,
` ${hyphenToPascal(contentType)}: dynamic(() => import(\'#/components/${contentType}\').then((mod) => mod.${hyphenToPascal(contentType)})),`
// Add component import to the top of the file.
mappings.unshift(
`import { ${hyphenToPascal(contentType)} } from '#/components/${contentType}/${contentType}';`
);
// Add the new component to the mappings file.
mappings.splice(-2, 0, ` ${hyphenToPascal(contentType)}: ${hyphenToPascal(contentType)},`);
const updatedMappings = mappings.join('\n');
fs.writeFileSync(mappingFilePath, updatedMappings, { encoding: 'utf-8' });
})
Expand Down
55 changes: 35 additions & 20 deletions components/component-renderer/component-renderer.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,49 @@
/**
* ComponentRenderer is a component that renders components based on the data supplied.
*
* A few ground principles for ComponentRenderer
*
* 1. It should accept data and decide which component(s) to render
* 2. It should allow arrays of data to be passed and handle null/undefined
* 3. As a proxy, it should let you know if it can't render the component you're asking because you didn't provide enough data.
* 4. It should be able to render the component(s) with the data you provided.
* 5. It should skip rendering if component is not found in the componentMap.
*/

import { componentMap } from './mappings';
import { ComponentProps } from 'react';

type ComponentMapType = typeof componentMap;
type Data = ComponentProps<ComponentMapType[ComponentKey]>['data'];
type ComponentKey = keyof ComponentMapType;
type DataWithTypename = (Data & { __typename: string }) | { __typename: string } | null;

function isComponentKey(key: string): key is ComponentKey {
return key in componentMap;
}

export default function ComponentRenderer({
data,
}: {
data: any; // @TODO: Fixme
}) {
export default function ComponentRenderer<T extends DataWithTypename | DataWithTypename[]>({ data }: { data: T }) {
if (data === null) {
return null;
}

// If we have an array, we render each item in the array
if (Array.isArray(data)) {
return (
<>
{data.map((item) => {
if (!item?.sys?.id) {
return null;
}

return <ComponentRenderer key={item.sys.id} data={item} />;
{data.map((item, index) => {
return <ComponentRenderer key={index} data={item} />;
})}
</>
);
}

if (!data?.__typename) {
return null;
}

// @TODO: Fix typings for componentMap.
// @ts-ignore
const Component = componentMap[data.__typename];
if (Component) {
return <Component data={data} />;
if (isComponentKey(data.__typename)) {
const Component = componentMap[data.__typename];
// At this point we know data is one of the accepted props for the component
return <Component data={data as any} />;
}

// If we don't know the component, we don't render anything
return null;
}
15 changes: 9 additions & 6 deletions components/component-renderer/mappings.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import dynamic from 'next/dynamic';
import { HeroBannerCtf } from '#/components/hero-banner-ctf/hero-banner-ctf';
import { DuplexCtf } from '#/components/duplex-ctf/duplex-ctf';
import { TopicBusinessInfo } from '#/components/topic-business-info/topic-business-info';
import { TopicPerson } from '#/components/topic-person/topic-person';

export const componentMap = {
ComponentHeroBanner: dynamic(() => import('#/components/hero-banner-ctf').then((mod) => mod.HeroBannerCtf)),
ComponentDuplex: dynamic(() => import('#/components/duplex-ctf').then((mod) => mod.DuplexCtf)),
TopicBusinessInfo: dynamic(() => import('#/components/topic-business-info').then((mod) => mod.TopicBusinessInfo)),
TopicPersons: dynamic(() => import('#/components/topic-person').then((mod) => mod.TopicPerson)),
};
ComponentHeroBanner: HeroBannerCtf,
ComponentDuplex: DuplexCtf,
TopicBusinessInfo: TopicBusinessInfo,
TopicPersons: TopicPerson,
} as const;
2 changes: 1 addition & 1 deletion components/duplex-ctf/duplex-ctf-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const DuplexCtfClient: React.FC<{
data: ResultOf<typeof ComponentDuplexFieldsFragment>;
}> = (props) => {
const { data: originalData } = props;
const { data, addAttributes } = useComponentPreview<typeof originalData>(originalData);
const { data, addAttributes } = useComponentPreview(originalData);
const { track } = useAnalytics();
return (
<TrackInView {...analyticsInViewEvent}>
Expand Down
2 changes: 1 addition & 1 deletion components/duplex-ctf/duplex-ctf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const ComponentDuplexFieldsFragment = graphql(
);

export type DuplexProps = {
data: FragmentOf<typeof ComponentDuplexFieldsFragment>;
data: FragmentOf<typeof ComponentDuplexFieldsFragment> & Record<string, any>;
};

export const DuplexCtf: React.FC<DuplexProps> = (props) => {
Expand Down
2 changes: 1 addition & 1 deletion components/hero-banner-ctf/hero-banner-ctf-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const HeroBannerCtfClient: React.FC<{
data: ResultOf<typeof ComponentHeroBannerFieldsFragment>;
}> = (props) => {
const { data: originalData } = props;
const { data, addAttributes } = useComponentPreview<typeof originalData>(originalData);
const { data, addAttributes } = useComponentPreview(originalData);
// We use createAnalyticsEvent helper to create typed event.
const analyticsInViewEvent = createAnalyticsEvent('heroBannerViewed', {
category: 'duplexViewed',
Expand Down
2 changes: 1 addition & 1 deletion components/hero-banner-ctf/hero-banner-ctf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const ComponentHeroBannerFieldsFragment = graphql(
);

export type HeroBannerProps = {
data: FragmentOf<typeof ComponentHeroBannerFieldsFragment>;
data: FragmentOf<typeof ComponentHeroBannerFieldsFragment> & Record<string, any>;
};

export const HeroBannerCtf: React.FC<HeroBannerProps> = (props) => {
Expand Down
6 changes: 4 additions & 2 deletions components/hooks/use-component-preview.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
'use client';

import { Argument } from '@contentful/live-preview/dist/types';
import { Entity } from '@contentful/live-preview/dist/types';
import { useContentfulInspectorMode, useContentfulLiveUpdates } from '@contentful/live-preview/react';

export const useComponentPreview = <T extends Argument>(data: (typeof useContentfulLiveUpdates)['arguments'][0]) => {
type Argument = Entity & { sys: { id: string } };

export const useComponentPreview = <T extends Argument>(data: T) => {
const previewData = useContentfulLiveUpdates<T>(data);
const inspectorProps = useContentfulInspectorMode({
entryId: data.sys.id,
Expand Down
6 changes: 3 additions & 3 deletions components/topic-business-info/topic-business-info-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import { RichTextCtf } from '#/components/rich-text-ctf';
import { TopicBusinessInfo } from '#/components/ui/topic-business-info';

import { useComponentPreview } from '../hooks/use-component-preview';
import { ComponentTopicBusinessInfoFieldsFragment } from './topic-business-info';
import { TopicBusinessInfoFieldsFragment } from './topic-business-info';

export const TopicBusinessInfoClient: React.FC<{
data: ResultOf<typeof ComponentTopicBusinessInfoFieldsFragment>;
data: ResultOf<typeof TopicBusinessInfoFieldsFragment>;
}> = (props) => {
const { data: originalData } = props;
const { data, addAttributes } = useComponentPreview<typeof originalData>(originalData);
const { data, addAttributes } = useComponentPreview(originalData);

return (
<TopicBusinessInfo
Expand Down
14 changes: 7 additions & 7 deletions components/topic-business-info/topic-business-info.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { FragmentOf, graphql, readFragment } from 'gql.tada';

import { ComponentTopicPersonFieldsFragment } from '#/components/topic-person/topic-person';
import { TopicPersonFieldsFragment } from '#/components/topic-person/topic-person';

import { AssetFieldsFragment } from '../asset-ctf';
import { TopicBusinessInfoClient } from './topic-business-info-client';

export const ComponentTopicBusinessInfoFieldsFragment = graphql(
export const TopicBusinessInfoFieldsFragment = graphql(
`
fragment ComponentTopicBusinessInfo on TopicBusinessInfo {
fragment TopicBusinessInfo on TopicBusinessInfo {
__typename
sys {
id
Expand All @@ -19,7 +19,7 @@ export const ComponentTopicBusinessInfoFieldsFragment = graphql(
links {
entries {
block {
...ComponentTopicPerson
...TopicPerson
}
}
}
Expand All @@ -29,14 +29,14 @@ export const ComponentTopicBusinessInfoFieldsFragment = graphql(
}
}
`,
[AssetFieldsFragment, ComponentTopicPersonFieldsFragment]
[AssetFieldsFragment, TopicPersonFieldsFragment]
);

export type TopicBusinessInfoProps = {
data: FragmentOf<typeof ComponentTopicBusinessInfoFieldsFragment>;
data: FragmentOf<typeof TopicBusinessInfoFieldsFragment & Record<string, any>>;
};

export const TopicBusinessInfo: React.FC<TopicBusinessInfoProps> = (props) => {
const data = readFragment(ComponentTopicBusinessInfoFieldsFragment, props.data);
const data = readFragment(TopicBusinessInfoFieldsFragment, props.data);
return <TopicBusinessInfoClient data={data} />;
};
6 changes: 3 additions & 3 deletions components/topic-person/topic-person-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import { RichTextCtf } from '#/components/rich-text-ctf';
import { TopicPerson } from '#/components/ui/topic-person';

import { useComponentPreview } from '../hooks/use-component-preview';
import { ComponentTopicPersonFieldsFragment } from '././topic-person';
import { TopicPersonFieldsFragment } from '././topic-person';

export const TopicPersonClient: React.FC<{
data: ResultOf<typeof ComponentTopicPersonFieldsFragment>;
data: ResultOf<typeof TopicPersonFieldsFragment>;
}> = (props) => {
const { data: originalData } = props;
const { data, addAttributes } = useComponentPreview<typeof originalData>(originalData);
const { data, addAttributes } = useComponentPreview(originalData);

return (
<TopicPerson
Expand Down
12 changes: 6 additions & 6 deletions components/topic-person/topic-person.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { FragmentOf, graphql, readFragment } from 'gql.tada';
import { AssetFieldsFragment } from '../asset-ctf';
import { TopicPersonClient } from './topic-person-client';

export const ComponentTopicPersonFieldsFragment = graphql(
export const TopicPersonFieldsFragment = graphql(
`
fragment ComponentTopicPerson on TopicPerson {
fragment TopicPerson on TopicPerson {
__typename
sys {
id
Expand All @@ -25,11 +25,11 @@ export const ComponentTopicPersonFieldsFragment = graphql(
[AssetFieldsFragment]
);

export type TopicBusinessInfoProps = {
data: FragmentOf<typeof ComponentTopicPersonFieldsFragment>;
export type TopicPersonProps = {
data: FragmentOf<typeof TopicPersonFieldsFragment & Record<string, any>>;
};

export const TopicPerson: React.FC<TopicBusinessInfoProps> = (props) => {
const data = readFragment(ComponentTopicPersonFieldsFragment, props.data);
export const TopicPerson: React.FC<TopicPersonProps> = (props) => {
const data = readFragment(TopicPersonFieldsFragment, props.data);
return <TopicPersonClient data={data} />;
};
2 changes: 1 addition & 1 deletion docs/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ Remember why we bother to create a Client Component in the first place? To get R

```jsx
const { data: originalData } = props;
const { data, addAttributes } = useComponentPreview<typeof originalData>(originalData);
const { data, addAttributes } = useComponentPreview(originalData);

return <Accordion
label={data.label}
Expand Down
Loading

0 comments on commit e78ee8c

Please sign in to comment.