Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Typesafe config / no code component definitions #56

Open
timoconnellaus opened this issue May 27, 2024 · 4 comments
Open

Feature: Typesafe config / no code component definitions #56

timoconnellaus opened this issue May 27, 2024 · 4 comments

Comments

@timoconnellaus
Copy link

This repo is the POC of typesafe of config / no code component definitions using zod style notiations

https://github.com/timoconnellaus/eb

@timoconnellaus
Copy link
Author

@pawel-commerce-studio - @r00dY said I should share this here for you. You can see docs on the repo above - and I have published the package too but it's a work in progress so don't count on it working great yet!

@pawel-commerce-studio
Copy link
Collaborator

Hey @timoconnellaus !

I went through the code only, but this looks really exciting. I also wanted to do something similar, but more in a Sanity-like way, but this Zod-like style also looks cool. I will try to play with it soon too!

@timoconnellaus
Copy link
Author

@pawel-commerce-studio - I pushed quite a big update. It's a lot better now! Includes these:

  • widget types are carried through to definitions - including the data type (for styles) and the ids to choose from
  • widget components receive types automatically from definition
  • select props work now - they resolve to a union type for the styles function

@timoconnellaus
Copy link
Author

@pawel-commerce-studio @r00dY - A sneak peak of what I'm close to finishing

This is type safe top-to-bottom - it works like this:

define baseConfig

  • define your devices
  • define your widgets - including their types
  • for external widgets, define your callback function to get the data - with type safety
  • define your tokens including their types
  • baseConfig exports inlineType, externalType, tokenType
  • zod has inbuilt validation - this will be as the validation function for the schema (coming later)

define your easyblocks types

  • these are dependent on your widgets and tokens types - hence we force the above to be defined first
  • when you define your types they are all handled with type safety from your widgets / tokens
  • this step exports schema, definition and all your props for the next step

define your no code component definitions

  • these are dependent on your easyblocks types - hence we force types to be defined first
  • define your schema - including with groups nested (it looks nice when devving this way!)
  • add custom types (inline, external, token) - they're type safe!

The final result is you can build no code components in a completely type safe way - change a type on a widget, get lint errors in your styles function

Here's a simple example

const productWidget = eb.externalWidget({
  zodType: z.object({ productId: z.string() }),
  component: (props) => {
    return <>{props.id}</>;
  },
  type: "shopify",
  callback: async ({ externalData, externalDataId }) => {
    return {
      a: { type: "text", value: { productId: "asb" } },
      b: { type: "text", value: { productId: "asb" } },
    };
  },
});

// ....

const banner = definition({
  schema: schema({
    size: group({
      height: numberProp().defaultValue(10),
      width: numberProp().defaultValue(10),
    }),
    product: externalCustom("product"),
  }),
  styles: ({ values }) => {
    const { height, width, product } = values;

    return {};
  },
}); 

This is the type of the product when you hover in VSCode

image

Here's the full example

import { z } from "zod";
import { eb } from "./eb";

// DEVICES
const devices = eb
  // initially set to default devices
  .devices({
    sm: eb.device().hidden(true), // change properties
  })
  .mainDevice("md"); // we can set the main device

// WIDGETS
const urlWidget = eb
  .inlineWidget({
    zodType: z.object({ val: z.string() }),
    defaultValue: { val: "" },
    component: (props) => {
      return <>{props.value.val}</>;
    },
  })
  .label("URL");

const urlWidgetTwo = eb
  .inlineWidget({
    zodType: z.object({ two: z.string() }),
    defaultValue: { two: "" },
    component: (props) => {
      return <>{props.value.two}</>;
    },
  })
  .label("URL2");

const colorWidget = eb.tokenWidget({
  zodType: z.string(),
  defaultValue: "#000000",
  component: (props) => {
    return <>{props.value}</>;
  },
});

const productWidget = eb.externalWidget({
  zodType: z.object({ productId: z.string() }),
  component: (props) => {
    return <>{props.id}</>;
  },
  type: "shopify",
  callback: async ({ externalData, externalDataId }) => {
    return {
      a: { type: "text", value: { productId: "asb" } },
      b: { type: "text", value: { productId: "asb" } },
    };
  },
});

const widgets = eb.widgets({
  inline: {
    url: urlWidget,
    urlTwo: urlWidgetTwo,
  },
  token: {
    color: colorWidget,
  },
  external: {
    product: productWidget,
  },
});

// TOKENS
const colorTokens = eb
  .colorTokens({
    blue: eb.colorToken({ $res: true, xs: "#0000FF", xl: "#0000FF" }),
    red: eb.colorToken("red"),
  })
  .default("blue");

const customToken = eb
  .customTokens({
    zodType: z.string(),
    tokens: {
      big: eb.customToken("big"),
      small: eb.customToken("small"),
    },
  })
  .default("big");

const customToken2 = eb
  .customTokens({
    zodType: z.number(),
    tokens: {
      big: eb.customToken(10),
      small: eb.customToken(5),
    },
  })
  .default("big");

const tokens = eb.tokens({
  standard: {
    color: colorTokens,
  },
  custom: {
    size: customToken,
    size2: customToken2,
  },
});

// Base Config
// This config sets up tokens and widgets whose types need to be available when setting up easyblocks types
const { inlineType, externalType, tokenType, baseConfigWithTypes } =
  eb.baseConfig({
    devices: devices,
    widgets,
    tokens,
  });

const urlType = inlineType("url").defaultValue({ val: "www.google.com" });
const colorType = tokenType("color").customValueWidget("color");
const productType = externalType(["product"]);

// Now we add the types to the base config - we now have everything we need to set up definitions
// in a type safe way
const {
  definition,
  schema,
  stringProp,
  numberProp,
  inlineCustom,
  tokenCustom,
  externalCustom,
  group,
} = baseConfigWithTypes({
  inlineTypes: {
    url: urlType,
  },
  tokenTypes: {
    color: colorType,
  },
  externalTypes: {
    product: productType,
  },
});

export const s = schema({
  size: group({
    height: numberProp().defaultValue(10),
    width: numberProp().defaultValue(10),
  }),
  title: stringProp().defaultValue("Banner"),
  url: inlineCustom("url"),
  color: tokenCustom("color"),
  product: externalCustom("product"),
});

const banner = definition({
  schema: schema({
    size: group({
      height: numberProp().defaultValue(10),
      width: numberProp().defaultValue(10),
    }),
    title: stringProp().defaultValue("Banner"),
    url: inlineCustom("url"),
    color: tokenCustom("color"),
    product: externalCustom("product"),
  }),
  styles: ({ values }) => {
    const { height, width, title, url, color, product } = values;

    return {};
  },
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants