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 | };
--------------------------------------------------------------------------------