├── .babelrc ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── 1.Bug_report.md │ ├── 2.Feature_request.md │ └── 3.Storybook_Bug_report.md ├── lock.yml ├── no-response.yml └── stale.yml ├── .gitignore ├── .prettierrc.js ├── .storybook ├── addons.js ├── config.js ├── preview-head.html └── webpack.config.js ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── package-lock.json ├── package.json ├── src ├── components │ ├── Fill │ │ └── index.tsx │ ├── Provider │ │ └── index.tsx │ └── Slot │ │ └── index.tsx └── index.tsx ├── stories └── index.stories.tsx └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-typescript", "@babel/preset-react"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-object-rest-spread", 5 | "@babel/plugin-proposal-class-properties" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | // ... 4 | 'react-hooks', 5 | ], 6 | extends: 'react-app', 7 | rules: { 8 | 'no-console': ['error', { allow: ['warn', 'error'] }], 9 | 'no-debugger': 'error', 10 | 'no-var': 'error', 11 | 'react-hooks/rules-of-hooks': 'error', 12 | 'react-hooks/exhaustive-deps': 'warn', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1.Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report for the React-Slot-Fill 4 | --- 5 | 6 | # Bug report 7 | 8 | ## Describe the bug 9 | 10 | A clear and concise description of what the bug is. 11 | 12 | ## To Reproduce 13 | 14 | Steps to reproduce the behavior, please provide code snippets or a repository: 15 | 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | ## Expected behavior 22 | 23 | A clear and concise description of what you expected to happen. 24 | 25 | ## Screenshots 26 | 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | ## System information 30 | 31 | - OS: [e.g. macOS, Windows] 32 | - Browser (if applies) [e.g. chrome, safari] 33 | - Version of React-Slot-Fill: [e.g. 1.4.0] 34 | 35 | ## Additional context 36 | 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2.Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Create a feature request for the React-Slot-Fill 4 | --- 5 | 6 | # Feature request 7 | 8 | ## Is your feature request related to a problem? Please describe. 9 | 10 | A clear and concise description of what you want and what your use case is. 11 | 12 | ## Describe the solution you'd like 13 | 14 | A clear and concise description of what you want to happen. 15 | 16 | ## Describe alternatives you've considered 17 | 18 | A clear and concise description of any alternative solutions or features you've considered. 19 | 20 | ## Additional context 21 | 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3.Storybook_Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report for Storybook 3 | about: Create a bug report for Storybook in React-Slot-Fill 4 | --- 5 | 6 | # Examples bug report 7 | 8 | ## Example name 9 | 10 | Provide the example name 11 | 12 | ## Describe the bug 13 | 14 | A clear and concise description of what the bug is. 15 | 16 | ## To Reproduce 17 | 18 | Steps to reproduce the behavior, please provide code snippets or a repository: 19 | 20 | 1. Go to '...' 21 | 2. Click on '....' 22 | 23 | 3. Scroll down to '....' 24 | 4. See error 25 | 26 | ## Expected behavior 27 | 28 | A clear and concise description of what you expected to happen. 29 | 30 | ## Screenshots 31 | 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | ## System information 35 | 36 | - OS: [e.g. macOS, Windows] 37 | - Browser (if applies) [e.g. chrome, safari] 38 | - Version of React-Slot-Fill: [e.g. 1.4.0] 39 | 40 | ## Additional context 41 | 42 | Add any other context about the problem here. 43 | -------------------------------------------------------------------------------- /.github/lock.yml: -------------------------------------------------------------------------------- 1 | # Configuration for lock-threads - https://github.com/dessant/lock-threads 2 | 3 | # Number of days of inactivity before a closed issue or pull request is locked 4 | daysUntilLock: 180 5 | # Comment to post before locking. Set to `false` to disable 6 | lockComment: > 7 | This issue has been automatically locked since there has not been 8 | any recent activity after it was closed. 9 | If you can still reproduce this issue then please open a new issue and fill out 10 | [the entire issue template](https://github.com/blackboxvision/react-slot-fill/blob/master/ISSUE_TEMPLATE.md) 11 | to ensure that we have enough information to address your issue. Thanks! 12 | # Issues or pull requests with these labels will not be locked 13 | exemptLabels: 14 | - help-wanted 15 | # Limit to only `issues` or `pulls` 16 | only: issues 17 | -------------------------------------------------------------------------------- /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an issue is closed for lack of response 4 | daysUntilClose: 28 5 | 6 | # Label requiring a response 7 | responseRequiredLabel: more-information-needed 8 | 9 | # Comment to post when closing an issue for lack of response. Set to `false` to disable. 10 | closeComment: > 11 | This issue has been automatically closed because there has been no response 12 | to our request for more information from the original author. With only the 13 | information that is currently in the issue, we don't have enough information 14 | to take action. Please reach out if you have or find the answers we need so 15 | that we can investigate further. 16 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 365 5 | # Number of days of inactivity before a stale Issue or Pull Request is closed 6 | daysUntilClose: 14 7 | # Issues or Pull Requests with these labels will never be considered stale 8 | exemptLabels: 9 | - regression 10 | - security 11 | - triaged 12 | # Label to use when marking as stale 13 | staleLabel: stale 14 | # Comment to post when marking as stale. Set to `false` to disable 15 | markComment: > 16 | Thanks for your contribution! 17 | 18 | This issue has been automatically marked as stale because it has not had 19 | recent activity. 20 | 21 | If you would like this issue to remain open: 22 | 23 | 1. Verify that you can still reproduce the issue in the latest version of react-slot-fill 24 | 1. Comment that the issue is still reproducible and include: 25 | * What version of react-slot-fill you reproduced the issue on 26 | * What OS and version you reproduced the issue on 27 | * What steps you followed to reproduce the issue 28 | 29 | Issues that are labeled as triaged will not be automatically marked as stale. 30 | # Comment to post when removing the stale label. Set to `false` to disable 31 | unmarkComment: false 32 | # Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable 33 | closeComment: false 34 | # Limit to only `issues` or `pulls` 35 | only: issues 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | build 3 | coverage 4 | node_modules 5 | storybook-static -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | singleQuote: true, 4 | }; 5 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { withInfo } from '@storybook/addon-info'; 2 | import { addDecorator, addParameters, configure } from '@storybook/react'; 3 | import { themes } from '@storybook/theming'; 4 | 5 | addParameters({ 6 | options: { 7 | name: 'React Slot/Fill', 8 | theme: themes.light, 9 | showAddonPanel: false, 10 | addonPanelInRight: true, 11 | }, 12 | }); 13 | 14 | addDecorator(withInfo({ inline: true, header: false })); 15 | 16 | const req = require.context('../stories', true, /.stories.tsx$/); 17 | function loadStories() { 18 | req.keys().forEach(filename => req(filename)); 19 | } 20 | 21 | configure(loadStories, module); 22 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = async ({ config }) => { 2 | config.module.rules.push({ 3 | test: /\.(ts|tsx)$/, 4 | use: [ 5 | { 6 | loader: require.resolve('awesome-typescript-loader'), 7 | }, 8 | { 9 | loader: require.resolve('react-docgen-typescript-loader'), 10 | }, 11 | ], 12 | }); 13 | 14 | config.resolve.extensions.push('.ts', '.tsx'); 15 | 16 | return config; 17 | }; 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to React-Slot-Fill 2 | 3 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device. 4 | 1. Install the dependencies: `npm i`. 5 | 1. Run `npm run storybook` to run the stories behing `/stories` folder. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 BlackBox Vision 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Slot and Fill [![npm version](https://badge.fury.io/js/%40blackbox-vision%2Freact-slot-fill.svg)](https://badge.fury.io/js/%40blackbox-vision%2Freact-slot-fill) [![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT) [![Known Vulnerabilities](https://snyk.io/test/github/blackboxvision/react-slot-fill/badge.svg)](https://snyk.io/test/github/blackboxvision/react-slot-fill) 2 | 3 | :rocket: React Slot and Fill pattern implementation made with React.createContext API. Check out the [demo](https://blackboxvision.github.io/react-slot-fill/). 4 | 5 | ## Install 6 | 7 | You can install this library via NPM or YARN. 8 | 9 | ### NPM 10 | 11 | ```bash 12 | npm i @blackbox-vision/react-slot-fill 13 | ``` 14 | 15 | ### YARN 16 | 17 | ```bash 18 | yarn add @blackbox-vision/react-slot-fill 19 | ``` 20 | 21 | ## Use case 22 | 23 | If you need to render a component from somepart of the DOM tree, but it needs to be visible in another part of the tree, this library solves it. 24 | 25 | This library is very similar to [`react-slot-fill`](https://github.com/camwest/react-slot-fill), but we solve two particular issues: 26 | 27 | - Support for `React.createContext`, this library is intended to use with React >= 16.3.1. 28 | - If a `Fill` is declared after a `Slot`, it can render properly, which [`react-slot-fill`](https://github.com/camwest/react-slot-fill) doesn't support. 29 | 30 | ## Usage 31 | 32 | The usage is really simple: 33 | 34 | ```javascript 35 | // Toolbar.js 36 | import React from 'react'; 37 | import { Slot, Fill } from '@blackbox-vision/react-slot-fill'; 38 | 39 | const Toolbar = (props) => ( 40 |
41 | 42 |
43 | ); 44 | 45 | export default Toolbar; 46 | 47 | Toolbar.Item = (props) => ( 48 | 49 | 50 | 51 | ); 52 | ``` 53 | 54 | ```javascript 55 | // Feature.js 56 | import React from 'react'; 57 | import Toolbar from './Toolbar'; 58 | 59 | const Feature = () => ; 60 | ``` 61 | 62 | ```javascript 63 | // App.js 64 | import React from 'react'; 65 | import ReactDOM from 'react-dom'; 66 | import { Provider } from '@blackbox-vision/react-slot-fill'; 67 | 68 | import Toolbar from './Toolbar'; 69 | import Feature from './Feature'; 70 | 71 | const App = () => ( 72 | 73 | 74 | 75 | 76 | ); 77 | 78 | ReactDOM.render(, document.getElementById('root')); 79 | ``` 80 | 81 | ## Props 82 | 83 | `Slot` and `Fill` components use the same props, which are the following ones: 84 | 85 | | Properties | Types | Default Value | Description | 86 | | ---------- | ------- | ------------- | ----------------------------------------------- | 87 | | name | string | none | Determines the name of the Slot/Fill. | 88 | | debug | boolean | false | Enable logging to detect issues with Slot/Fill. | 89 | 90 | ## TODO 91 | 92 | - [x] Support for passing props from Fill to Slot. 93 | - [ ] Support for multiple Fill for one Slot. 94 | 95 | ## Issues 96 | 97 | Please, open an [issue](https://github.com/BlackBoxVision/react-slot-fill/issues) following one of the issues templates. We will do our best to fix them. 98 | 99 | ## Contributing 100 | 101 | If you want to contribute to this project see [contributing](https://github.com/BlackBoxVision/react-slot-fill/blob/master/CONTRIBUTING.md) for more information. 102 | 103 | ## License 104 | 105 | Distributed under the **MIT license**. See [LICENSE](https://github.com/BlackBoxVision/react-slot-fill/blob/master/LICENSE) for more information. 106 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@blackbox-vision/react-slot-fill", 3 | "version": "2.0.1", 4 | "private": false, 5 | "description": "React Slot and Fill implementation made with React.createContext API", 6 | "publishConfig": { 7 | "registry": "https://registry.npmjs.org/" 8 | }, 9 | "scripts": { 10 | "clean": "rimraf pkg && rimraf storybook-static", 11 | "publish": "pika publish", 12 | "build": "npm run clean && pika build", 13 | "test": "jest --passWithNoTests -- -u", 14 | "start": "npm run storybook", 15 | "storybook": "start-storybook -p 6006", 16 | "build-storybook": "build-storybook", 17 | "deploy-storybook": "storybook-to-ghpages" 18 | }, 19 | "@pika/pack": { 20 | "pipeline": [ 21 | [ 22 | "@pika/plugin-standard-pkg", 23 | { 24 | "exclude": [ 25 | "**/*.md", 26 | "**/*.tests.*", 27 | "**/*.stories.*", 28 | "**/__snapshots/*" 29 | ] 30 | } 31 | ], 32 | [ 33 | "@pika/plugin-build-node", 34 | { 35 | "exclude": [ 36 | "**/*.md", 37 | "**/*.tests.*", 38 | "**/*.stories.*", 39 | "**/__snapshots/*" 40 | ] 41 | } 42 | ], 43 | [ 44 | "@pika/plugin-build-web", 45 | { 46 | "exclude": [ 47 | "**/*.md", 48 | "**/*.tests.*", 49 | "**/*.stories.*", 50 | "**/__snapshots/*" 51 | ] 52 | } 53 | ], 54 | [ 55 | "@pika/plugin-build-types", 56 | { 57 | "exclude": [ 58 | "**/*.md", 59 | "**/*.tests.*", 60 | "**/*.stories.*", 61 | "**/__snapshots/*" 62 | ] 63 | } 64 | ] 65 | ] 66 | }, 67 | "repository": { 68 | "type": "git", 69 | "url": "git+https://github.com/BlackBoxVision/react-slot-fill.git" 70 | }, 71 | "author": "Jonatan E. Salas ", 72 | "license": "MIT", 73 | "bugs": { 74 | "url": "https://github.com/BlackBoxVision/react-slot-fill/issues" 75 | }, 76 | "homepage": "https://github.com/BlackBoxVision/react-slot-fill#readme", 77 | "peerDependencies": { 78 | "react": "^16.3.1" 79 | }, 80 | "devDependencies": { 81 | "@babel/core": "^7.9.0", 82 | "@babel/plugin-proposal-class-properties": "^7.8.3", 83 | "@babel/plugin-proposal-object-rest-spread": "^7.9.0", 84 | "@babel/preset-react": "^7.9.4", 85 | "@babel/preset-typescript": "^7.9.0", 86 | "@commitlint/cli": "^8.3.5", 87 | "@commitlint/config-conventional": "^8.3.4", 88 | "@pika/pack": "^0.5.0", 89 | "@pika/plugin-build-node": "^0.9.2", 90 | "@pika/plugin-build-types": "^0.9.2", 91 | "@pika/plugin-build-web": "^0.9.2", 92 | "@pika/plugin-standard-pkg": "^0.9.2", 93 | "@storybook/addon-actions": "^5.3.18", 94 | "@storybook/addon-info": "^5.3.18", 95 | "@storybook/addon-links": "^5.3.18", 96 | "@storybook/addons": "^5.3.18", 97 | "@storybook/react": "^5.3.18", 98 | "@storybook/storybook-deployer": "^2.8.5", 99 | "@storybook/theming": "^5.3.18", 100 | "@types/react": "^16.9.32", 101 | "@types/storybook__react": "^5.2.1", 102 | "@typescript-eslint/eslint-plugin": "^2.26.0", 103 | "@typescript-eslint/parser": "^2.26.0", 104 | "awesome-typescript-loader": "^5.2.1", 105 | "babel-eslint": "^10.1.0", 106 | "babel-loader": "^8.1.0", 107 | "chai": "^4.1.2", 108 | "eslint": "^6.8.0", 109 | "eslint-config-react-app": "^5.2.1", 110 | "eslint-import-resolver-lerna": "^1.1.0", 111 | "eslint-plugin-flowtype": "^4.7.0", 112 | "eslint-plugin-import": "^2.20.2", 113 | "eslint-plugin-jsx-a11y": "^6.2.3", 114 | "eslint-plugin-react": "^7.19.0", 115 | "eslint-plugin-react-hooks": "^3.0.0", 116 | "husky": "^4.2.3", 117 | "isparta": "^4.1.1", 118 | "jest": "^25.2.7", 119 | "lint-staged": "^10.1.1", 120 | "mocha": "^7.1.1", 121 | "prettier": "^2.0.2", 122 | "react": "^16.13.1", 123 | "react-docgen-typescript-loader": "^3.7.2", 124 | "rimraf": "^3.0.2", 125 | "ts-loader": "^6.2.2" 126 | }, 127 | "husky": { 128 | "hooks": { 129 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 130 | "pre-commit": "lint-staged" 131 | } 132 | }, 133 | "lint-staged": { 134 | "*.{js,json,css,md}": [ 135 | "prettier --write", 136 | "git add" 137 | ], 138 | "*.{js,ts,tsx}": [ 139 | "eslint --fix --ext .ts,.tsx,.js", 140 | "git add" 141 | ] 142 | }, 143 | "dependencies": {} 144 | } 145 | -------------------------------------------------------------------------------- /src/components/Fill/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | import { SlotFillContext } from '../Provider'; 4 | 5 | export interface FillProps { 6 | name: string; 7 | children: any; 8 | } 9 | 10 | export const Fill: React.FunctionComponent = ({ 11 | name, 12 | children, 13 | }) => { 14 | const { debug, setFillForSlot } = useContext(SlotFillContext); 15 | 16 | if (!name) { 17 | debug && console.warn(`[Fill]: id is null or undefined.`); 18 | return null; 19 | } 20 | 21 | if (Array.isArray(children) && children.length === 0) { 22 | debug && console.warn(`[Fill]: children array is empty.`); 23 | return null; 24 | } 25 | 26 | setFillForSlot(name, () => children); 27 | 28 | return null; 29 | }; 30 | 31 | Fill.displayName = 'Fill'; 32 | 33 | export default Fill; 34 | -------------------------------------------------------------------------------- /src/components/Provider/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, createContext } from 'react'; 2 | 3 | export interface SlotFillContextProps { 4 | setFillForSlot: (slotId: string, renderCallback: () => any) => void; 5 | subscribe: (slotId: string, renderCallback: () => any) => void; 6 | unsubscribe: (slotId: string, slotIndex: number) => void; 7 | getFillForSlot: (slotId: string) => () => any; 8 | notify: (slotId: string) => any; 9 | debug: boolean; 10 | } 11 | 12 | export interface SlotFillItem { 13 | slotId: string; 14 | callback: () => any; 15 | } 16 | 17 | export interface SlotFillProviderProps { 18 | debug?: boolean; 19 | children?: any; 20 | } 21 | 22 | export const SlotFillContext = createContext({} as any); 23 | 24 | export const Provider: React.FunctionComponent = ({ 25 | debug, 26 | children, 27 | }) => { 28 | const [subscribers, setSubscribers] = useState([]); 29 | 30 | const noopRenderCallback = () => { 31 | debug && 32 | console.warn( 33 | `[SlotAndFillProvider]: NoopRenderCallback has nothing to render` 34 | ); 35 | 36 | return null; 37 | }; 38 | 39 | const setFillForSlot = ( 40 | slotId: string, 41 | renderCallback = noopRenderCallback 42 | ) => { 43 | const fillForSlot = subscribers.find( 44 | (suscriber: any) => suscriber.slotId === slotId 45 | ); 46 | 47 | if (fillForSlot) { 48 | debug && 49 | console.warn( 50 | `[SlotAndFillProvider]: You've already registered a Fill for the following slotId: ${slotId}` 51 | ); 52 | 53 | return; 54 | } 55 | 56 | subscribe(slotId, renderCallback); 57 | notify(slotId); 58 | }; 59 | 60 | const getFillForSlot = (slotId: string) => { 61 | const fillById: SlotFillItem | undefined = subscribers.find( 62 | (suscriber: any) => suscriber.slotId === slotId 63 | ); 64 | 65 | if (!fillById) { 66 | debug && 67 | console.warn( 68 | `[SlotAndFillProvider]: There's no Fill registered for the following slotId: ${slotId}` 69 | ); 70 | 71 | return noopRenderCallback; 72 | } 73 | 74 | return fillById.callback; 75 | }; 76 | 77 | const subscribe = (slotId: string, callback: () => any) => { 78 | debug && 79 | console.warn( 80 | `[SlotAndFillProvider]: Subscribe callback for slotId ${slotId}` 81 | ); 82 | 83 | setSubscribers((suscribers: any) => 84 | suscribers.concat({ slotId, callback }) 85 | ); 86 | }; 87 | 88 | const unsubscribe = (slotId: string, slotIndex: number) => { 89 | debug && 90 | console.warn( 91 | `[SlotAndFillProvider]: Unsubscribe callback for slotId ${slotId} and slotIndex ${slotIndex}` 92 | ); 93 | 94 | setSubscribers((suscribers: any) => 95 | suscribers.filter( 96 | (suscriber: any, idx: number) => 97 | suscriber.slotId === slotId && idx === slotIndex 98 | ) 99 | ); 100 | }; 101 | 102 | const notify = (slotId: string) => { 103 | debug && 104 | console.warn( 105 | `[SlotAndFillProvider]: Notify subscribers for slotId ${slotId}` 106 | ); 107 | debug && 108 | console.warn( 109 | `[SlotAndFillProvider]: Current amount of subscribers is ${subscribers.length}` 110 | ); 111 | 112 | subscribers.forEach((suscriber: any, slotIndex: number) => { 113 | if (suscriber.slotId !== slotId) { 114 | debug && 115 | console.warn( 116 | `[SlotAndFillProvider]: Subscriber isn't matching slotId value` 117 | ); 118 | 119 | return; 120 | } 121 | 122 | suscriber.callback(slotIndex); 123 | }); 124 | }; 125 | 126 | return ( 127 | 137 | {children} 138 | 139 | ); 140 | }; 141 | 142 | Provider.displayName = 'SlotFillProvider'; 143 | 144 | export default Provider; 145 | -------------------------------------------------------------------------------- /src/components/Slot/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext, useRef } from 'react'; 2 | 3 | import { SlotFillContext } from '../Provider'; 4 | 5 | export interface SlotProps { 6 | children?: React.ReactChildren | null; 7 | name: string; 8 | } 9 | 10 | export const Slot: React.FunctionComponent = ({ 11 | name, 12 | children, 13 | ...rest 14 | }) => { 15 | const slotIndexRef = useRef(null); 16 | const { debug, subscribe, unsubscribe, getFillForSlot } = useContext( 17 | SlotFillContext 18 | ); 19 | 20 | useEffect(() => { 21 | subscribe(name, (slotIdx: number) => { 22 | debug && 23 | console.warn( 24 | `Slot: Calling suscribe for slotIndex ${slotIdx}, where name is ${name}` 25 | ); 26 | 27 | slotIndexRef.current = slotIdx; 28 | }); 29 | 30 | return function willUnmount() { 31 | if (slotIndexRef.current) { 32 | unsubscribe(name, slotIndexRef.current); 33 | } 34 | }; 35 | }, [name]); 36 | 37 | if (!name) { 38 | debug && console.warn(`Slot: You forget to pass id to `); 39 | return null; 40 | } 41 | 42 | const renderCallback = getFillForSlot(name); 43 | const child = renderCallback(); 44 | 45 | return child ? React.cloneElement(child, rest) : null; 46 | }; 47 | 48 | Slot.displayName = 'Slot'; 49 | 50 | export default Slot; 51 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './components/Fill'; 2 | export * from './components/Slot'; 3 | export * from './components/Provider'; 4 | -------------------------------------------------------------------------------- /stories/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import React from 'react'; 3 | import { Fill, Provider, Slot } from '../src'; 4 | 5 | storiesOf('Slot-Fill', module).add('Slot-Fill Demo', () => ( 6 |
15 | 16 | 17 | 18 |
29 | Red Box 30 |
31 |
32 | 33 |
44 | Green Box 45 |
46 |
47 | 48 |
49 |
50 | )); 51 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/lib", 4 | "module": "commonjs", 5 | "target": "es5", 6 | "lib": ["es5", "es6", "es7", "es2017", "dom"], 7 | "sourceMap": true, 8 | "allowJs": false, 9 | "jsx": "react", 10 | "moduleResolution": "node", 11 | "rootDirs": ["src", "stories"], 12 | "baseUrl": "src", 13 | "esModuleInterop": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "noImplicitAny": true, 18 | "strictNullChecks": true, 19 | "suppressImplicitAnyIndexErrors": true, 20 | "noUnusedLocals": true, 21 | "declaration": true, 22 | "allowSyntheticDefaultImports": true, 23 | "experimentalDecorators": true, 24 | "emitDecoratorMetadata": true 25 | }, 26 | "include": ["src/**/*"], 27 | "exclude": ["node_modules", "build"] 28 | } 29 | --------------------------------------------------------------------------------