├── .editorconfig
├── .gitignore
├── .travis.yml
├── README.md
├── package.json
├── src
├── index.js
├── slot-content.js
├── slot-provider.js
├── slot.js
└── with-slot.js
└── test
├── index.test.js
├── slot-content.test.js
├── slot-provider.test.js
├── slot.test.js
└── with-slot.test.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [{package.json,.*rc,*.yml}]
11 | indent_style = space
12 | indent_size = 2
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | *.log
4 | dist
5 | package-lock.json
6 | yarn.lock
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - stable
4 | dist: trusty
5 | sudo: false
6 | addons:
7 | chrome: stable
8 | cache:
9 | npm: true
10 | directories:
11 | - node_modules
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | # preact-slots [](https://www.npmjs.org/package/preact-slots) [](https://travis-ci.org/developit/preact-slots)
6 |
7 | Render Preact trees into other Preact trees, like portals.
8 |
9 |
10 | ## Install
11 |
12 | **preact-slots** is available on npm:
13 |
14 | `npm install --save preact-slots`
15 |
16 |
17 | ### Usage
18 |
19 | Define "holes" in your appliation using ``,
20 | then fill them using `some content`:
21 |
22 | ```js
23 | import { SlotProvider, Slot, SlotContent } from 'preact-slots'
24 |
25 | render(
26 |
27 |
28 |
29 | Some Fallback Content
30 |
31 |
32 | Replacement Content
33 |
34 |
35 |
36 | )
37 | ```
38 |
39 | The above renders `Replacement Content
`.
40 |
41 | An extended example follows:
42 |
43 | ```js
44 | import { Slot, SlotContent, SlotProvider } from 'preact-slots'
45 |
46 | const Header = () => (
47 |
53 | )
54 |
55 | const EditPage = ({ page, content, onSave, onCancel }) => (
56 |
57 | Editing {page}
58 |
59 |
60 |
61 |
62 |
63 |
64 | )
65 |
66 | render(
67 |
68 |
69 |
70 |
71 |
72 |
73 | )
74 | ```
75 |
76 |
77 | ### Similar Libraries
78 |
79 | Slots are a concept that has been around for a while.
80 |
81 | In particular, [@camwest](https://github.com/camwest)'s [react-slot-fill](https://github.com/camwest/react-slot-fill) is similar to preact-slots, but geared towards React.
82 |
83 |
84 | ### License
85 |
86 | [MIT License](https://oss.ninja/mit/developit) © [Jason Miller](https://jasonformat.com)
87 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "preact-slots",
3 | "version": "1.0.0",
4 | "description": "Render Preact trees into other Preact trees, like portals.",
5 | "source": "src/index.js",
6 | "main": "dist/preact-slots.js",
7 | "module": "dist/preact-slots.m.js",
8 | "scripts": {
9 | "prepare": "microbundle",
10 | "dev": "karmatic watch",
11 | "test": "microbundle && karmatic",
12 | "release": "npm t && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish"
13 | },
14 | "eslintConfig": {
15 | "extends": "eslint-config-developit",
16 | "rules": {
17 | "jest/valid-expect": 0,
18 | "no-console": 0
19 | }
20 | },
21 | "files": [
22 | "src",
23 | "dist"
24 | ],
25 | "keywords": [
26 | "preact",
27 | "slots",
28 | "slot",
29 | "portal"
30 | ],
31 | "author": "Jason Miller (http://jasonformat.com)",
32 | "repository": "developit/preact-slots",
33 | "license": "MIT",
34 | "devDependencies": {
35 | "eslint": "^4.16.0",
36 | "eslint-config-developit": "^1.1.1",
37 | "karmatic": "^1.0.6",
38 | "microbundle": "^0.4.3",
39 | "preact": "^8.2.7",
40 | "preact-context-provider": "^1.0.0"
41 | },
42 | "peerDependencies": {
43 | "preact": "*"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { SlotProvider } from './slot-provider';
2 | export { SlotContent } from './slot-content';
3 | export { Slot } from './slot';
4 | export { withSlot } from './with-slot';
5 |
--------------------------------------------------------------------------------
/src/slot-content.js:
--------------------------------------------------------------------------------
1 | export class SlotContent {
2 | apply(slot, content, fireChange) {
3 | let { slots } = this.context;
4 | if (slot) {
5 | slots.named[slot] = content;
6 | if (fireChange) {
7 | for (let i=0; i {
6 | let content = this.context.slots.named[this.props.name];
7 | if (content!=this.state.content) {
8 | this.setState({ content });
9 | }
10 | };
11 | this.componentDidMount = () => {
12 | this.context.slots.onChange.push(update);
13 | };
14 | this.componentWillUnmount = () => {
15 | this.context.slots.onChange.push(update);
16 | };
17 | update();
18 | }
19 | (Slot.prototype = new Component()).constructor = Slot;
20 | Slot.prototype.render = function(props, state) {
21 | let child = props.children[0];
22 | return typeof child==='function' ? child(state.content) : (state.content || child);
23 | };
24 |
--------------------------------------------------------------------------------
/src/with-slot.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { Slot } from './slot';
3 |
4 | export function withSlot(name, alias) {
5 | return Child => props => h(Slot, { name }, content => {
6 | let childProps = {};
7 | childProps[alias || name] = content;
8 | for (let i in props) childProps[i] = props[i];
9 | return h(Child, childProps);
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/test/index.test.js:
--------------------------------------------------------------------------------
1 | import { SlotProvider, SlotContent, Slot, withSlot } from 'src';
2 | const exported = require('preact-slots');
3 |
4 | describe('index.js', () => {
5 | it('should export { SlotProvider, SlotContent, Slot, withSlot }', () => {
6 | expect(SlotProvider).toEqual(jasmine.any(Function));
7 | expect(SlotContent).toEqual(jasmine.any(Function));
8 | expect(Slot).toEqual(jasmine.any(Function));
9 | expect(withSlot).toEqual(jasmine.any(Function));
10 | });
11 |
12 | it('(cjs) export should be a bare object', () => {
13 | expect(exported).toEqual({
14 | SlotProvider: jasmine.any(Function),
15 | SlotContent: jasmine.any(Function),
16 | Slot: jasmine.any(Function),
17 | withSlot: jasmine.any(Function)
18 | });
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/test/slot-content.test.js:
--------------------------------------------------------------------------------
1 | import { h, render } from 'preact';
2 | import { SlotProvider, SlotContent } from 'src';
3 |
4 | describe('Slot', () => {
5 | it('should provide slots into context', () => {
6 | const Spy = new jasmine.Spy();
7 |
8 | render((
9 |
10 |
11 | bar
12 |
13 |
14 |
15 | ), document.createElement('x-root'));
16 |
17 | expect(Spy).toHaveBeenCalledTimes(1);
18 | expect(Spy).toHaveBeenCalledWith(jasmine.any(Object), {
19 | slots: {
20 | named: {
21 | foo: 'bar'
22 | },
23 | onChange: []
24 | }
25 | });
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/test/slot-provider.test.js:
--------------------------------------------------------------------------------
1 | import { h, render } from 'preact';
2 | import { SlotProvider } from 'src';
3 |
4 | describe('SlotProvider', () => {
5 | it('should provide slots into context', () => {
6 | const Spy = new jasmine.Spy();
7 |
8 | render((
9 |
10 |
11 |
12 | ), document.createElement('x-root'));
13 |
14 | expect(Spy).toHaveBeenCalledWith(jasmine.any(Object), {
15 | slots: {
16 | named: {},
17 | onChange: []
18 | }
19 | });
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/test/slot.test.js:
--------------------------------------------------------------------------------
1 | import { h, render } from 'preact';
2 | import { SlotProvider, Slot } from 'src';
3 | import Provider from 'preact-context-provider';
4 |
5 | const tick = () => new Promise(setTimeout);
6 |
7 | describe('Slot', () => {
8 | it('should render null for no content', () => {
9 | const spy = new jasmine.Spy();
10 |
11 | render((
12 |
13 | {spy}
14 |
15 | ), document.createElement('x-root'));
16 |
17 | expect(spy).toHaveBeenCalledTimes(1);
18 | expect(spy).toHaveBeenCalledWith(undefined);
19 | });
20 |
21 | it('should invoke child as function with content', () => {
22 | const spy = new jasmine.Spy();
23 |
24 | const slots = {
25 | named: {
26 | foo: 'bar'
27 | },
28 | onChange: []
29 | };
30 |
31 | render((
32 |
33 | {spy}
34 |
35 | ), document.createElement('x-root'));
36 |
37 | expect(spy).toHaveBeenCalledTimes(1);
38 | expect(spy).toHaveBeenCalledWith('bar');
39 | });
40 |
41 | it('should update when content changes', async () => {
42 | const spy = new jasmine.Spy();
43 |
44 | const slots = {
45 | named: {},
46 | onChange: []
47 | };
48 |
49 | render((
50 |
51 | {spy}
52 |
53 | ), document.createElement('x-root'));
54 |
55 | expect(slots.onChange).toContain(jasmine.any(Function));
56 |
57 | expect(spy).toHaveBeenCalledTimes(1);
58 | expect(spy).toHaveBeenCalledWith(undefined);
59 |
60 | slots.named.foo = 'bar';
61 | slots.onChange[0]();
62 | await tick();
63 |
64 | expect(spy).toHaveBeenCalledTimes(2);
65 | expect(spy).toHaveBeenCalledWith('bar');
66 | });
67 |
68 | it('should only update if content has changed', async () => {
69 | const spy = new jasmine.Spy();
70 |
71 | const slots = {
72 | named: {
73 | foo: 'bar'
74 | },
75 | onChange: []
76 | };
77 |
78 | render((
79 |
80 | {spy}
81 |
82 | ), document.createElement('x-root'));
83 |
84 | expect(spy).toHaveBeenCalledTimes(1);
85 | expect(spy).toHaveBeenCalledWith('bar');
86 |
87 | slots.onChange[0]();
88 | await tick();
89 |
90 | expect(spy).toHaveBeenCalledTimes(1);
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/test/with-slot.test.js:
--------------------------------------------------------------------------------
1 | import { h, render } from 'preact';
2 | import { withSlot } from 'src';
3 | import Provider from 'preact-context-provider';
4 |
5 | const tick = () => new Promise(setTimeout);
6 |
7 | describe('withSlot()', () => {
8 | it('should pass slot content to composed component', async () => {
9 | const Spy = new jasmine.Spy();
10 |
11 | const slots = {
12 | named: {
13 | foo: 'bar'
14 | },
15 | onChange: []
16 | };
17 |
18 | const Child = withSlot('foo')(Spy);
19 |
20 | render((
21 |
22 |
23 |
24 | ), document.createElement('x-root'));
25 |
26 | expect(Spy).toHaveBeenCalledTimes(1);
27 | expect(Spy).toHaveBeenCalledWith({ foo: 'bar', children: [] }, jasmine.anything());
28 | });
29 |
30 | it('should update when content changes', async () => {
31 | const Spy = new jasmine.Spy();
32 |
33 | const slots = {
34 | named: {},
35 | onChange: []
36 | };
37 |
38 | const Child = withSlot('foo')(Spy);
39 |
40 | render((
41 |
42 |
43 |
44 | ), document.createElement('x-root'));
45 |
46 | expect(Spy).toHaveBeenCalledTimes(1);
47 | expect(Spy).toHaveBeenCalledWith({ foo: undefined, children: [] }, jasmine.anything());
48 |
49 | expect(slots.onChange).toContain(jasmine.any(Function));
50 |
51 | slots.named.foo = 'bar';
52 | slots.onChange[0]();
53 | await tick();
54 |
55 | expect(Spy).toHaveBeenCalledTimes(2);
56 | expect(Spy).toHaveBeenCalledWith({ foo: 'bar', children: [] }, jasmine.anything());
57 | });
58 | });
59 |
--------------------------------------------------------------------------------