Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .size-limit.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[{ "path": "dist/index.js", "limit": "850 B" }]
[{ "path": "dist/index.js", "limit": "900 B" }]
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,49 @@
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](http://semver.org).

## 3.0.0

### Changed

- **BREAKING:** `component` field renamed to `Component` (capital C) to enable React hooks usage in inline components without ESLint warnings

- **BREAKING:** API structure changed to target-first approach

**Before (v2):**

```tsx
slotsApi.insert.into.Description({ component: MyComponent });
```

**After (v3):**

```tsx
slotsApi.Description.insert({ Component: MyComponent });
```

**Benefits:**
- **Discoverability:** Type `slotsApi.` to see all available slots, then `slotsApi.[SlotName].` to see all actions for that slot
- **Logical grouping:** All methods for a specific slot are in one place

### Added

- `clear` method to clear all components from a slot

### Migration from v2 to v3

1. Replace `slotsApi.insert.into.[SlotName]` with `slotsApi.[SlotName].insert`
2. Replace `component:` with `Component:` in all insert calls

**Example:**

```tsx
// v2
slotsApi.insert.into.Header({ component: MyComponent });

// v3
slotsApi.Header.insert({ Component: MyComponent });
```

## 2.0.0

### Changed
Expand Down
90 changes: 62 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,24 +76,21 @@ const Sidebar = () => (
);

// plugin-analytics/index.ts - inject from anywhere!
slotsApi.insert.into.Widgets({
component: () => <AnalyticsWidget />,
slotsApi.Widgets.insert({
Component: () => <AnalyticsWidget />,
});

// plugin-user-stats/index.ts - another plugin
slotsApi.insert.into.Widgets({
component: () => <UserStatsWidget />,
slotsApi.Widgets.insert({
Component: () => <UserStatsWidget />,
});
```

### Result

```tsx
<aside>
<nav>Core navigation</nav>
<AnalyticsWidget />
<UserStatsWidget />
</aside>
// Result:
// <aside>
// <nav>Core navigation</nav>
// <AnalyticsWidget />
// <UserStatsWidget />
// </aside>
```

No props drilling, no boilerplate - just define slots and inject content from anywhere in your codebase.
Expand All @@ -110,7 +107,7 @@ bun add @grlt-hub/react-slots
yarn add @grlt-hub/react-slots
```

TypeScript types are included out of the box.
**Note:** TypeScript types are included out of the box.

### Peer dependencies

Expand Down Expand Up @@ -140,8 +137,8 @@ const App = () => (
);

// 3. Insert content into the slot
slotsApi.insert.into.Footer({
component: () => <p>© 1955–1985–2015 Outatime Corp.</p>,
slotsApi.Footer.insert({
Component: () => <p>© 1955–1985–2015 Outatime Corp.</p>,
});

// Result:
Expand All @@ -165,8 +162,8 @@ const { slotsApi, Slots } = createSlots({
<Slots.UserPanel userId={123} />;

// Insert component - receives props automatically
slotsApi.insert.into.UserPanel({
component: (props) => <UserWidget id={props.userId} />,
slotsApi.UserPanel.insert({
Component: (props) => <UserWidget id={props.userId} />,
});
```

Expand All @@ -179,13 +176,13 @@ const { slotsApi, Slots } = createSlots({

<Slots.UserPanel userId={123} />;

slotsApi.insert.into.UserPanel({
slotsApi.UserPanel.insert({
// Transform userId into userName and isAdmin before passing to component
mapProps: (slotProps) => ({
userName: getUserName(slotProps.userId),
isAdmin: checkAdmin(slotProps.userId),
}),
component: (props) => <UserBadge name={props.userName} admin={props.isAdmin} />,
Component: (props) => <UserBadge name={props.userName} admin={props.isAdmin} />,
});
```

Expand All @@ -195,14 +192,14 @@ Components are inserted in any order, but rendered according to `order` value (l

```tsx
// This is inserted first, but will render second
slotsApi.insert.into.Sidebar({
component: () => <SecondWidget />,
slotsApi.Sidebar.insert({
Component: () => <SecondWidget />,
order: 2,
});

// This is inserted second, but will render first
slotsApi.insert.into.Sidebar({
component: () => <FirstWidget />,
slotsApi.Sidebar.insert({
Component: () => <FirstWidget />,
order: 1,
});

Expand All @@ -215,6 +212,35 @@ slotsApi.insert.into.Sidebar({

**Note:** Components with the same `order` value keep their insertion order and all of them are rendered.

### Clear slot content

Remove all components from a slot:

```tsx
// Insert components
slotsApi.Sidebar.insert({
Component: () => <Widget1 />,
});

slotsApi.Sidebar.insert({
Component: () => <Widget2 />,
});

// Result after inserts:
// <aside>
// <Widget1 />
// <Widget2 />
// </aside>

// Later, clear the slot
slotsApi.Sidebar.clear();

// Result after clear:
// <aside>
// {/* Sidebar slot is now empty */}
// </aside>
```

### Defer insertion until event fires

Wait for data to load before inserting component. The component won't render until the event fires:
Expand All @@ -225,19 +251,27 @@ import { createEvent } from 'effector';
const userLoaded = createEvent<{ id: number; name: string }>();

// Component will be inserted only after userLoaded fires
slotsApi.insert.into.Header({
slotsApi.Header.insert({
when: userLoaded,
mapProps: (slotProps, whenPayload) => ({
userId: whenPayload.id,
userName: whenPayload.name,
}),
component: (props) => <UserWidget id={props.userId} name={props.userName} />,
Component: (props) => <UserWidget id={props.userId} name={props.userName} />,
});

// Component is not rendered yet...
// Result before userLoaded fires:
// <header>
// {/* Header slot is empty, waiting... */}
// </header>

// Later, when data arrives:
userLoaded({ id: 123, name: 'John' }); // NOW the component is inserted and rendered
userLoaded({ id: 123, name: 'John' });

// Result after userLoaded fires:
// <header>
// <UserWidget id={123} name="John" />
// </header>
```

**Note:** You can pass an array of events `when: [event1, event2]` - component inserts when **any** of them fires. Use [once](https://patronum.effector.dev/operators/once/) from `patronum` if you need one-time insertion.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@grlt-hub/react-slots",
"version": "2.0.0",
"version": "3.0.0",
"type": "module",
"private": false,
"main": "dist/index.js",
Expand Down
9 changes: 4 additions & 5 deletions sandbox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { createEvent } from 'effector';
import { createGate } from 'effector-react';
import React from 'react';
import { createSlotIdentifier, createSlots } from './src';
Expand All @@ -9,13 +8,13 @@ const { slotsApi } = createSlots({

const appGate = createGate<number>();

slotsApi.insert.into.ConfirmScreenBottom({
slotsApi.ConfirmScreenBottom.insert({
when: [appGate.open],
mapProps: (__, y) => ({ id: Number(y) }),
component: (props) => <p>Hello world! {props.id}</p>,
Component: (props) => <p>Hello world! {props.id}</p>,
});

slotsApi.insert.into.ConfirmScreenBottom({
slotsApi.ConfirmScreenBottom.insert({
mapProps: (x) => x,
component: (props) => <p>Hello world! {props.id}</p>,
Component: (props) => <p>Hello world! {props.id}</p>,
});
36 changes: 18 additions & 18 deletions src/__tests__/payload.spec-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,77 +13,77 @@ const { slotsApi } = createSlots({
Bottom: noPropsSlot,
});

slotsApi.insert.into.Top({
slotsApi.Top.insert({
mapProps: (data) => ({ text: data.text }),
component: (props) => {
Component: (props) => {
expectTypeOf<{ text: string }>(props);
return <div />;
},
});

slotsApi.insert.into.Top({
slotsApi.Top.insert({
when: signal,
mapProps: (__, signalPayload) => ({ text: String(signalPayload) }),
component: (props) => {
Component: (props) => {
expectTypeOf<{ text: string }>(props);
return <div />;
},
});

slotsApi.insert.into.Top({
slotsApi.Top.insert({
when: [signal],
mapProps: (__, signalPayload) => ({ text: String(signalPayload) }),
component: (props) => {
Component: (props) => {
expectTypeOf<{ text: string }>(props);
return <div />;
},
});

slotsApi.insert.into.Top({
slotsApi.Top.insert({
when: [signal, createEvent<string[]>()],
mapProps: (__, signalPayload) => {
const text = Array.isArray(signalPayload) ? signalPayload[0] : String(signalPayload);

return { text };
},
component: (props) => {
Component: (props) => {
expectTypeOf<{ text: string }>(props);
return <div />;
},
});

slotsApi.insert.into.Top({
component: (props) => {
slotsApi.Top.insert({
Component: (props) => {
expectTypeOf<{ text: string }>(props);
return <div />;
},
});

slotsApi.insert.into.Bottom({
component: (props) => {
slotsApi.Bottom.insert({
Component: (props) => {
expectTypeOf<EmptyObject>(props);
return <div />;
},
});

slotsApi.insert.into.Top({
slotsApi.Top.insert({
mapProps: () => {},
component: () => <div />,
Component: () => <div />,
});

slotsApi.insert.into.Top({
slotsApi.Top.insert({
when: signal,
mapProps: (slotPayload, signalPayload) => ({ data: { signalPayload, slotPayload } }),
component: (props) => {
Component: (props) => {
expectTypeOf<{
data: { slotPayload: { text: string }; signalPayload: number };
}>(props);
return <div />;
},
});

slotsApi.insert.into.Top({
slotsApi.Top.insert({
mapProps: (data) => ({ text: data.text }),
// @ts-expect-error
component: (_: { wrong: number }) => <div />,
Component: (_: { wrong: number }) => <div />,
});
2 changes: 1 addition & 1 deletion src/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const makeChildWithProps = (child) =>
memo<any>((props) => {
const childProps = useMemo(() => child.mapProps(props), [props]);

return <child.component {...childProps} />;
return <child.Component {...childProps} />;
});

export { insertSorted, isNil, makeChildWithProps, type EmptyObject, type Entries };
Loading