prismic-client icon indicating copy to clipboard operation
prismic-client copied to clipboard

A helper to add/remove/edit data in a Slice Zone's Slices (`mapSliceZone()`)

Open angeloashmore opened this issue 2 years ago • 2 comments

Is your feature request related to a problem? Please describe.

By default, Slice Zones only contain data provided directly from the Prismic API. This is sufficient in most cases, but extra data, or less data in some cases, is needed.

Developers can modify their Slice Zone data manually today using Array.prototype.map() and custom logic, but the solution is not straightforward. Anyone needing this functionality will need to implement it ad-hoc.

Describe the solution you'd like

We could provide a mapSliceZone() function that accepts a Slice Zone array and an object of functions that map a particular type of Slice to some other obhject. The function could add, edit, or remove data from a Slice.

The following example reshapes Code Block Slices (id: code_block) by replacing it with an object containing a single codeHTML property. The property contains HTML produced from a highlight() function, which could be a function that calls a syntax highlighting library, like Shiki.

import { mapSliceZone } from "@prismicio/client";

import { highlight } from "@/lib/highlight";

const mappedSliceZone = await mapSliceZone(page.data.slices, {
  code_block: async ({ slice }) => ({
    codeHTML: await highlight(slice.primary.code, slice.primary.language),
  }),
});

Any occurance of a Code Block Slice will be replaced with the mapped version. Note how the function provided to the code_block property receives the slice object. Other data is included, such as slices, and index.

A third parameter (context) can be provided to unstable_mapSliceZone(), whose contents will be passed to the function in the context property. The context object is most helpful when mapping functions are defined outside the unstable_mapSliceZone() calling scope, such as in a different module.

const mappedSliceZone = await mapSliceZone(
  page.data.slices,
  {
    code_block: async ({ slice, context }) => ({
      codeHTML: await highlight(slice.primary.code, slice.primary.language),
    }),
  },
  context
);

Describe alternatives you've considered

Developers can use Array.prototype.map() directly and implement custom logic. This helper would do exactly that, but with a framework around how to define that custom logic, along with thorough TypeScript types.

Additional context

This feature request has been prototyped internally.

angeloashmore avatar May 31 '23 21:05 angeloashmore

An initial implementation is being developed in #302. It is being exposed as unstable_mapSliceZone(), notably with the unstable_ prefix as the implementation and name could change.

The following serves as documentation for unstable_mapSliceZone(). The guide is specific to Next.js, but could be generalized to any JavaScript project that can run async functions.


Things to know

  • It is not designed to work with Slice Simulator at this time. Slice Simulator needs to be modified to work with server-side data fetching before Slice mappers can be used.
  • The names “mapper” and “mapSliceToProps” are temporary.
  • Sync or async functions are supported.
  • Mapper functions can add data to a Slice, but also reduce data sent across the network. When trying this new functionality, try to trim down the data returned by getStaticProps(); do as much pre-processing on the server as possible.

How to use

1. Create a mapper

  1. In a Slice’s directory (e.g. src/slices/CallToAction), create a new file named mapper.ts.

  2. Copy the following contents into the file (a CallToAction Slice is used as an example):

    // src/slices/CallToAction/mapper.ts
    
    import { Content } from "@prismicio/client";
    
    import { Mapper } from "@/components/SliceZone";
    
    import { CallToActionProps } from ".";
    
    const mapSliceToProps: Mapper<
      Content.CallToActionSlice,
      CallToActionProps
    > = async () => {
      return {
        // Add your modified Slice props here.
      };
    };
    
    export default mapSliceToProps;
    

    Note that the mapSliceToProps() function is typed using Mapper. The Mapper type accepts two type parameters: the mapper’s Slice type and the Slice’s component props type.

  3. Add your custom props to mapSliceToProps()'s return value. This object will become your Slice’s props, replacing the default { slice, slices, index, context } props.

2. Create a mapper record

  1. In the Slice Library’s root directory, create a new file named mappers.ts adjacent to the Slice Machine-generated index.js file.

  2. Copy the following contents into the file (a CallToAction Slice is used as an example):

    import { Mappers } from "@prismicio/client";
    
    export const mappers = {
      call_to_action: () => import("./CallToAction/mapper"),
    } satisfies Mappers;
    
  3. Anytime a new mapper is created, add it to this record using the Slice API ID as a key and the lazy-loaded mapper function as a value. It will automatically be picked up by the mapSliceZone() function in getStaticProps().

3. Map Slices in getStaticProps()

  1. In your page’s getStaticProps() function, query your document.

  2. In a separate variable, use the mapSliceZone() function to trigger your mapper functions.

    import { GetStaticPropsContext } from "next";
    
    import { mapSliceZone } from "@/components/SliceZone";
    
    export async function getStaticProps({ previewData }: GetStaticPropsContext) {
      const client = createClient({ previewData });
    
      const page = await client.getByUID("page", "home");
      const slices = await mapSliceZone(page.data.slices, mappers);
    
      return {
        props: {
          slices,
        },
      };
    }
    
  3. In your page’s component, pass slices to <SliceZone>. The final page should look like this:

    import { GetStaticPropsContext, InferGetStaticPropsType } from "next";
    
    import { createClient } from "../../prismicio";
    import { components } from "@/slices";
    import { mappers } from "@/slices/mappers";
    
    import { mapSliceZone, SliceZone } from "@/components/SliceZone";
    
    type PageProps = InferGetStaticPropsType<typeof getStaticProps>;
    
    export default function Page({ slices }: PageProps) {
      return <SliceZone slices={slices} components={components} />;
    }
    
    export async function getStaticProps({ previewData }: GetStaticPropsContext) {
      const client = createClient({ previewData });
    
      const page = await client.getByUID("page", "home");
      const slices = await mapSliceZone(page.data.slices, mappers);
    
      return {
        props: {
          slices,
        },
      };
    }
    

Feedback

Please leave any feedback on the API, naming, or anything else in this issue. Thanks!

angeloashmore avatar May 31 '23 21:05 angeloashmore

unstable_mapSliceZone() is available as of v7.1.0.

angeloashmore avatar Jun 07 '23 23:06 angeloashmore