├── .eslintignore ├── .npmignore ├── src ├── typings.d.ts ├── setupTests.ts ├── lib │ ├── index.ts │ ├── utils │ │ └── PropTypes.ts │ ├── .babelrc │ ├── components │ │ ├── Fill.ts │ │ ├── Provider.ts │ │ └── Slot.ts │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ ├── Provider.test.tsx │ │ └── index.test.tsx │ └── Manager.ts ├── index.tsx └── demo │ ├── App.css │ ├── advanced │ ├── Canvas.css │ ├── AppBar.css │ ├── index.tsx │ ├── Workspace.css │ ├── Canvas.tsx │ ├── Settings.tsx │ ├── Drafting.tsx │ ├── Workspace.tsx │ ├── AppBar.tsx │ └── Keybinding.tsx │ ├── simple │ ├── index.tsx │ ├── Viewer.tsx │ ├── News.tsx │ ├── Survey.tsx │ ├── Toolbar.tsx │ ├── SurveyForm.tsx │ └── NewsContent.tsx │ └── App.tsx ├── public ├── favicon.ico └── index.html ├── images └── slot-fill-logo.png ├── tsconfig.test.json ├── .gitignore ├── rollup.config.js ├── circle.yml ├── LICENSE ├── tsconfig.json ├── script └── prepublish.js ├── DEVELOPING.md ├── CHANGELOG.md ├── package.json ├── CONTRIBUTING.md ├── tslint.json ├── README.md ├── CODE_OF_CONDUCT.md └── .eslintrc.js /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.d.ts 2 | build 3 | coverage 4 | lib -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build 2 | coverage 3 | public 4 | src 5 | script -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-icons/lib*"; 2 | declare module "mitt"; -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camwest/react-slot-fill/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /images/slot-fill-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camwest/react-slot-fill/HEAD/images/slot-fill-logo.png -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | (global as any).requestAnimationFrame = function(callback: any) { 2 | setTimeout(callback, 0); 3 | }; 4 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import Fill from './components/Fill'; 2 | import Provider from './components/Provider'; 3 | import Slot from './components/Slot'; 4 | 5 | export { Provider, Slot, Fill }; 6 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | 4 | import App from './demo/App'; 5 | import 'tachyons/css/tachyons.min.css'; 6 | 7 | ReactDOM.render( 8 | , 9 | document.getElementById('root') 10 | ); 11 | -------------------------------------------------------------------------------- /src/demo/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | 7 | html, body, #root { 8 | height: 100%; 9 | width: 100%; 10 | } 11 | 12 | * { 13 | box-sizing: border-box; 14 | } 15 | 16 | .AppHeader { 17 | background: black; 18 | width: 100%; 19 | padding: 8px; 20 | } -------------------------------------------------------------------------------- /src/demo/advanced/Canvas.css: -------------------------------------------------------------------------------- 1 | .Canvas { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | font-size: 28px; 8 | 9 | color: grey; 10 | } 11 | 12 | .Canvas:focus { 13 | outline: none; 14 | 15 | background: lightgrey; 16 | color: white; 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production demo 10 | /build 11 | 12 | # production lib 13 | /lib 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | -------------------------------------------------------------------------------- /src/lib/utils/PropTypes.ts: -------------------------------------------------------------------------------- 1 | import * as PropTypes from 'prop-types'; 2 | 3 | export const managerShape = PropTypes.shape({ 4 | onComponentsChange: PropTypes.func.isRequired, 5 | removeOnComponentsChange: PropTypes.func.isRequired, 6 | }); 7 | 8 | export const busShape = PropTypes.shape({ 9 | emit: PropTypes.func.isRequired, 10 | on: PropTypes.func.isRequired, 11 | off: PropTypes.func.isRequired 12 | }); -------------------------------------------------------------------------------- /src/lib/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "ie": 9, 6 | "uglify": true 7 | }, 8 | "modules": false, 9 | "useBuiltIns": false 10 | }], 11 | "react" 12 | ], 13 | "plugins": [ 14 | "external-helpers", 15 | ["transform-object-rest-spread", {"useBuiltIns": true}], 16 | ["transform-react-jsx", {"useBuiltIns": true}] 17 | ] 18 | } -------------------------------------------------------------------------------- /src/demo/simple/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Toolbar from './Toolbar'; 3 | import Viewer from './Viewer'; 4 | import News from './News'; 5 | import Survey from './Survey'; 6 | 7 | import { Provider } from '../../lib'; 8 | 9 | export const Simple = () => 10 | ( 11 | 12 |
13 | 14 | 15 | 16 | 17 |
18 |
19 | ); 20 | 21 | export default Simple; -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import typescript from 'rollup-plugin-typescript2'; 3 | 4 | const pkg = JSON.parse(fs.readFileSync('./package.json')); 5 | 6 | export default { 7 | input: 'src/lib/index.ts', 8 | plugins: [ 9 | typescript({ 10 | typescript: require('typescript') 11 | }) 12 | ], 13 | 14 | output: [ 15 | { file: pkg.main, format: 'cjs' }, 16 | { file: pkg.module, format: 'es' } 17 | ], 18 | 19 | external: ['react', 'prop-types', 'mitt'] 20 | }; 21 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 7.7.3 4 | environment: 5 | PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin" 6 | dependencies: 7 | override: 8 | - yarn 9 | cache_directories: 10 | - ~/.cache/yarn 11 | test: 12 | override: 13 | - yarn test-+-coverage 14 | - yarn run send-coverage 15 | deployment: 16 | production: 17 | branch: master 18 | commands: 19 | - git config user.email "camwest@gmail.com" 20 | - git config user.name "Cameron Westland" 21 | - yarn run deploy 22 | -------------------------------------------------------------------------------- /src/demo/simple/Viewer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Slot, Fill } from '../../lib'; 3 | 4 | export default class Viewer extends React.Component { 5 | static Content = (props: any) => ( 6 | 7 |
{props.children}
8 |
9 | ) 10 | 11 | render() { 12 | return ( 13 |
14 |

Content

15 | 16 | {(items: any) =>
{items[items.length - 1]}
} 17 |
18 |
19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Cameron Westland 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /src/demo/advanced/AppBar.css: -------------------------------------------------------------------------------- 1 | .AppBar-AppBarItem:focus { 2 | outline: none; 3 | } 4 | 5 | .AppBar-AppBarItem:focus, 6 | .AppBar-AppBarItem.AppBar-AppBarItem--active:focus { 7 | background-color: #E5E5E5; 8 | } 9 | 10 | .AppBar-AppBarItem:hover .AppBar-AppBarItemIcon { 11 | color: #B4B4B4; 12 | } 13 | 14 | .AppBar-AppBarItem.AppBar-AppBarItem--active { 15 | background-color: #D2D2D2; 16 | } 17 | 18 | .AppBar-AppBarItem:active { 19 | color: #046A97; 20 | } 21 | 22 | .AppBar-AppBarItem { 23 | background: none; 24 | border: none; 25 | color: #3C3C3C; 26 | font-size: 11px; 27 | padding: 8px; 28 | } 29 | -------------------------------------------------------------------------------- /src/demo/advanced/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Workspace from './Workspace'; 3 | import AppBar from './AppBar'; 4 | import Drafting from './Drafting'; 5 | import Settings from './Settings'; 6 | import Keybinding from './Keybinding'; 7 | import Canvas from './Canvas'; 8 | 9 | import { Provider } from '../../lib'; 10 | 11 | export const Advanced = () => ( 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 | ); 23 | 24 | export default Advanced; -------------------------------------------------------------------------------- /src/lib/components/Fill.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { busShape } from '../utils/PropTypes'; 4 | import { Requireable } from 'prop-types'; 5 | 6 | export interface Props { 7 | name: string | Symbol; 8 | [key: string]: any; 9 | } 10 | 11 | export default class Fill extends React.Component { 12 | static contextTypes = { 13 | bus: busShape 14 | }; 15 | 16 | componentWillMount() { 17 | this.context.bus.emit('fill-mount', { 18 | fill: this 19 | }); 20 | } 21 | 22 | componentDidUpdate() { 23 | this.context.bus.emit('fill-updated', { 24 | fill: this 25 | }); 26 | } 27 | 28 | componentWillUnmount() { 29 | this.context.bus.emit('fill-unmount', { 30 | fill: this 31 | }); 32 | } 33 | 34 | render() { 35 | return null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "target": "es5", 7 | "lib": [ 8 | "es6", 9 | "dom" 10 | ], 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "jsx": "react", 14 | "moduleResolution": "node", 15 | "rootDir": "src", 16 | "forceConsistentCasingInFileNames": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noImplicitAny": true, 20 | "strictNullChecks": true, 21 | "suppressImplicitAnyIndexErrors": true 22 | }, 23 | "exclude": [ 24 | "build", 25 | "coverage", 26 | "jest", 27 | "lib", 28 | "node_modules", 29 | "rollup.config.js", 30 | "scripts", 31 | "webpack" 32 | ], 33 | "types": [ 34 | "typePatches" 35 | ] 36 | } -------------------------------------------------------------------------------- /src/lib/__tests__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Fills the a simple slot 1`] = ` 4 |
5 |
6 | 9 |
10 |
11 | `; 12 | 13 | exports[`Fills the appropriate slot 1`] = ` 14 |
15 |
16 | 19 | 22 |
23 |
24 | 27 | Twitter 28 | 29 |
30 |
31 |
32 | `; 33 | 34 | exports[`Replaces the contents of the slot with the matching fill when the slot's \`name\` property changes 1`] = ` 35 |
36 |
37 | 38 | Home 1 39 | 40 |
41 |
42 | , 43 | , 44 |
45 |
46 | `; 47 | 48 | exports[`allows slots to render null 1`] = `
`; 49 | -------------------------------------------------------------------------------- /src/lib/__tests__/Provider.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { configure, mount } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import { Fill, Slot, Provider } from '../'; 5 | 6 | configure({ adapter: new Adapter() }); 7 | 8 | it('tests foo', () => { 9 | const wrapper = mount( 10 | 11 |
12 | 13 |
14 | FooBar-Children 15 |
16 |
17 |
18 |
19 | ); 20 | 21 | const provider: Provider = wrapper.instance() as any; 22 | const [fill] = provider.getFillsByName('FooBar'); 23 | 24 | if (!fill) { 25 | throw new Error('expected fill to be defined'); 26 | } 27 | 28 | expect(fill.props.other).toBe('prop'); 29 | 30 | const elements = provider.getChildrenByName('FooBar'); 31 | 32 | expect(elements.length).toBe(1); 33 | }); -------------------------------------------------------------------------------- /src/demo/simple/News.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Toolbar from './Toolbar'; 3 | import Viewer from './Viewer'; 4 | 5 | import NewsContent from './NewsContent'; 6 | 7 | export default class Feature extends React.Component { 8 | constructor(props: any) { 9 | super(props); 10 | this.state = { active: false }; 11 | this.handleActive = this.handleActive.bind(this); 12 | this.handleDeactive = this.handleDeactive.bind(this); 13 | } 14 | 15 | handleActive() { 16 | this.setState({ active: true }); 17 | } 18 | 19 | handleDeactive() { 20 | this.setState({ active: false }); 21 | } 22 | 23 | render() { 24 | return ( 25 |
26 | 31 | {this.state.active && } 32 |
33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/demo/simple/Survey.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Toolbar from './Toolbar'; 3 | import Viewer from './Viewer'; 4 | 5 | import SurveyForm from './SurveyForm'; 6 | 7 | export default class Feature extends React.Component { 8 | constructor(props: any) { 9 | super(props); 10 | this.state = { active: false }; 11 | this.handleActive = this.handleActive.bind(this); 12 | this.handleDeactive = this.handleDeactive.bind(this); 13 | } 14 | 15 | handleActive() { 16 | this.setState({ active: true }); 17 | } 18 | 19 | handleDeactive() { 20 | this.setState({ active: false }); 21 | } 22 | 23 | render() { 24 | return ( 25 |
26 | 31 | {this.state.active && } 32 |
33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/demo/simple/Toolbar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Slot, Fill } from '../../lib'; 4 | 5 | class Toolbar extends React.Component { 6 | static Item = ({ label, onActive, onDeactive }: any) => ( 7 | 8 | 9 | 10 | ) 11 | 12 | constructor(props: any) { 13 | super(props); 14 | this.state = { currentItem: null }; 15 | this.handleClick = this.handleClick.bind(this); 16 | } 17 | 18 | handleClick({ props }: any) { 19 | if (this.state.currentItem) { 20 | this.state.currentItem.onDeactive(); 21 | } 22 | 23 | props.onActive(); 24 | this.setState({ currentItem: props }); 25 | } 26 | 27 | render() { 28 | return ( 29 |
30 |

Toolbar

31 | {this.state.currentItem && 32 |

Current Item: {this.state.currentItem.label}

} 33 | 34 | 35 |
36 | ); 37 | } 38 | } 39 | 40 | export default Toolbar; 41 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | react-slot-fill demo 17 | 18 | 19 |
20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/lib/components/Provider.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import mitt from 'mitt'; 3 | 4 | import { Requireable } from 'prop-types'; 5 | 6 | import { managerShape, busShape } from '../utils/PropTypes'; 7 | import Manager from '../Manager'; 8 | import Fill from './Fill'; 9 | 10 | export default class Provider extends React.Component { 11 | static childContextTypes = { 12 | manager: managerShape, 13 | bus: busShape 14 | }; 15 | 16 | private _bus: mitt.Emitter; 17 | private _manager: Manager; 18 | 19 | constructor() { 20 | super(); 21 | this._bus = new mitt(); 22 | this._manager = new Manager(this._bus); 23 | this._manager.mount(); 24 | } 25 | 26 | componentWillUnmount() { 27 | this._manager.unmount(); 28 | } 29 | 30 | getChildContext() { 31 | return { 32 | bus: this._bus, 33 | manager: this._manager 34 | }; 35 | } 36 | 37 | render(): any { 38 | return this.props.children; 39 | } 40 | 41 | /** 42 | * Returns instances of Fill react components 43 | */ 44 | getFillsByName(name: string): Fill[] { 45 | return this._manager.getFillsByName(name); 46 | } 47 | 48 | /** 49 | * Return React elements that were inside Fills 50 | */ 51 | getChildrenByName(name: string): React.ReactChild[] { 52 | return this._manager.getChildrenByName(name); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/demo/advanced/Workspace.css: -------------------------------------------------------------------------------- 1 | .Resizer { 2 | background: #000; 3 | opacity: .2; 4 | z-index: 1; 5 | -moz-box-sizing: border-box; 6 | -webkit-box-sizing: border-box; 7 | box-sizing: border-box; 8 | -moz-background-clip: padding; 9 | -webkit-background-clip: padding; 10 | background-clip: padding-box; 11 | } 12 | 13 | .Resizer:hover { 14 | -webkit-transition: all 2s ease; 15 | transition: all 2s ease; 16 | } 17 | 18 | .Resizer.horizontal { 19 | height: 11px; 20 | margin: -5px 0; 21 | border-top: 5px solid rgba(255, 255, 255, 0); 22 | border-bottom: 5px solid rgba(255, 255, 255, 0); 23 | cursor: row-resize; 24 | width: 100%; 25 | } 26 | 27 | .Resizer.horizontal:hover { 28 | border-top: 5px solid rgba(0, 0, 0, 0.5); 29 | border-bottom: 5px solid rgba(0, 0, 0, 0.5); 30 | } 31 | 32 | .Resizer.vertical { 33 | width: 11px; 34 | margin: 0 -5px; 35 | border-left: 5px solid rgba(255, 255, 255, 0); 36 | border-right: 5px solid rgba(255, 255, 255, 0); 37 | cursor: col-resize; 38 | } 39 | 40 | .Resizer.vertical:hover { 41 | border-left: 5px solid rgba(0, 0, 0, 0.5); 42 | border-right: 5px solid rgba(0, 0, 0, 0.5); 43 | } 44 | 45 | Resizer.disabled { 46 | cursor: not-allowed; 47 | } 48 | 49 | Resizer.disabled:hover { 50 | border-color: transparent; 51 | } -------------------------------------------------------------------------------- /src/demo/advanced/Canvas.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Workspace from './Workspace'; 3 | import Keybinding from './Keybinding'; 4 | 5 | import './Canvas.css'; 6 | 7 | export default class Canvas extends React.Component { 8 | private _canvas: any; 9 | 10 | constructor(props: any) { 11 | super(props); 12 | this.state = { focused: false }; 13 | this.handleInvoke = this.handleInvoke.bind(this); 14 | this.handleFocusIn = this.handleFocusIn.bind(this); 15 | this.handleFocusOut = this.handleFocusOut.bind(this); 16 | } 17 | 18 | handleInvoke() { 19 | this._canvas.focus(); 20 | } 21 | 22 | handleFocusIn() { 23 | this.setState({ focused: true }); 24 | } 25 | 26 | handleFocusOut() { 27 | this.setState({ focused: false }); 28 | } 29 | 30 | render() { 31 | const message = this.state.focused 32 | ? 'Canvas Focused' 33 | : 'Canvas Unfocused'; 34 | 35 | return ( 36 |
37 | 38 |
this._canvas = r} 42 | onFocus={this.handleFocusIn} 43 | onBlur={this.handleFocusOut} 44 | > 45 | {message} 46 |
47 |
48 | 54 |
55 | ); 56 | } 57 | } -------------------------------------------------------------------------------- /script/prepublish.js: -------------------------------------------------------------------------------- 1 | /* eslint-env shelljs */ 2 | 3 | require('shelljs/global'); 4 | const path = require('path'); 5 | 6 | /** 7 | * Clean lib directory 8 | */ 9 | if (test('-e', path.join(__dirname, '..', 'lib'))) { 10 | console.log('Cleaning lib'); 11 | rm('-rf', path.join(__dirname, '..', 'lib')); 12 | } 13 | 14 | /** 15 | * Run rollup 16 | */ 17 | exec('rollup -c', { silent: false }); 18 | 19 | console.log('Building lib'); 20 | 21 | /** 22 | * Run TypeScript 23 | */ 24 | 25 | exec('tsc', { silent: true }); 26 | console.log('Generating .d.ts files'); 27 | 28 | /** 29 | * Copy .d.ts files into lib 30 | */ 31 | 32 | const r = path.join(__dirname, '..', 'build', 'dist') 33 | 34 | const directories = new Set(); 35 | const files = new Map(); 36 | 37 | ls(path.join(__dirname, '..', 'build', 'dist', 'lib', '**', '*.d.ts')).forEach(source => { 38 | const destination = path.join(__dirname, '..', path.relative(r, source)); 39 | 40 | // Guard copying test directories to lib 41 | if (path.dirname(destination).includes('__tests__')) { 42 | return; 43 | } 44 | 45 | const directory = path.dirname(destination); 46 | 47 | if (!test('-e', directory)) { 48 | // Create directory 49 | directories.add(path.dirname(destination)); 50 | } 51 | 52 | // Create these files 53 | files.set(source, destination); 54 | }); 55 | 56 | if (Array.from(directories).length !== 0) { 57 | for (let directory of directories) { 58 | mkdir('-p', directory); 59 | } 60 | } 61 | 62 | for (let source of files.keys()) { 63 | const destination = files.get(source); 64 | cp(source, destination); 65 | } -------------------------------------------------------------------------------- /src/demo/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Advanced from './advanced'; 3 | import Simple from './simple'; 4 | 5 | import './App.css'; 6 | 7 | const AppStyle: any = { 8 | display: 'flex', 9 | height: '100%', 10 | flexDirection: 'column', 11 | width: '100%' 12 | }; 13 | 14 | const linkClass = 'f4 fw7 dib pa2 no-underline bg-animate bg-white hover-bg-light-blue black'; 15 | 16 | const examples = { 17 | simple: { 18 | label: 'Simple', 19 | extensions: 20 | }, 21 | 22 | advanced: { 23 | label: 'Advanced', 24 | extensions: 25 | } 26 | }; 27 | 28 | export interface State { 29 | example: string; 30 | } 31 | 32 | class App extends React.Component<{}, State> { 33 | constructor() { 34 | super(); 35 | this.state = { example: Object.keys(examples)[0] }; 36 | this.handleChange = this.handleChange.bind(this); 37 | } 38 | 39 | handleChange(event: React.ChangeEvent) { 40 | this.setState({ example: event.target.value }); 41 | } 42 | render() { 43 | return ( 44 |
45 |
46 | github 47 | 48 | 53 |
54 | 55 | {examples[this.state.example].extensions} 56 |
57 | ); 58 | } 59 | } 60 | 61 | export default App; 62 | -------------------------------------------------------------------------------- /DEVELOPING.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | ## Development on react-slot-fill 18 | 19 | ### Installation 20 | 21 | 1. Install [NodeJS](https://nodejs.org). 22 | 2. In the repo directory, run `npm i` command to install the required npm packages. 23 | 4. Run `npm start` - to start development server 24 | 25 | ### Build & Test 26 | 27 | | Command | Description | 28 | | ---------------- | ----------------------------------- | 29 | | `npm start` | runs dev server | 30 | | `npm run build` | runs production build of demos | 31 | | `npm test` | runs tests suite | 32 | 33 | ## Repository Layout 34 |
35 |   src/             - Top level packages which publish to npm go here
36 |       demo/        - todo
37 |         advanced/  - advanced example
38 |         simple/    - simple example
39 |       lib/         - main library directory
40 |         index.js   - primary library entry point
41 |       index.js     - demo entry point
42 |   docs/            - documentation
43 |   examples/        - example Orion projects
44 | 
45 | -------------------------------------------------------------------------------- /src/demo/advanced/Settings.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import SettingsIcon from 'react-icons/lib/md/settings'; 3 | 4 | import { Slot, Fill } from '../../lib'; 5 | import AppBar from './AppBar'; 6 | import Workspace from './Workspace'; 7 | 8 | export default class Settings extends React.Component { 9 | static Group = ({ label, children }: any) => ( 10 | 11 |

{label}

12 | {children} 13 |
14 | ) 15 | 16 | static TextInput = ({ label, ...rest }: any) => ( 17 |
18 | {label} 19 |
20 | ) 21 | 22 | static Checkbox = ({ label, ...rest }: any) => ( 23 |
24 | {label} 25 |
26 | ) 27 | 28 | constructor(props: any) { 29 | super(props); 30 | this.state = { active: false }; 31 | this.handleEnter = this.handleEnter.bind(this); 32 | this.handleExit = this.handleExit.bind(this); 33 | } 34 | 35 | handleEnter() { 36 | this.setState({ active: true }); 37 | } 38 | 39 | handleExit() { 40 | this.setState({ active: false }); 41 | } 42 | 43 | render() { 44 | // const Panel = this.state.active 45 | // ? 46 | // : null; 47 | 48 | return ( 49 |
50 | } 55 | onEnter={this.handleEnter} 56 | onExit={this.handleExit} 57 | /> 58 | {/*{Panel}*/} 59 |
60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/demo/simple/SurveyForm.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default () => ( 4 |
5 |
6 | Favorite Movies 7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 | 33 | 34 |
35 |
36 |
37 | ); -------------------------------------------------------------------------------- /src/demo/advanced/Drafting.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Workspace from './Workspace'; 3 | import AppBar from './AppBar'; 4 | import Settings from './Settings'; 5 | import CreateIcon from 'react-icons/lib/md/create'; 6 | 7 | export default class Drafting extends React.Component { 8 | constructor(props: any) { 9 | super(props); 10 | this.state = { active: false, snapping: false }; 11 | this.handleEnter = this.handleEnter.bind(this); 12 | this.handleExit = this.handleExit.bind(this); 13 | this.handleEnableSnapping = this.handleEnableSnapping.bind(this); 14 | } 15 | 16 | handleEnter() { 17 | this.setState({ active: true }); 18 | } 19 | handleExit() { 20 | this.setState({ active: false }); 21 | } 22 | 23 | handleEnableSnapping(e: any) { 24 | this.setState({ snapping: e.target.checked }); 25 | } 26 | 27 | render() { 28 | // const snappingNotice = (this.state.snapping) 29 | // ?
Snapping is enabled!
30 | // :
Snapping is NOT enabled!
; 31 | 32 | // const Panel = this.state.active 33 | // ? {snappingNotice} 34 | // : null; 35 | 36 | return ( 37 |
38 | } 43 | onEnter={this.handleEnter} 44 | onExit={this.handleExit} 45 | /> 46 | {/*{Panel} 47 | 48 | 53 | */} 54 |
55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.0.0 2 | 3 | * Remove react-dom requirement and require React 16 4 | 5 | # 1.0.2 6 | 7 | * Update React peerDependency to ^16.0.0 8 | 9 | 10 | # 1.0.0 11 | 12 | * Fix issue with Fill typescript definitions 13 | * First proper 1.0.0 release 14 | 15 | # 1.0.0.alpha.12 16 | 17 | * fix issue with mitt es6 imports in webpack 2 18 | 19 | # 1.0.0.alpha.11 20 | 21 | * update to support React 15.5.x and TypeScript 2.3.x 22 | 23 | # 1.0.0.alpha.10 24 | 25 | * lock down mitt version due to breaking change 26 | 27 | # 1.0.0.alpha.9 28 | 29 | * add Provider#getFillsByName and Provider#getChildrenByName (#13) 30 | 31 | # 1.0.0.alpha.8 32 | 33 | * Bad build 34 | 35 | # 1.0.0.alpha.7 36 | 37 | * Fix bug where Slot couldn't render null 38 | 39 | # 1.0.0.alpha.6 40 | 41 | * [Converted to TypeScript](https://github.com/camwest/react-slot-fill/pull/10) 42 | * React 15.x is now supported and extra div elements will be inserted where needed 43 | 44 | # 1.0.0.alpha.5 45 | 46 | * [Move mutt to dependencies](https://github.com/camwest/react-slot-fill/commit/b628e8f4cf1ba83c78fb037ce147867f06bb2296) 47 | * [remove react-dom peerDependency. react-slot-fill should work with react-native now](https://github.com/camwest/react-slot-fill/commit/47a0a9569e90443d6addd03bb21adc6988a1a90e) 48 | 49 | # 1.0.0.alpha.4 50 | 51 | * [Replace DOM usage with mitt pub-sub](https://github.com/camwest/react-slot-fill/commit/7c4bac3d4cab2969c01362febb5deb87a6b78cc3) 52 | 53 | ## Contributors 54 | 55 | * @Craga89 56 | 57 | # 1.0.0.alpha.3 58 | 59 | oops! no changes 60 | 61 | # 1.0.0.alpha.2 62 | 63 | * [Add react@next and react-dom@next dependencies instead of hard-coding Fiber](https://github.com/camwest/react-slot-fill/commit/c3179db4b5abe2ab59298707c6c8e76e0dc605ae) 64 | * [Add Provider component which avoids using global state](https://github.com/camwest/react-slot-fill/commit/b5166d365e809cf68c6cf261f5b5c80040a43528) 65 | * [Change highlight to JSX on README](https://github.com/camwest/react-slot-fill/commit/9e06bc64b96b6465894a855d423752cac79ae283) 66 | 67 | ## Contributors: 68 | 69 | * @rogeliog 70 | * @camwest 71 | * @davesnx 72 | 73 | # 1.0.0.alpha.1 74 | 75 | Initial Release. Note this requires react-fiber to function correctly since Slot 76 | returns an array of elements which is only supported there. 77 | -------------------------------------------------------------------------------- /src/demo/simple/NewsContent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default () => ( 4 |
5 |
6 |
7 |
8 |

9 | Tech Giant Invests Huge Money to Build a Computer Out of Science Fiction

10 |

11 | The tech giant says it is ready to begin planning a quantum 12 | computer, a powerful cpu machine that relies on subatomic particles instead 13 | of transistors. 14 |

15 |
16 |
17 | Dimly lit room with a computer interface terminal. 22 |
23 |
24 |

By Robin Darnell

25 | 26 |
27 |
28 |
29 |
30 |

A whale takes up residence in a large body of water

31 |

32 | This giant of a whale says it is ready to begin planning a new 33 | swim later this afternoon. A powerful mammal that relies on fish and plankton instead 34 | of hamburgers. 35 |

36 |
37 |
38 | Whale's tale coming crashing out of the water. 43 |
44 |
45 |

By Katherine Grant

46 | 47 |
48 |
49 | ); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-slot-fill", 3 | "version": "2.0.0", 4 | "private": false, 5 | "homepage": "https://camwest.github.io/react-slot-fill", 6 | "module": "lib/rsf.es.js", 7 | "main": "lib/rsf.js", 8 | "typings": "lib/index.d.ts", 9 | "dependencies": { 10 | "mitt": "^1.1.0" 11 | }, 12 | "devDependencies": { 13 | "@types/enzyme": "^3.0.0", 14 | "@types/enzyme-adapter-react-16": "^1.0.0", 15 | "@types/jest": "^21.1.5", 16 | "@types/mousetrap": "^1.5.34", 17 | "@types/node": "^8.0.47", 18 | "@types/prop-types": "^15.5.2", 19 | "@types/react": "^16.0.0", 20 | "@types/react-dom": "^16.0.2", 21 | "@types/react-split-pane": "^0.1.10", 22 | "@types/react-test-renderer": "^16.0.0", 23 | "babel-cli": "^6.23.0", 24 | "babel-eslint": "^7.2.1", 25 | "babel-plugin-external-helpers": "^6.22.0", 26 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 27 | "babel-plugin-transform-react-jsx": "^6.23.0", 28 | "babel-preset-env": "^1.2.1", 29 | "babel-preset-es2015": "^6.22.0", 30 | "babel-preset-react": "^6.23.0", 31 | "codacy-coverage": "^2.0.1", 32 | "enzyme": "^3.0.0", 33 | "enzyme-adapter-react-16": "^1.0.0", 34 | "eslint": "^3.19.0", 35 | "eslint-plugin-import": "^2.2.0", 36 | "eslint-plugin-jsx-a11y": "^4.0.0", 37 | "eslint-plugin-react": "^6.10.3", 38 | "gh-pages": "^0.12.0", 39 | "mousetrap": "^1.6.0", 40 | "prop-types": "^15.5.8", 41 | "react": "^16.0.0", 42 | "react-dom": "^16.0.0", 43 | "react-icons": "^2.2.3", 44 | "react-scripts-ts": "^2.8.0", 45 | "react-split-pane": "^0.1.58", 46 | "react-test-renderer": "^16.0.0", 47 | "rollup": "^0.50.0", 48 | "rollup-plugin-babel": "^2.7.1", 49 | "rollup-plugin-typescript2": "^0.5.0", 50 | "shelljs": "^0.7.7", 51 | "tachyons": "^4.6.2", 52 | "ts-jest": "^21.1.4", 53 | "tslint": "^5.8.0", 54 | "tslint-react": "^3.2.0", 55 | "typescript": "^2.5.3" 56 | }, 57 | "peerDependencies": { 58 | "prop-types": "^15.5.8", 59 | "react": "^16.0.0" 60 | }, 61 | "scripts": { 62 | "start": "react-scripts-ts start", 63 | "build": "react-scripts-ts build", 64 | "test": "react-scripts-ts test --env=jsdom", 65 | "eject": "react-scripts-ts eject", 66 | "predeploy": "npm run build", 67 | "deploy": "gh-pages -d build", 68 | "prepublish": "node script/prepublish.js", 69 | "lint": "eslint .", 70 | "test-+-coverage": 71 | "react-scripts-ts test --env=jsdom --coverage --collectCoverageFrom=src/lib/**/*.{ts,tsx}", 72 | "send-coverage": "cat coverage/lcov.info | codacy-coverage" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | ## Contributing to react-slot-fill 18 | 19 | ### Filing Issues 20 | 21 | **Suggestions** 22 | 23 | react-slot-fill is meant to evolve with feedback - the project and its users greatly appreciate any thoughts on ways to improve the design or features. Please use the `enhancement` tag to specifically denote issues that are suggestions - this helps us triage and respond appropriately. 24 | 25 | **Bugs** 26 | 27 | As with all pieces of software, you may end up running into bugs. Please submit bugs as regular issues on GitHub - Orion developers are regularly monitoring issues and will try to fix open bugs quickly. 28 | 29 | The best bug reports include a detailed way to predictably reproduce the issue, and possibly even a working example that demonstrates the issue. 30 | 31 | ### Contributing Code 32 | 33 | react-slot-fill accepts and greatly appreciates contributions. The project follows the [fork & pull](https://help.github.com/articles/using-pull-requests/#fork--pull) model for accepting contributions. 34 | 35 | When contributing code, please also include appropriate tests as part of the pull request, and follow the same comment and coding style as the rest of the project. Take a look through the existing code for examples of the testing and style practices the project follows. 36 | 37 | ### Contributing Features 38 | 39 | All pull requests for new features must go through the following process: 40 | * Start an Intent-to-implement GitHub issue for discussion of the new feature. 41 | * LGTM from Tech Lead and one other core committer is required 42 | * Development occurs on a separate branch of a separate fork, noted in the intent-to-implement issue 43 | * A pull request is created, referencing the issue. 44 | * Core team developers will provide feedback on pull requests, looking at code quality, style, tests, performance, and directional alignment with the goals of the project. That feedback should be discussed and incorporated 45 | * LGTM from Tech Lead and one other core committer, who confirm engineering quality and direction. 46 | 47 | ### See Also 48 | 49 | * [Code of conduct](CODE_OF_CONDUCT.md) 50 | * [DEVELOPING](DEVELOPING.md) resources 51 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-react" 4 | ], 5 | "rules": { 6 | "align": [ 7 | true, 8 | "parameters", 9 | "arguments", 10 | "statements" 11 | ], 12 | "ban": false, 13 | "class-name": true, 14 | "comment-format": [ 15 | true, 16 | "check-space" 17 | ], 18 | "curly": true, 19 | "eofline": false, 20 | "forin": true, 21 | "indent": [ 22 | true, 23 | "spaces" 24 | ], 25 | "interface-name": [ 26 | true, 27 | "never-prefix" 28 | ], 29 | "jsdoc-format": true, 30 | "jsx-no-lambda": false, 31 | "jsx-no-multiline-js": false, 32 | "label-position": true, 33 | "max-line-length": [ 34 | true, 35 | 120 36 | ], 37 | "member-ordering": [ 38 | true, 39 | "public-before-private", 40 | "static-before-instance", 41 | "variables-before-functions" 42 | ], 43 | "no-any": false, 44 | "no-arg": true, 45 | "no-bitwise": true, 46 | "no-console": [ 47 | true, 48 | "log", 49 | "error", 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-consecutive-blank-lines": true, 57 | "no-construct": true, 58 | "no-debugger": true, 59 | "no-duplicate-variable": true, 60 | "no-empty": true, 61 | "no-eval": true, 62 | "no-shadowed-variable": true, 63 | "no-string-literal": true, 64 | "no-switch-case-fall-through": true, 65 | "no-trailing-whitespace": false, 66 | "no-unused-expression": true, 67 | "no-use-before-declare": true, 68 | "one-line": [ 69 | true, 70 | "check-catch", 71 | "check-else", 72 | "check-open-brace", 73 | "check-whitespace" 74 | ], 75 | "quotemark": [ 76 | true, 77 | "single", 78 | "jsx-double" 79 | ], 80 | "radix": true, 81 | "semicolon": [ 82 | true, 83 | "always" 84 | ], 85 | "switch-default": true, 86 | "trailing-comma": false, 87 | "triple-equals": [ 88 | true, 89 | "allow-null-check" 90 | ], 91 | "typedef": [ 92 | true, 93 | "parameter", 94 | "property-declaration" 95 | ], 96 | "typedef-whitespace": [ 97 | true, 98 | { 99 | "call-signature": "nospace", 100 | "index-signature": "nospace", 101 | "parameter": "nospace", 102 | "property-declaration": "nospace", 103 | "variable-declaration": "nospace" 104 | } 105 | ], 106 | "variable-name": [ 107 | true, 108 | "ban-keywords", 109 | "check-format", 110 | "allow-leading-underscore", 111 | "allow-pascal-case" 112 | ], 113 | "whitespace": [ 114 | true, 115 | "check-branch", 116 | "check-decl", 117 | "check-module", 118 | "check-operator", 119 | "check-separator", 120 | "check-type", 121 | "check-typecast" 122 | ] 123 | } 124 | } -------------------------------------------------------------------------------- /src/demo/advanced/Workspace.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as SplitPane from 'react-split-pane'; 3 | 4 | import './Workspace.css'; 5 | import { Slot, Fill } from '../../lib'; 6 | 7 | const style: { [key: string]: any } = { 8 | container: { 9 | background: '#FFFFFF', 10 | opacity: 0.85, 11 | display: 'flex', 12 | flexGrow: 1 13 | }, 14 | 15 | AppBar: { 16 | width: '61px', 17 | background: '#F9F9F9', 18 | borderRight: 'solid 1px #C2C2C2' 19 | }, 20 | 21 | Panel: { 22 | background: '#FFFFFF', 23 | padding: '28px', 24 | overflow: 'hidden', 25 | height: '100%' 26 | }, 27 | 28 | SplitPaneContainer: { 29 | position: 'relative', 30 | flexGrow: 1 31 | } 32 | }; 33 | 34 | class Workspace extends React.Component { 35 | static AppBar = (props: any) => ( 36 | 37 |
{props.children}
38 |
39 | ) 40 | 41 | static Panel = (props: any) => ( 42 | 43 | 44 | 45 | ) 46 | 47 | static Canvas = (props: any) => ( 48 | 49 |
{props.children}
50 |
51 | ) 52 | 53 | constructor(props: any) { 54 | super(props); 55 | this.state = { showPanel: false, size: 500 }; 56 | this.handleOnMount = this.handleOnMount.bind(this); 57 | this.handleOnUnmount = this.handleOnUnmount.bind(this); 58 | this.handleSplitChange = this.handleSplitChange.bind(this); 59 | } 60 | 61 | handleOnMount() { 62 | this.setState({ showPanel: true }); 63 | } 64 | 65 | handleOnUnmount() { 66 | this.setState({ showPanel: false }); 67 | } 68 | 69 | handleSplitChange(size: any) { 70 | this.setState({ size }); 71 | } 72 | 73 | render() { 74 | const canvas = ; 75 | 76 | let content; 77 | 78 | if (this.state.showPanel) { 79 | content = ( 80 |
81 | 82 |
83 | 87 | {(items: any) =>
{items[items.length - 1]}
} 88 |
89 |
90 | {canvas} 91 |
92 |
93 | ); 94 | } else { 95 | content = ( 96 |
97 | 101 | {(items: any) =>
{items[items.length - 1]}
} 102 |
103 | {canvas} 104 |
105 | ); 106 | } 107 | 108 | return ( 109 |
110 |
111 | 112 |
113 | {content} 114 |
115 | ); 116 | } 117 | } 118 | 119 | class Panel extends React.Component { 120 | componentDidMount() { 121 | this.props.onMount(); 122 | } 123 | 124 | componentWillUnmount() { 125 | this.props.onUnmount(); 126 | } 127 | 128 | render() { 129 | const title = this.props.title 130 | ?

{this.props.title}

131 | : null; 132 | 133 | return ( 134 |
135 | {title} 136 | {this.props.children} 137 |
138 | ); 139 | } 140 | } 141 | 142 | export default Workspace; 143 | -------------------------------------------------------------------------------- /src/lib/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as renderer from 'react-test-renderer'; 3 | import { Fill, Slot, Provider } from '../'; 4 | 5 | class Toolbar extends React.Component { 6 | static Item = ({ label }: { label: string }) => ( 7 | 8 | 9 | 10 | ) 11 | 12 | render() { 13 | return ( 14 |
15 | 16 |
17 | ); 18 | } 19 | } 20 | 21 | class Footer extends React.Component { 22 | static Item = ({ href, label }: { href: string, label: string }) => ( 23 | 24 | {label} 25 | 26 | ) 27 | 28 | render() { 29 | return ( 30 |
31 | 32 |
33 | ); 34 | } 35 | } 36 | 37 | it('Fills the a simple slot', () => { 38 | const Feature = () => ; 39 | 40 | const fillComponent = renderer.create( 41 | 42 |
43 | 44 | 45 |
46 |
47 | ); 48 | 49 | expect(fillComponent).toMatchSnapshot(); 50 | }); 51 | 52 | it('Fills the appropriate slot', () => { 53 | class Feature extends React.Component { 54 | render() { 55 | return ( 56 |
57 | 58 | 59 | 60 |
61 | ); 62 | } 63 | } 64 | 65 | const fillComponent = renderer.create( 66 | 67 |
68 | 69 |
70 | 71 |
72 |
73 | ); 74 | 75 | expect(fillComponent).toMatchSnapshot(); 76 | }); 77 | 78 | it('allows slots to render null', () => { 79 | const Extensible = () => { 80 | return ( 81 | 82 | {(items: any) => { 83 | return null; 84 | }} 85 | 86 | ); 87 | }; 88 | 89 | const Insertion = () => ( 90 | Hello world! 91 | ); 92 | 93 | const tree: any = renderer.create( 94 | 95 |
96 | 97 | 98 |
99 |
100 | ); 101 | 102 | expect(tree).toMatchSnapshot(); 103 | }); 104 | 105 | it('Replaces the contents of the slot with the matching fill when the slot\'s `name` property changes', () => { 106 | 107 | class DynamicToolbar extends React.Component { 108 | // This example is contrived, but it covers Slot's componentWillReceiveProps 109 | static Active = ({ label }: { label: string }) => ( 110 | 111 | 112 | 113 | ) 114 | 115 | static Inactive = ({ label }: { label: string }) => ( 116 | 117 | {label} 118 | 119 | ) 120 | 121 | render() { 122 | return ( 123 |
124 | 125 |
126 | ); 127 | } 128 | } 129 | 130 | class Feature extends React.Component { 131 | render() { 132 | return ( 133 |
134 | , 135 | , 136 |
137 | ); 138 | } 139 | } 140 | 141 | const fillComponent: any = renderer.create( 142 | 143 |
144 | 145 | 146 |
147 |
148 | ); 149 | 150 | fillComponent.update( 151 | 152 |
153 | 154 | 155 |
156 |
157 | ); 158 | 159 | expect(fillComponent).toMatchSnapshot(); 160 | }); 161 | -------------------------------------------------------------------------------- /src/demo/advanced/AppBar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Slot, Fill } from '../../lib'; 3 | import Workspace from './Workspace'; 4 | import Keybinding from './Keybinding'; 5 | 6 | import './AppBar.css'; 7 | 8 | const style: any = { 9 | AppBar: { 10 | display: 'flex', 11 | flexDirection: 'column', 12 | justifyContent: 'space-between', 13 | height: '100%' 14 | }, 15 | 16 | AppBarGroup: { 17 | display: 'flex', 18 | flexDirection: 'column' 19 | } 20 | }; 21 | 22 | const AppBarPrimary = 'AppBar.Primary'; 23 | const AppBarUtility = 'AppBar.Utility'; 24 | 25 | class AppBar extends React.Component { 26 | static PrimaryItem = (props: any) => ; 27 | static UtilityItem = (props: any) => ; 28 | 29 | constructor(props: any) { 30 | super(props); 31 | this.state = { selection: null }; 32 | this.handleActivate = this.handleActivate.bind(this); 33 | } 34 | 35 | handleActivate(target: any) { 36 | if (this.state.selection === target) { 37 | target.props.onExit(); 38 | this.setState({ selection: null }); 39 | } else { 40 | target.props.onEnter(); 41 | 42 | // Remove old selection if we have one 43 | if (this.state.selection) { 44 | this.state.selection.props.onExit(); 45 | } 46 | 47 | // Remember selection 48 | this.setState({ selection: target }); 49 | } 50 | } 51 | 52 | render() { 53 | return ( 54 | 55 |
56 | 57 |
58 |
59 | 60 |
61 |
62 | ); 63 | } 64 | } 65 | 66 | export default AppBar; 67 | 68 | class HotkeyButton extends React.Component { 69 | static defaultProps = { 70 | onActivate: () => { /* no-op */ } 71 | }; 72 | 73 | constructor(props: any) { 74 | super(props); 75 | this.handleActivate = this.handleActivate.bind(this); 76 | } 77 | 78 | handleActivate() { 79 | this.props.onActivate(); 80 | } 81 | 82 | render() { 83 | // eslint-disable-next-line 84 | const { children, hotkey, onActivate, label, ...rest } = this.props; 85 | 86 | return ( 87 |
88 | 94 | 97 |
98 | ); 99 | } 100 | } 101 | 102 | class BasicIcon extends React.Component { 103 | static defaultProps = { 104 | icon: null, 105 | order: null, 106 | onEnter: () => { /* no-op */ }, 107 | onExit: () => { /* no-op */ } 108 | }; 109 | 110 | constructor(props: any) { 111 | super(props); 112 | this.state = { active: false }; 113 | this.handleEnter = this.handleEnter.bind(this); 114 | this.handleExit = this.handleExit.bind(this); 115 | } 116 | 117 | handleEnter() { 118 | this.setState({ active: true }); 119 | this.props.onEnter(); 120 | } 121 | 122 | handleExit() { 123 | this.setState({ active: false }); 124 | this.props.onExit(); 125 | } 126 | 127 | render() { 128 | const { icon, fill, label, order, hotkey } = this.props; 129 | 130 | const iconElement = icon 131 | ? React.cloneElement(icon, { size: 30, className: 'AppBar-AppBarItemIcon' }) 132 | : null; 133 | 134 | let className = 'AppBar-AppBarItem'; 135 | 136 | if (this.state.active) { 137 | className += ' AppBar-AppBarItem--active'; 138 | } 139 | 140 | return ( 141 | 142 | 143 | {iconElement} 144 | {label} 145 | 146 | 147 | ); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/demo/advanced/Keybinding.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as Mousetrap from 'mousetrap'; 3 | import { Slot, Fill } from '../../lib'; 4 | 5 | import Settings from './Settings'; 6 | 7 | export class Binding extends React.Component { 8 | static defaultProps = { 9 | hotkey: '', 10 | groupName: '', 11 | description: '', 12 | onInvoke: () => { /*no-op*/ } 13 | }; 14 | 15 | render() { 16 | const { onInvoke, hotkey, groupName, description } = this.props; 17 | const fillprops = { hotkey, groupName, description }; 18 | 19 | return ( 20 | 21 | 22 | 23 | ); 24 | } 25 | } 26 | 27 | export default class Keybinding extends React.Component { 28 | static Binding = Binding; 29 | 30 | constructor(props: any) { 31 | super(props); 32 | this.handleRegistered = this.handleRegistered.bind(this); 33 | this.handleUnregistered = this.handleUnregistered.bind(this); 34 | this.state = {}; 35 | } 36 | 37 | handleRegistered({ props }: { props: any }) { 38 | this.setState({ 39 | [props.hotkey]: { 40 | hotkey: props.hotkey, 41 | groupName: props.groupName, 42 | description: props.description 43 | } 44 | }); 45 | } 46 | 47 | handleUnregistered({ props }: { props: any }) { 48 | this.setState({ 49 | [props.hotkey]: null 50 | }); 51 | } 52 | 53 | render() { 54 | const groupByFn = (acc: any, key: any) => { 55 | const binding = this.state[key]; 56 | 57 | const existingGroup: any = acc.find((group: any) => group.name === binding.groupName); 58 | 59 | if (existingGroup) { 60 | existingGroup.bindings.push(binding); 61 | } else { 62 | acc.push({ 63 | name: binding.groupName, 64 | bindings: [binding] 65 | }); 66 | } 67 | 68 | return acc; 69 | }; 70 | 71 | const groups = Object.keys(this.state).reduce(groupByFn, []); 72 | 73 | return ( 74 |
75 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | {groups.map((group: any, index: any) => { 89 | const numItems = group.bindings.length; 90 | return ( 91 | 92 | {group.bindings.map((binding: any, index2: any) => ( 93 | 94 | {index2 === 0 && 95 | 96 | } 97 | 98 | 99 | 100 | ))} 101 | 102 | ); 103 | })} 104 |
GroupHotkeyDescription
{group.name}{binding.hotkey}{binding.description}
105 |
106 |
107 | ); 108 | } 109 | } 110 | 111 | class Keybind extends React.Component { 112 | constructor(props: any) { 113 | super(props); 114 | this.handleInvoke = this.handleInvoke.bind(this); 115 | } 116 | 117 | handleInvoke() { 118 | this.props.onInvoke(); 119 | return false; 120 | } 121 | 122 | componentWillMount() { 123 | Mousetrap.bind(this.props.hotkey, this.handleInvoke); 124 | this.props.onRegistered(); 125 | } 126 | 127 | componentWillUnmount() { 128 | Mousetrap.unbind(this.props.hotkey); 129 | this.props.onUnRegistered(); 130 | } 131 | 132 | componentWillReceiveProps(nextProps: any) { 133 | Mousetrap.unbind(this.props.hotkey); 134 | Mousetrap.bind(this.props.hotkey, this.handleInvoke); 135 | } 136 | 137 | render() { 138 | return null; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/lib/components/Slot.ts: -------------------------------------------------------------------------------- 1 | import { managerShape } from '../utils/PropTypes'; 2 | import * as React from 'react'; 3 | import Fill from './Fill'; 4 | import Manager, { Component } from '../Manager'; 5 | 6 | import { Requireable } from 'prop-types'; 7 | 8 | export interface Props { 9 | /** 10 | * The name of the component. Use a symbol if you want to be 100% sue the Slot 11 | * will only be filled by a component you create 12 | */ 13 | name: string | Symbol; 14 | 15 | /** 16 | * Props to be applied to the child Element of every fill which has the same name. 17 | * 18 | * If the value is a function, it must have the following signature: 19 | * (target: Fill, fills: Fill[]) => void; 20 | * 21 | * This allows you to access props on the fill which invoked the function 22 | * by using target.props.something() 23 | */ 24 | fillChildProps?: { [key: string]: any }; 25 | } 26 | 27 | export interface State { 28 | components: Component[]; 29 | } 30 | 31 | export interface Context { 32 | manager: Manager; 33 | } 34 | 35 | export default class Slot extends React.Component { 36 | static contextTypes = { 37 | manager: managerShape 38 | }; 39 | 40 | context: Context; 41 | 42 | constructor(props: Props) { 43 | super(props); 44 | this.state = { components: [] }; 45 | this.handleComponentChange = this.handleComponentChange.bind(this); 46 | } 47 | 48 | componentWillMount() { 49 | this.context.manager.onComponentsChange(this.props.name, this.handleComponentChange); 50 | } 51 | 52 | handleComponentChange(components: Component[]) { 53 | this.setState({ components }); 54 | } 55 | 56 | get fills(): Fill[] { 57 | return this.state.components.map(c => c.fill); 58 | } 59 | 60 | componentWillReceiveProps(nextProps: Props) { 61 | if (nextProps.name !== this.props.name) { 62 | this.context.manager.removeOnComponentsChange(this.props.name, this.handleComponentChange); 63 | 64 | const name = nextProps.name; 65 | 66 | this.context.manager.onComponentsChange(name, this.handleComponentChange); 67 | } 68 | } 69 | 70 | componentWillUnmount() { 71 | const name = this.props.name; 72 | this.context.manager.removeOnComponentsChange(name, this.handleComponentChange); 73 | } 74 | 75 | render() { 76 | const aggElements: React.ReactNode[] = []; 77 | 78 | this.state.components.forEach((component, index) => { 79 | const { fill, children } = component; 80 | const { fillChildProps } = this.props; 81 | 82 | if (fillChildProps) { 83 | const transform = (acc: {}, key: string) => { 84 | const value = fillChildProps[key]; 85 | 86 | if (typeof value === 'function') { 87 | acc[key] = () => value(fill, this.fills); 88 | } else { 89 | acc[key] = value; 90 | } 91 | 92 | return acc; 93 | }; 94 | 95 | const fillChildProps2 = Object.keys(fillChildProps).reduce(transform, {}); 96 | 97 | children.forEach((child, index2) => { 98 | if (typeof child === 'number' || typeof child === 'string') { 99 | throw new Error('Only element children will work here'); 100 | } 101 | aggElements.push( 102 | React.cloneElement(child, { key: index.toString() + index2.toString(), ...fillChildProps2 }) 103 | ); 104 | }); 105 | } else { 106 | children.forEach((child, index2) => { 107 | if (typeof child === 'number' || typeof child === 'string') { 108 | throw new Error('Only element children will work here'); 109 | } 110 | 111 | aggElements.push( 112 | React.cloneElement(child, { key: index.toString() + index2.toString() }) 113 | ); 114 | }); 115 | } 116 | }); 117 | 118 | if (typeof this.props.children === 'function') { 119 | const element = this.props.children(aggElements); 120 | 121 | if (React.isValidElement(element) || element === null) { 122 | return element; 123 | } else { 124 | const untypedThis: any = this; 125 | const parentConstructor = untypedThis._reactInternalInstance._currentElement._owner._instance.constructor; 126 | const displayName = parentConstructor.displayName || parentConstructor.name; 127 | const message = `Slot rendered with function must return a valid React ` + 128 | `Element. Check the ${displayName} render function.`; 129 | throw new Error(message); 130 | } 131 | } else { 132 | return aggElements; 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-slot-fill · [![CircleCI Status](https://circleci.com/gh/camwest/react-slot-fill.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/camwest/react-slot-fill) [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/e7c3e47817fc4a81baca16cdb9a78ac1)](https://www.codacy.com/app/cameron_4/react-slot-fill?utm_source=github.com&utm_medium=referral&utm_content=camwest/react-slot-fill&utm_campaign=Badge_Coverage) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) 2 | 3 | 4 | 5 | ![Image](images/slot-fill-logo.png) 6 | 7 | Slot & Fill component for merging React subtrees together. 8 | 9 | ## Check out the [simple demo on glitch](https://rsf-demo.glitch.me/) ([view source](https://glitch.com/edit/#!/project/rsf-demo)) 10 | 11 | ## Installation 12 | 13 | ``` 14 | npm install react-slot-fill --save 15 | ``` 16 | 17 | ### Check out the examples locally 18 | 19 | ``` 20 | git clone https://github.com/camwest/react-slot-fill 21 | cd react-slot-fill 22 | npm start 23 | ``` 24 | 25 | ## Usage 26 | 27 | **Note** These examples use React Fiber Alpha 28 | 29 | ### Toolbar.js 30 | 31 | ```jsx 32 | import { Slot, Fill } from 'react-slot-fill'; 33 | 34 | const Toolbar = (props) => 35 |
36 | 37 |
38 | 39 | export default Toolbar; 40 | 41 | Toolbar.Item = (props) => 42 | 43 | 44 | 45 | ``` 46 | 47 | ### Feature.js 48 | 49 | ```jsx 50 | import Toolbar from './Toolbar'; 51 | 52 | const Feature = () => 53 | [ 54 | 55 | ]; 56 | ``` 57 | 58 | ### App.js 59 | 60 | ```jsx 61 | import Toolbar from './Toolbar'; 62 | import Feature from './Feature'; 63 | 64 | import { Provider } from 'react-slot-fill'; 65 | 66 | const App = () => 67 | 68 | 69 | 70 | 71 | 72 | ReactDOMFiber.render( 73 | , 74 | document.getElementById('root') 75 | ); 76 | ``` 77 | 78 | ## Components 79 | 80 | ### 81 | 82 | Creates a Slot/Fill context. All Slot/Fill components must be descendants of Provider. You may only pass a single descendant to `Provider`. 83 | 84 | ```typescript 85 | interface Provider { 86 | /** 87 | * Returns instances of Fill react components 88 | */ 89 | getFillsByName(name: string): Fill[]; 90 | /** 91 | * Return React elements that were inside Fills 92 | */ 93 | getChildrenByName(name: string): React.ReactChild[]; 94 | } 95 | ``` 96 | 97 | `getFillsByName` and `getChildrenByName` are really useful for testing Fill components. 98 | See [src/lib/__tests/Provider.test.tsx](src/lib/__tests/Provider.test.tsx) for an example. 99 | 100 | ### 101 | 102 | Expose a global extension point 103 | 104 | ```javascript 105 | import { Slot } from 'react-slot-fill'; 106 | ``` 107 | 108 | #### Props 109 | 110 | ```typescript 111 | interface Props { 112 | /** 113 | * The name of the component. Use a symbol if you want to be 100% sue the Slot 114 | * will only be filled by a component you create 115 | */ 116 | name: string | Symbol; 117 | 118 | /** 119 | * Props to be applied to the child Element of every fill which has the same name. 120 | * 121 | * If the value is a function, it must have the following signature: 122 | * (target: Fill, fills: Fill[]) => void; 123 | * 124 | * This allows you to access props on the fill which invoked the function 125 | * by using target.props.something() 126 | */ 127 | fillChildProps?: {[key: string]: any} 128 | 129 | /** 130 | * A an optional function which gets all of the current fills for this slot 131 | * Allows sorting, or filtering before rendering. An example use-case could 132 | * be to only show a limited amount of fills. 133 | * 134 | * By default Slot injects an unstyled `
` element. If you want greater 135 | * control over presentation use this function. 136 | * 137 | * @example 138 | * 139 | * {(items) => {items}} 140 | * 141 | */ 142 | children?: (fills) => JSX.Element 143 | } 144 | ``` 145 | 146 | ### 147 | 148 | Render children into a Slot 149 | 150 | ```javascript 151 | import { Fill } from 'react-slot-fill'; 152 | ``` 153 | 154 | #### Props 155 | 156 | ```typescript 157 | interface Props { 158 | /** 159 | * The name of the slot that this fill should be related to. 160 | */ 161 | name: string | Symbol 162 | 163 | /** 164 | * one or more JSX.Elements which will be rendered 165 | */ 166 | children: JSX.Element | JSX.Element[] 167 | } 168 | ``` 169 | 170 | You can add additional props to the Fill which can be accessed in the parent node of the slot via fillChildProps. 171 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 16 | # The react-slot-fill open source project code of conduct 17 | 18 | All members, committers and volunteers in this community are required to act according to the following code of conduct. We encourage you to follow these guidelines, which help steer our interactions, keep the react-slot-fill Project a positive, growing project and community and provide and ensure a safe environment for everyone. 19 | 20 | ## Responsible and enforcing this code of conduct 21 | 22 | If you are being harassed, notice that someone else is being harassed, or have any other concerns, please contact us immediately: contact Cameron Westland (cameron.westland@autodesk.com) or Ron Meldiner ron.meldiner@autodesk.com. We are here to help you. Your reports will be taken seriously and not dismissed or argued with. 23 | 24 | ## What we believe in and how we act 25 | 26 | - We are committed to providing a friendly, safe and welcoming environment for everyone, regardless of gender, sexual orientation, personal ability or disability, ethnicity, religion, level of experience, set of skills or similar personal characteristics. 27 | - Our community is based on mutual respect, tolerance, and encouragement. 28 | - We believe that a diverse community where people treat each other with respect is stronger, more vibrant and has more potential contributors and more sources for ideas. We aim for more diversity. 29 | - We are kind, welcoming and courteous to everyone. 30 | - We’re respectful of others, their positions, their skills, their commitments and their efforts. 31 | - We’re attentive in our communications, whether in person or online, and we're tactful when approaching differing views. 32 | - We are aware that language shapes reality. Thus, we use inclusive, gender-neutral language in the documents we provide and when we talk to people. When referring to a group of people, we aim to use gender-neutral terms like “team”, “folks”, “everyone”. (For details, we recommend [this post](https://modelviewculture.com/pieces/gendered-language-feature-or-bug-in-software-documentation)). 33 | - We respect that people have differences of opinion and criticize constructively. 34 | 35 | ## Don't 36 | 37 | - Don’t be mean or rude. 38 | - Don’t discriminate against anyone. Although we have phrased the formal diversity statement generically to make it all-inclusive, we recognize that there are specific attributes that are used to discriminate against people. In alphabetical order, some of these attributes include (but are not limited to): age, culture, ethnicity, gender identity or expression, national origin, physical or mental difference, politics, race, religion, sex, sexual orientation, socio-economic status, and subculture. We welcome people regardless of these or other attributes. 39 | - Sexism and racism of any kind (including sexist and racist “jokes”), demeaning or insulting behaviour and harassment are seen as direct violations to this code of conduct. Harassment includes offensive verbal comments related to gender, sexual orientation, disability, physical appearance, body size, race, religion, sexual images in public spaces, deliberate intimidation, stalking, following, harassing photography or recording, inappropriate physical contact, and unwelcome sexual attention. 40 | - On IRC, Slack and other online or offline communications channels, don't use overtly sexual nicknames or other nicknames that might detract from a friendly, safe and welcoming environment for all. 41 | - Unwelcome / non-consensual sexual advances over IRC or any other channels related with this community are not okay. 42 | - Derailing, tone arguments and otherwise playing on people's desires to be nice are not welcome, especially in discussions about violations to this code of conduct. 43 | - Please avoid unstructured critique. 44 | - Likewise, any spamming, trolling, flaming, baiting or other attention-stealing behaviour is not welcome. 45 | 46 | ## Consequences for violations of this code of conduct 47 | 48 | If a participant engages in any behavior violating this code of conduct, the core members of this community may take any action they deem appropriate, including warning the offender or expulsion from the community, exclusion from any interaction and loss of all rights in this community. 49 | 50 | ## Decisions about consequences of violations 51 | 52 | Decisions about consequences of violations of this code of conduct are being made by this community's core committers and will not be discussed with the person responsible for the violation. 53 | 54 | ## For questions or feedback 55 | 56 | If you have any questions or feedback on this code of conduct, we're happy to hear from you: camwest@gmail.com 57 | 58 | ## Thanks for the inspiration 59 | 60 | This code of conduct is based on the [AMP open source project code of conduct](https://github.com/ampproject/amphtml/blob/master/CODE_OF_CONDUCT.md). 61 | 62 | ## License 63 | 64 | This page is licensed as [CC-BY-NC](http://creativecommons.org/licenses/by-nc/4.0/). -------------------------------------------------------------------------------- /src/lib/Manager.ts: -------------------------------------------------------------------------------- 1 | import { Children } from 'react'; 2 | import Fill from './components/Fill'; 3 | import * as mitt from 'mitt'; 4 | 5 | export type Name = string | Symbol; 6 | export type Listener = (components: Component[]) => void; 7 | 8 | export interface Component { 9 | name: Name; 10 | fill: Fill; 11 | children: React.ReactChild[]; 12 | } 13 | 14 | export interface FillRegistration { 15 | listeners: Listener[]; 16 | components: Component[]; 17 | } 18 | 19 | export interface Db { 20 | byName: Map; 21 | byFill: Map; 22 | } 23 | 24 | export default class Manager { 25 | private _bus: mitt.Emitter; 26 | private _db: Db; 27 | 28 | constructor(bus: mitt.Emitter) { 29 | this._bus = bus; 30 | 31 | this.handleFillMount = this.handleFillMount.bind(this); 32 | this.handleFillUpdated = this.handleFillUpdated.bind(this); 33 | this.handleFillUnmount = this.handleFillUnmount.bind(this); 34 | 35 | this._db = { 36 | byName: new Map(), 37 | byFill: new Map() 38 | }; 39 | } 40 | 41 | mount() { 42 | this._bus.on('fill-mount', this.handleFillMount); 43 | this._bus.on('fill-updated', this.handleFillUpdated); 44 | this._bus.on('fill-unmount', this.handleFillUnmount); 45 | } 46 | 47 | unmount() { 48 | this._bus.off('fill-mount', this.handleFillMount); 49 | this._bus.off('fill-updated', this.handleFillUpdated); 50 | this._bus.off('fill-unmount', this.handleFillUnmount); 51 | } 52 | 53 | handleFillMount({ fill }: { fill: Fill }) { 54 | const children = Children.toArray(fill.props.children); 55 | const name = fill.props.name; 56 | const component = { fill, children, name }; 57 | 58 | // If the name is already registered 59 | const reg = this._db.byName.get(name); 60 | 61 | if (reg) { 62 | reg.components.push(component); 63 | 64 | // notify listeners 65 | reg.listeners.forEach(fn => fn(reg.components)); 66 | } else { 67 | this._db.byName.set(name, { 68 | listeners: [], 69 | components: [component] 70 | }); 71 | } 72 | 73 | this._db.byFill.set(fill, component); 74 | } 75 | 76 | handleFillUpdated({ fill }: { fill: Fill }) { 77 | // Find the component 78 | const component = this._db.byFill.get(fill); 79 | 80 | // Get the new elements 81 | const newElements = Children.toArray(fill.props.children); 82 | 83 | if (component) { 84 | // replace previous element with the new one 85 | component.children = newElements; 86 | 87 | const name = component.name; 88 | 89 | // notify listeners 90 | const reg = this._db.byName.get(name); 91 | 92 | if (reg) { 93 | reg.listeners.forEach(fn => fn(reg.components)); 94 | } else { 95 | throw new Error('registration was expected to be defined'); 96 | } 97 | } else { 98 | throw new Error('component was expected to be defined'); 99 | } 100 | } 101 | 102 | handleFillUnmount({ fill }: { fill: Fill }) { 103 | const oldComponent = this._db.byFill.get(fill); 104 | 105 | if (!oldComponent) { 106 | throw new Error('component was expected to be defined'); 107 | } 108 | 109 | const name = oldComponent.name; 110 | const reg = this._db.byName.get(name); 111 | 112 | if (!reg) { 113 | throw new Error('registration was expected to be defined'); 114 | } 115 | 116 | const components = reg.components; 117 | 118 | // remove previous component 119 | components.splice(components.indexOf(oldComponent), 1); 120 | 121 | // Clean up byFill reference 122 | this._db.byFill.delete(fill); 123 | 124 | if (reg.listeners.length === 0 && 125 | reg.components.length === 0) { 126 | this._db.byName.delete(name); 127 | } else { 128 | // notify listeners 129 | reg.listeners.forEach(fn => fn(reg.components)); 130 | } 131 | } 132 | 133 | /** 134 | * Triggers once immediately, then each time the components change for a location 135 | * 136 | * name: String, fn: (components: Component[]) => void 137 | */ 138 | onComponentsChange(name: Name, fn: Listener) { 139 | const reg = this._db.byName.get(name); 140 | 141 | if (reg) { 142 | reg.listeners.push(fn); 143 | fn(reg.components); 144 | } else { 145 | this._db.byName.set(name, { 146 | listeners: [fn], 147 | components: [] 148 | }); 149 | fn([]); 150 | } 151 | } 152 | 153 | getFillsByName(name: string): Fill[] { 154 | const registration = this._db.byName.get(name); 155 | 156 | if (!registration) { 157 | return []; 158 | } else { 159 | return registration.components.map(c => c.fill); 160 | } 161 | } 162 | 163 | getChildrenByName(name: string): React.ReactChild[] { 164 | const registration = this._db.byName.get(name); 165 | 166 | if (!registration) { 167 | return []; 168 | } else { 169 | return registration.components 170 | .map(component => component.children) 171 | .reduce((acc, memo) => acc.concat(memo), []); 172 | } 173 | } 174 | 175 | /** 176 | * Removes previous listener 177 | * 178 | * name: String, fn: (components: Component[]) => void 179 | */ 180 | removeOnComponentsChange(name: Name, fn: Listener) { 181 | const reg = this._db.byName.get(name); 182 | 183 | if (!reg) { 184 | throw new Error('expected registration to be defined'); 185 | } 186 | 187 | const listeners = reg.listeners; 188 | listeners.splice(listeners.indexOf(fn), 1); 189 | } 190 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | 6 | parser: 'babel-eslint', 7 | 8 | plugins: ['import', 'jsx-a11y', 'react'], 9 | 10 | env: { 11 | browser: true, 12 | commonjs: true, 13 | es6: true, 14 | jest: true, 15 | node: true, 16 | }, 17 | 18 | parserOptions: { 19 | ecmaVersion: 6, 20 | sourceType: 'module', 21 | ecmaFeatures: { 22 | jsx: true, 23 | generators: true, 24 | experimentalObjectRestSpread: true, 25 | }, 26 | }, 27 | 28 | settings: { 29 | 'import/ignore': ['node_modules'], 30 | 'import/extensions': ['.js'], 31 | 'import/resolver': { 32 | node: { 33 | extensions: ['.js', '.json'], 34 | }, 35 | }, 36 | }, 37 | 38 | rules: { 39 | // http://eslint.org/docs/rules/ 40 | 'array-callback-return': 'warn', 41 | 'default-case': ['warn', { commentPattern: '^no default$' }], 42 | 'dot-location': ['warn', 'property'], 43 | eqeqeq: ['warn', 'allow-null'], 44 | 'new-parens': 'warn', 45 | 'no-array-constructor': 'warn', 46 | 'no-caller': 'warn', 47 | 'no-cond-assign': ['warn', 'always'], 48 | 'no-const-assign': 'warn', 49 | 'no-control-regex': 'warn', 50 | 'no-delete-var': 'warn', 51 | 'no-dupe-args': 'warn', 52 | 'no-dupe-class-members': 'warn', 53 | 'no-dupe-keys': 'warn', 54 | 'no-duplicate-case': 'warn', 55 | 'no-empty-character-class': 'warn', 56 | 'no-empty-pattern': 'warn', 57 | 'no-eval': 'warn', 58 | 'no-ex-assign': 'warn', 59 | 'no-extend-native': 'warn', 60 | 'no-extra-bind': 'warn', 61 | 'no-extra-label': 'warn', 62 | 'no-fallthrough': 'warn', 63 | 'no-func-assign': 'warn', 64 | 'no-implied-eval': 'warn', 65 | 'no-invalid-regexp': 'warn', 66 | 'no-iterator': 'warn', 67 | 'no-label-var': 'warn', 68 | 'no-labels': ['warn', { allowLoop: false, allowSwitch: false }], 69 | 'no-lone-blocks': 'warn', 70 | 'no-loop-func': 'warn', 71 | 'no-mixed-operators': [ 72 | 'warn', 73 | { 74 | groups: [ 75 | ['&', '|', '^', '~', '<<', '>>', '>>>'], 76 | ['==', '!=', '===', '!==', '>', '>=', '<', '<='], 77 | ['&&', '||'], 78 | ['in', 'instanceof'], 79 | ], 80 | allowSamePrecedence: false, 81 | }, 82 | ], 83 | 'no-multi-str': 'warn', 84 | 'no-native-reassign': 'warn', 85 | 'no-negated-in-lhs': 'warn', 86 | 'no-new-func': 'warn', 87 | 'no-new-object': 'warn', 88 | 'no-new-symbol': 'warn', 89 | 'no-new-wrappers': 'warn', 90 | 'no-obj-calls': 'warn', 91 | 'no-octal': 'warn', 92 | 'no-octal-escape': 'warn', 93 | 'no-redeclare': 'warn', 94 | 'no-regex-spaces': 'warn', 95 | 'no-restricted-syntax': ['warn', 'LabeledStatement', 'WithStatement'], 96 | 'no-script-url': 'warn', 97 | 'no-self-assign': 'warn', 98 | 'no-self-compare': 'warn', 99 | 'no-sequences': 'warn', 100 | 'no-shadow-restricted-names': 'warn', 101 | 'no-sparse-arrays': 'warn', 102 | 'no-template-curly-in-string': 'warn', 103 | 'no-this-before-super': 'warn', 104 | 'no-throw-literal': 'warn', 105 | 'no-undef': 'error', 106 | 'no-restricted-globals': ['error', 'event'], 107 | 'no-unexpected-multiline': 'warn', 108 | 'no-unreachable': 'warn', 109 | 'no-unused-expressions': [ 110 | 'warn', 111 | { 112 | allowShortCircuit: true, 113 | allowTernary: true, 114 | }, 115 | ], 116 | 'no-unused-labels': 'warn', 117 | 'no-unused-vars': [ 118 | 'warn', 119 | { 120 | vars: 'local', 121 | varsIgnorePattern: '^_', 122 | args: 'none', 123 | ignoreRestSiblings: true, 124 | }, 125 | ], 126 | 'no-use-before-define': ['warn', 'nofunc'], 127 | 'no-useless-computed-key': 'warn', 128 | 'no-useless-concat': 'warn', 129 | 'no-useless-constructor': 'warn', 130 | 'no-useless-escape': 'warn', 131 | 'no-useless-rename': [ 132 | 'warn', 133 | { 134 | ignoreDestructuring: false, 135 | ignoreImport: false, 136 | ignoreExport: false, 137 | }, 138 | ], 139 | 'no-with': 'warn', 140 | 'no-whitespace-before-property': 'warn', 141 | 'operator-assignment': ['warn', 'always'], 142 | radix: 'warn', 143 | 'require-yield': 'warn', 144 | 'rest-spread-spacing': ['warn', 'never'], 145 | strict: ['warn', 'never'], 146 | 'unicode-bom': ['warn', 'never'], 147 | 'use-isnan': 'warn', 148 | 'valid-typeof': 'warn', 149 | 'no-restricted-properties': [ 150 | 'error', 151 | { 152 | object: 'require', 153 | property: 'ensure', 154 | message: 'Please use import() instead. More info: https://webpack.js.org/guides/code-splitting-import/#dynamic-import', 155 | }, 156 | { 157 | object: 'System', 158 | property: 'import', 159 | message: 'Please use import() instead. More info: https://webpack.js.org/guides/code-splitting-import/#dynamic-import', 160 | }, 161 | ], 162 | 163 | 164 | 'import/no-webpack-loader-syntax': 'error', 165 | 166 | // https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules 167 | 'react/jsx-equals-spacing': ['warn', 'never'], 168 | 'react/jsx-no-duplicate-props': ['warn', { ignoreCase: true }], 169 | 'react/jsx-no-undef': 'error', 170 | 'react/jsx-pascal-case': [ 171 | 'warn', 172 | { 173 | allowAllCaps: true, 174 | ignore: [], 175 | }, 176 | ], 177 | 'react/jsx-uses-react': 'warn', 178 | 'react/jsx-uses-vars': 'warn', 179 | 'react/no-danger-with-children': 'warn', 180 | 'react/no-deprecated': 'warn', 181 | 'react/no-direct-mutation-state': 'warn', 182 | 'react/no-is-mounted': 'warn', 183 | 'react/react-in-jsx-scope': 'error', 184 | 'react/require-render-return': 'warn', 185 | 'react/style-prop-object': 'warn', 186 | 187 | // https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules 188 | 'jsx-a11y/aria-role': 'warn', 189 | 'jsx-a11y/img-has-alt': 'warn', 190 | 'jsx-a11y/img-redundant-alt': 'warn', 191 | 'jsx-a11y/no-access-key': 'warn' 192 | }, 193 | }; --------------------------------------------------------------------------------