easyblockshq / easyblocks

The open-source visual builder framework.
https://easyblocks.io
GNU Affero General Public License v3.0
313 stars 44 forks source link

Feature: Typesafe config / no code component definitions #56

Open timoconnellaus opened 4 months ago

timoconnellaus commented 4 months ago

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

https://github.com/timoconnellaus/eb

timoconnellaus commented 4 months ago

@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 commented 4 months ago

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 commented 4 months ago

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

timoconnellaus commented 4 months ago

@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 easyblocks types

define your no code component definitions

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 {};
  },
});