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: +// ``` 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: +// +``` + ### 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;