diff --git a/.size-limit.json b/.size-limit.json
index 9fa916f..3f9c63e 100644
--- a/.size-limit.json
+++ b/.size-limit.json
@@ -1 +1 @@
-[{ "path": "dist/index.js", "limit": "850 B" }]
+[{ "path": "dist/index.js", "limit": "900 B" }]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e8480e7..d532b06 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/README.md b/README.md
index 31fd4d9..97941c8 100644
--- a/README.md
+++ b/README.md
@@ -76,24 +76,21 @@ const Sidebar = () => (
);
// plugin-analytics/index.ts - inject from anywhere!
-slotsApi.insert.into.Widgets({
- component: () => ,
+slotsApi.Widgets.insert({
+ Component: () => ,
});
// plugin-user-stats/index.ts - another plugin
-slotsApi.insert.into.Widgets({
- component: () => ,
+slotsApi.Widgets.insert({
+ Component: () => ,
});
-```
-
-### Result
-```tsx
-
+// Result:
+//
+// Core navigation
+//
+//
+//
```
No props drilling, no boilerplate - just define slots and inject content from anywhere in your codebase.
@@ -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
@@ -140,8 +137,8 @@ const App = () => (
);
// 3. Insert content into the slot
-slotsApi.insert.into.Footer({
- component: () =>
© 1955–1985–2015 Outatime Corp.
,
+slotsApi.Footer.insert({
+ Component: () => © 1955–1985–2015 Outatime Corp.
,
});
// Result:
@@ -165,8 +162,8 @@ const { slotsApi, Slots } = createSlots({
;
// Insert component - receives props automatically
-slotsApi.insert.into.UserPanel({
- component: (props) => ,
+slotsApi.UserPanel.insert({
+ Component: (props) => ,
});
```
@@ -179,13 +176,13 @@ const { slotsApi, Slots } = createSlots({
;
-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) => ,
+ Component: (props) => ,
});
```
@@ -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: () => ,
+slotsApi.Sidebar.insert({
+ Component: () => ,
order: 2,
});
// This is inserted second, but will render first
-slotsApi.insert.into.Sidebar({
- component: () => ,
+slotsApi.Sidebar.insert({
+ Component: () => ,
order: 1,
});
@@ -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: () => ,
+});
+
+slotsApi.Sidebar.insert({
+ Component: () => ,
+});
+
+// Result after inserts:
+//
+
+// Later, clear the slot
+slotsApi.Sidebar.clear();
+
+// Result after clear:
+//
+// {/* Sidebar slot is now empty */}
+//
+```
+
### Defer insertion until event fires
Wait for data to load before inserting component. The component won't render until the event fires:
@@ -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) => ,
+ Component: (props) => ,
});
-// Component is not rendered yet...
+// Result before userLoaded fires:
+//
+// {/* Header slot is empty, waiting... */}
+//
// 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:
+//
```
**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.
diff --git a/package-lock.json b/package-lock.json
index 1c6272c..7ea10ad 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@grlt-hub/react-slots",
- "version": "2.0.0",
+ "version": "3.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@grlt-hub/react-slots",
- "version": "2.0.0",
+ "version": "3.0.0",
"license": "MIT",
"devDependencies": {
"@rslib/core": "0.17.0",
diff --git a/package.json b/package.json
index 5eb9172..6b2da71 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/sandbox.tsx b/sandbox.tsx
index c7c99a8..4f5a52f 100644
--- a/sandbox.tsx
+++ b/sandbox.tsx
@@ -1,4 +1,3 @@
-import { createEvent } from 'effector';
import { createGate } from 'effector-react';
import React from 'react';
import { createSlotIdentifier, createSlots } from './src';
@@ -9,13 +8,13 @@ const { slotsApi } = createSlots({
const appGate = createGate();
-slotsApi.insert.into.ConfirmScreenBottom({
+slotsApi.ConfirmScreenBottom.insert({
when: [appGate.open],
mapProps: (__, y) => ({ id: Number(y) }),
- component: (props) => Hello world! {props.id}
,
+ Component: (props) => Hello world! {props.id}
,
});
-slotsApi.insert.into.ConfirmScreenBottom({
+slotsApi.ConfirmScreenBottom.insert({
mapProps: (x) => x,
- component: (props) => Hello world! {props.id}
,
+ Component: (props) => Hello world! {props.id}
,
});
diff --git a/src/__tests__/payload.spec-d.tsx b/src/__tests__/payload.spec-d.tsx
index 083f052..002fd6a 100644
--- a/src/__tests__/payload.spec-d.tsx
+++ b/src/__tests__/payload.spec-d.tsx
@@ -13,68 +13,68 @@ 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
;
},
});
-slotsApi.insert.into.Top({
+slotsApi.Top.insert({
when: signal,
mapProps: (__, signalPayload) => ({ text: String(signalPayload) }),
- component: (props) => {
+ Component: (props) => {
expectTypeOf<{ text: string }>(props);
return
;
},
});
-slotsApi.insert.into.Top({
+slotsApi.Top.insert({
when: [signal],
mapProps: (__, signalPayload) => ({ text: String(signalPayload) }),
- component: (props) => {
+ Component: (props) => {
expectTypeOf<{ text: string }>(props);
return
;
},
});
-slotsApi.insert.into.Top({
+slotsApi.Top.insert({
when: [signal, createEvent()],
mapProps: (__, signalPayload) => {
const text = Array.isArray(signalPayload) ? signalPayload[0] : String(signalPayload);
return { text };
},
- component: (props) => {
+ Component: (props) => {
expectTypeOf<{ text: string }>(props);
return
;
},
});
-slotsApi.insert.into.Top({
- component: (props) => {
+slotsApi.Top.insert({
+ Component: (props) => {
expectTypeOf<{ text: string }>(props);
return
;
},
});
-slotsApi.insert.into.Bottom({
- component: (props) => {
+slotsApi.Bottom.insert({
+ Component: (props) => {
expectTypeOf(props);
return
;
},
});
-slotsApi.insert.into.Top({
+slotsApi.Top.insert({
mapProps: () => {},
- component: () =>
,
+ Component: () =>
,
});
-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);
@@ -82,8 +82,8 @@ slotsApi.insert.into.Top({
},
});
-slotsApi.insert.into.Top({
+slotsApi.Top.insert({
mapProps: (data) => ({ text: data.text }),
// @ts-expect-error
- component: (_: { wrong: number }) =>
,
+ Component: (_: { wrong: number }) =>
,
});
diff --git a/src/helpers.tsx b/src/helpers.tsx
index 41af7c3..7158509 100644
--- a/src/helpers.tsx
+++ b/src/helpers.tsx
@@ -20,7 +20,7 @@ const makeChildWithProps = (child) =>
memo((props) => {
const childProps = useMemo(() => child.mapProps(props), [props]);
- return ;
+ return ;
});
export { insertSorted, isNil, makeChildWithProps, type EmptyObject, type Entries };
diff --git a/src/index.tsx b/src/index.tsx
index 7aec810..9c1305c 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,4 +1,4 @@
-import { createEvent, createStore } from 'effector';
+import { createEvent, createStore, type EventCallable } from 'effector';
import { useStoreMap } from 'effector-react';
import { nanoid } from 'nanoid';
import React, { memo, type FunctionComponent } from 'react';
@@ -19,6 +19,17 @@ const createSlots = >>(config: T) =>
[key in keyof T]: T[key] extends (_: any) => unknown ? Payload[0]> : never;
};
+ type ClearApi = {
+ [key in keyof T]: EventCallable;
+ };
+
+ type SlotApi = {
+ [key in keyof T]: {
+ insert: SetApi[key];
+ clear: ClearApi[key];
+ };
+ };
+
type State = {
[key in keyof T]: (Parameters[0] & { id: string })[];
};
@@ -80,6 +91,19 @@ const createSlots = >>(config: T) =>
return acc;
}, {} as SetApi);
+ const clearApi = keys.reduce((acc, key) => {
+ const clear = createEvent();
+
+ $slots.on(clear, (state) => {
+ if (state[key].length === 0) return state;
+ return { ...state, [key]: [] };
+ });
+
+ acc[key] = clear;
+
+ return acc;
+ }, {} as ClearApi);
+
const slots = keys.reduce((acc, key) => {
const component = memo>((props) => {
const slotChildren = useStoreMap($slots, (x) => x[key]);
@@ -88,7 +112,7 @@ const createSlots = >>(config: T) =>
if (isNil(child.mapProps)) {
return (
-
+
);
}
@@ -108,9 +132,13 @@ const createSlots = >>(config: T) =>
return acc;
}, {} as Slots);
- const slotsApi = {
- insert: { into: insertApi },
- };
+ const slotsApi = keys.reduce((acc, key) => {
+ acc[key] = {
+ insert: insertApi[key],
+ clear: clearApi[key],
+ };
+ return acc;
+ }, {} as SlotApi);
return { slotsApi, Slots: slots };
};
diff --git a/src/payload.ts b/src/payload.ts
index 70c74cc..f03c7d6 100644
--- a/src/payload.ts
+++ b/src/payload.ts
@@ -6,7 +6,7 @@ type ExtractWhenPayload = T extends Event ? P : T extends Event
type Payload = {
// When mapProps is provided with when
| Event[]>(params: {
- component: (props: unknown extends R ? EmptyObject : R extends void ? EmptyObject : R) => React.JSX.Element;
+ Component: (props: unknown extends R ? EmptyObject : R extends void ? EmptyObject : R) => React.JSX.Element;
mapProps: (arg: T, whenPayload: ExtractWhenPayload) => R;
order?: number;
when: W;
@@ -14,7 +14,7 @@ type Payload = {
// When mapProps is provided without when
(params: {
- component: (props: unknown extends R ? EmptyObject : R extends void ? EmptyObject : R) => React.JSX.Element;
+ Component: (props: unknown extends R ? EmptyObject : R extends void ? EmptyObject : R) => React.JSX.Element;
mapProps: (arg: T) => R;
order?: number;
when?: undefined;
@@ -22,7 +22,7 @@ type Payload = {
// When mapProps is not provided
(params: {
- component: (props: unknown extends T ? EmptyObject : T extends void ? EmptyObject : T) => React.JSX.Element;
+ Component: (props: unknown extends T ? EmptyObject : T extends void ? EmptyObject : T) => React.JSX.Element;
mapProps?: undefined;
order?: number;
when?: undefined;