Skip to content

feat: support slot() field factory in react-native, sveltekit and vue-quasar #8

@wilcorrea

Description

@wilcorrea

Contexto

O PR #7 introduziu o slot() field factory no @ybyra/core e a implementação do lado do rendering no @ybyra/react-web. Esta issue rastreia a implementação equivalente nos demais frameworks.

O que o slot()

O slot() é um field factory que permite embutir qualquer componente customizado dentro de um DataForm sem precisar registrar renderers globais. É ideal para páginas read-only que precisam exibir elementos visuais específicos (badges de status, listas formatadas, viewers de JSON, timelines) em posições determinadas pelo layout do schema.

// schema.ts
import { slot, text } from '@ybyra/core'

const fields = {
  id:     text().disabled(),
  status: slot().width(50),
  result: slot(),
}

Todos os modificadores normais de campo funcionam: .width(), .hidden(), .scopes(), .group().

Estado atual

Pacote Core (slot()) Rendering
@ybyra/react-web ✅ PR #7 ✅ PR #7
@ybyra/react-native ✅ PR #7 ❌ pendente
@ybyra/sveltekit ✅ PR #7 ❌ pendente

O comportamento atual nos 3 frameworks sem a mudança: slot fields somem silenciosamente (sem renderer registrado, o campo é omitido). Não quebra nada.


Implementação por framework

React Native

Mudança idêntica ao react-web. O FieldsGrid.tsx precisa aceitar um slots prop e lidar com component === 'slot':

// FieldsGrid.tsx
export function FieldsGrid({ fields, getFieldProps, slots }: FieldsGridProps) {
  return (
    <View>
      {fields.map((field) => {
        if (field.proxy.hidden) return null

        const isSlot = field.config.component === 'slot'
        const SlotRenderer = isSlot ? slots?.[field.name] : undefined


        if (isSlot && SlotRenderer) {
          const { value, proxy, scope, domain } = getFieldProps(field.name)
          return (
            <View key={field.name}>
              <SlotRenderer domain={domain} name={field.name} value={value} proxy={proxy} scope={scope} />
            </View>
          )
        }

        const Renderer = getRenderer(field.config.component)
        return (
          <View key={field.name}>
            <Renderer {...getFieldProps(field.name)} />
          </View>
        )
      })}
    </View>
  )
}

O Form.tsx precisa aceitar e repassar o slots prop (mesma mudança do react-web).

API de uso:

<DataForm
  schema={schema}
  scope={Scope.view}
  component={component}
  slots={{
    status: ({ value }) => <StatusBadge status={value as string} />,
    result: ({ value }) => <Text>{String(value)}</Text>,
  }}
/>

SvelteKit

Em Svelte 5, o idiomático é usar snippets como props. O DataForm.svelte precisa aceitar slots como um record de snippets e renderizá-los com {@render}:

<script lang="ts">
  import type { Snippet } from 'svelte'

  let props: UseDataFormOptions & {
    debug?: boolean
    slots?: Record<string, Snippet<[{ domain: string; name: string; value: unknown; proxy: FieldProxy; scope: ScopeValue }]>>
  } = ()
</script>

{#each section.fields as field (field.name)}
    {#if field.config.component === 'slot' && props.slots?.[field.name]}
      {@const fieldProps = form.getFieldProps(field.name)}
      <div style="width: {field.proxy.width}%">
        {@render props.slots[field.name](fieldProps)}
      </div>
    {:else}
      {@const renderer = getRenderer(field.config.component)}
      {@const fieldProps = form.getFieldProps(field.name)}
      {#if renderer}
        <div style="width: {field.proxy.width}%">
          <svelte:component this={renderer} {...fieldProps} />
        </div>
      {/if}
    {/if}
  {/if}
{/each}

API de uso:

<DataForm {schema} {scope} {component} slots={{ status: statusSnippet }}>
  {#snippet statusSnippet({ value })}
    <StatusBadge status={value} />
  {/snippet}
</DataForm>

Vue + Quasar

Vue tem scoped slots nativos, que são a forma mais idiomática para esse caso. O DataForm.vue precisa expor um <slot> nomeado para cada campo do tipo slot:

<template>
  <template v-for="field in section.fields" :key="field.name">

      <template v-if="field.config.component === 'slot'">
        <slot
          :name="field.name"
          v-bind="form.getFieldProps(field.name)"
        />
      </template>

      <component
        v-else-if="getRenderer(field.config.component)"
        :is="getRenderer(field.config.component)"
        v-bind="form.getFieldProps(field.name)"
      />
    </div>
  </template>
</template>

API de uso — 100% idiomático Vue:

<DataForm :schema="schema" :scope="Scope.view" :component="component">
  <template #status="{ value }">
    <StatusBadge :status="value" />
  </template>
  <template #result="{ value }">
    <pre>{{ value }}</pre>
  </template>
</DataForm>

Comparação de API entre frameworks

React Web / Native          SvelteKit                    Vue + Quasar
  ───────────────────────────  ──────────────────────────
<DataForm                   <DataForm                    <DataForm
  schema={schema}             {schema} {scope}             :schema="schema"
  slots={{                    slots={{ status: snip }}>    :scope="scope">
    status: ({ value }) =>    {#snippet snip({ value })}   <template #status="{value}">
      <Badge s={value} />,      <Badge s={value} />          <Badge :s="value" />
  }}                          {/snippet}                 </template>
/>                          </DataForm>                  </DataForm>

Critério de aceite

  • @ybyra/react-native: FieldsGrid e Form suportam slots prop
  • @ybyra/sveltekit: DataForm.svelte suporta snippets como slots prop
  • @ybyra/vue-quasar: DataForm.vue expõe scoped slots nomeados para campos do tipo slot
  • Documentação atualizada em docs/ para cada framework

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions