react-custom-portal
Advanced tools
Comparing version 1.0.3 to 1.0.4
@@ -1,6 +0,11 @@ | ||
import React, { createContext, useContext, useState, useLayoutEffect, useEffect } from 'react'; | ||
export const createPortal = (name = undefined) => { | ||
import React, { createContext, useContext, useState, useLayoutEffect, useEffect } from "react"; // | ||
// | ||
export const createPortal = options => { | ||
const { | ||
displayName | ||
} = getOptions(options); | ||
const PortalContext = createContext(); | ||
const Root = ({ | ||
const PortalRoot = ({ | ||
children | ||
@@ -14,20 +19,11 @@ }) => { | ||
const Content = props => { | ||
const [id] = useState(createID); | ||
PortalRoot.displayName = `${displayName}.Root`; | ||
const PortalContent = props => { | ||
const portal = useContext(PortalContext); | ||
const data = React.createElement(ContentRenderer, Object.assign({}, props, { | ||
key: id | ||
})); | ||
portal.data.set(id, data); | ||
useLayoutEffect(() => { | ||
portal.data.set(id, data); | ||
portal.dataChanged(); | ||
}); | ||
useEffect(() => () => { | ||
portal.data.delete(id); | ||
portal.dataChanged(); | ||
}, []); | ||
return null; | ||
return renderContentProxy(portal, ContentRenderer, props); | ||
}; | ||
PortalContent.displayName = `${displayName}.Content`; | ||
const ContentRenderer = ({ | ||
@@ -37,32 +33,128 @@ children | ||
const Render = ({ | ||
render = defaultRender | ||
ContentRenderer.displayName = PortalContent.displayName; | ||
const PortalRender = ({ | ||
children | ||
}) => { | ||
const [id] = useState(createID); | ||
const portal = useContext(PortalContext); | ||
const [, subscription] = useState(); | ||
portal.subscriptions.set(id, subscription); | ||
useLayoutEffect(() => { | ||
portal.subscriptions.set(id, subscription); | ||
}); | ||
useEffect(() => () => { | ||
portal.subscriptions.delete(id); | ||
}, []); | ||
return render([...portal.data.values()]); | ||
return usePortalRender(portal, children); | ||
}; | ||
if (name) { | ||
Root.displayName = `${name}.Root`; | ||
Content.displayName = `${name}.Content`; | ||
Render.displayName = `${name}.Render`; | ||
} | ||
PortalRender.displayName = `${displayName}.Render`; | ||
return { | ||
Root: PortalRoot, | ||
Content: PortalContent, | ||
Render: PortalRender | ||
}; | ||
}; // | ||
// | ||
ContentRenderer.displayName = Content.displayName || Content.name; | ||
export const createGlobalPortal = options => { | ||
const { | ||
displayName | ||
} = getOptions(options); | ||
const portal = createCustomPortal(); | ||
const PortalContent = props => renderContentProxy(portal, ContentRenderer, props); | ||
PortalContent.displayName = `${displayName}.Content`; | ||
const ContentRenderer = ({ | ||
children | ||
}) => children; | ||
ContentRenderer.displayName = PortalContent.displayName; | ||
const PortalRender = ({ | ||
children | ||
}) => usePortalRender(portal, children); | ||
PortalRender.displayName = `${displayName}.Render`; | ||
return { | ||
Root, | ||
Content, | ||
Render | ||
Content: PortalContent, | ||
Render: PortalRender | ||
}; | ||
}; | ||
}; // | ||
// | ||
const getOptions = (options, component) => { | ||
let actualOptions = typeof options === "string" ? { | ||
displayName: options | ||
} : { ...options | ||
}; | ||
if (!actualOptions.displayName) { | ||
actualOptions.displayName = `${component}${++DefaultPortalName}`; | ||
} | ||
return actualOptions; | ||
}; // | ||
// | ||
let DefaultPortalName = 0; // | ||
// | ||
const renderContentProxy = (portal, ContentRenderer, { | ||
children, | ||
preserveContexts, | ||
...proxyProps | ||
}) => function render(index, content) { | ||
if (preserveContexts && index < preserveContexts.length) { | ||
const Context = preserveContexts[index]; | ||
return React.createElement(Context.Consumer, null, value => render(index + 1, React.createElement(Context.Provider, { | ||
value: value | ||
}, content))); | ||
} else { | ||
return React.createElement(ContentProxy, { | ||
content, | ||
proxyProps, | ||
portal, | ||
ContentRenderer | ||
}); | ||
} | ||
}(0, children); // | ||
// | ||
const ContentProxy = ({ | ||
content, | ||
proxyProps, | ||
portal, | ||
ContentRenderer | ||
}) => { | ||
const [id] = useState(createID); | ||
const markup = React.createElement(ContentRenderer, Object.assign({}, proxyProps, { | ||
key: id | ||
}), content); | ||
portal.data.set(id, markup); | ||
useLayoutEffect(() => { | ||
portal.data.set(id, markup); | ||
portal.dataChanged(); | ||
}); | ||
useEffect(() => () => { | ||
portal.data.delete(id); | ||
portal.dataChanged(); // eslint-disable-next-line | ||
}, []); | ||
return null; | ||
}; // | ||
// | ||
const usePortalRender = (portal, children = defaultRender) => { | ||
const [id] = useState(createID); | ||
const [, subscription] = useState(); | ||
portal.subscriptions.set(id, subscription); // eslint-disable-next-line | ||
useLayoutEffect(() => { | ||
portal.subscriptions.set(id, subscription); | ||
}); | ||
useEffect(() => () => { | ||
portal.subscriptions.delete(id); // eslint-disable-next-line | ||
}, []); | ||
return children([...portal.data.values()]); | ||
}; // | ||
// | ||
const createCustomPortal = () => new CustomPortal(); | ||
@@ -77,3 +169,5 @@ | ||
dataChanged() { | ||
[...this.subscriptions.values()].forEach(subscription => subscription({})); | ||
for (const subscription of this.subscriptions.values()) { | ||
subscription({}); | ||
} | ||
} | ||
@@ -80,0 +174,0 @@ |
@@ -1,1 +0,1 @@ | ||
export { createPortal } from './createPortal'; | ||
export { createPortal, createGlobalPortal } from './createPortal'; |
{ | ||
"name": "react-custom-portal", | ||
"version": "1.0.3", | ||
"version": "1.0.4", | ||
"main": "dist/index.js", | ||
@@ -11,8 +11,8 @@ "dependencies": {}, | ||
"@babel/cli": "^7.8.4", | ||
"@testing-library/jest-dom": "^4.2.4", | ||
"@testing-library/react": "^9.3.2", | ||
"@testing-library/user-event": "^7.1.2", | ||
"@testing-library/jest-dom": "^5.1.1", | ||
"@testing-library/react": "^9.4.0", | ||
"@testing-library/user-event": "^10.0.0", | ||
"react": "^16.12.0", | ||
"react-dom": "^16.12.0", | ||
"react-scripts": "3.4.0" | ||
"react-scripts": "^3.4.0" | ||
}, | ||
@@ -72,4 +72,3 @@ "scripts": { | ||
"url": "https://github.com/react-custom-portal/react-custom-portal/issues" | ||
}, | ||
"homepage": "https://github.com/react-custom-portal/react-custom-portal#readme" | ||
} | ||
} |
@@ -15,3 +15,3 @@ ## react-custom-portal | ||
<ExternalMarkup.Content> | ||
This will not be rendered here, | ||
This text will not be rendered here, | ||
but instead it will be rendered within ExternalMarkup.Render | ||
@@ -26,4 +26,3 @@ </ExternalMarkup.Content> | ||
... | ||
{/* The content of <ExternalMarkup.Content ... /> will be rendered here | ||
instead of <ExternalMarkup.Render /> */} | ||
{/* The content of <ExternalMarkup.Content ... /> will be rendered here */} | ||
<ExternalMarkup.Render /> | ||
@@ -46,4 +45,21 @@ ... | ||
By default <Portal.Render /> will render the content of all the <Portal.Content /> been rendered within the <Portal.Root />. If you want to tweak some things you can set the `render` property of <Portal.Render /> to a function and it will be passed an array of <Portal.Content />s. Then filter them by their props and return some markup to render. | ||
By default <Portal.Render /> will render the content of all the <Portal.Content />s been rendered within the <Portal.Root />. If you want to tweak some things you can pass a function as the only child of <Portal.Render /> and it will be passed an array of <Portal.Content />s. | ||
`createPortal` can be passed a string `name` to tune the names of created components for debug purpose. | ||
```js | ||
... | ||
<ExternalMarkup.Content shape="circle" ... /> | ||
... | ||
<ExternalMarkup.Content shape="rectangle" ... /> | ||
... | ||
<ExternalMarkup.Render> | ||
{children => children.filter(child => child.props.shape === "rectangle")} | ||
</ExternalMarkup.Render> | ||
... | ||
``` | ||
`createPortal` can be passed an options object with `displayName` prop to tune the names of created components for debug purpose. | ||
```js | ||
const Objects3DSettings = createPortal({ displayName: "Objects3DSettings" }) | ||
// now react tools displays "Objects3DSettings.Content" for <Objects3DSettings.Content ... /> instead of "Portal.Content" | ||
``` |
@@ -1,7 +0,11 @@ | ||
import React, { createContext, useContext, useState, useLayoutEffect, useEffect } from 'react' | ||
import React, { createContext, useContext, useState, useLayoutEffect, useEffect } from "react" | ||
export const createPortal = (name = undefined) => { | ||
// | ||
// | ||
export const createPortal = (options) => { | ||
const { displayName } = getOptions(options) | ||
const PortalContext = createContext() | ||
const Root = ({ children }) => { | ||
const PortalRoot = ({ children }) => { | ||
const [portal] = useState(createCustomPortal) | ||
@@ -14,53 +18,122 @@ return ( | ||
} | ||
PortalRoot.displayName = `${displayName}.Root` | ||
const Content = (props) => { | ||
const [id] = useState(createID) | ||
const PortalContent = (props) => { | ||
const portal = useContext(PortalContext) | ||
const data = <ContentRenderer {...props} key={id} /> | ||
portal.data.set(id, data) | ||
useLayoutEffect(() => { | ||
portal.data.set(id, data) | ||
portal.dataChanged() | ||
}) | ||
useEffect(() => () => { | ||
portal.data.delete(id) | ||
portal.dataChanged() | ||
}, []) | ||
return null | ||
return renderContentProxy(portal, ContentRenderer, props) | ||
} | ||
PortalContent.displayName = `${displayName}.Content` | ||
const ContentRenderer = ({ children }) => children | ||
ContentRenderer.displayName = PortalContent.displayName | ||
const Render = ({ render = defaultRender }) => { | ||
const [id] = useState(createID) | ||
const PortalRender = ({ children }) => { | ||
const portal = useContext(PortalContext) | ||
const [, subscription] = useState() | ||
portal.subscriptions.set(id, subscription) | ||
useLayoutEffect(() => { | ||
portal.subscriptions.set(id, subscription) | ||
}) | ||
useEffect(() => () => { | ||
portal.subscriptions.delete(id) | ||
}, []) | ||
return render([...portal.data.values()]) | ||
return usePortalRender(portal, children) | ||
} | ||
PortalRender.displayName = `${displayName}.Render` | ||
if (name) { | ||
Root.displayName = `${name}.Root` | ||
Content.displayName = `${name}.Content` | ||
Render.displayName = `${name}.Render` | ||
return { | ||
Root: PortalRoot, | ||
Content: PortalContent, | ||
Render: PortalRender, | ||
} | ||
ContentRenderer.displayName = Content.displayName || Content.name | ||
} | ||
// | ||
// | ||
export const createGlobalPortal = (options) => { | ||
const { displayName } = getOptions(options) | ||
const portal = createCustomPortal() | ||
const PortalContent = props => renderContentProxy(portal, ContentRenderer, props) | ||
PortalContent.displayName = `${displayName}.Content` | ||
const ContentRenderer = ({ children }) => children | ||
ContentRenderer.displayName = PortalContent.displayName | ||
const PortalRender = ({ children }) => usePortalRender(portal, children) | ||
PortalRender.displayName = `${displayName}.Render` | ||
return { | ||
Root, | ||
Content, | ||
Render, | ||
Content: PortalContent, | ||
Render: PortalRender, | ||
} | ||
} | ||
// | ||
// | ||
const getOptions = (options, component) => { | ||
let actualOptions = typeof options === "string" ? { displayName: options } : { ...options } | ||
if (!actualOptions.displayName) { | ||
actualOptions.displayName = `${component}${++DefaultPortalName}` | ||
} | ||
return actualOptions | ||
} | ||
// | ||
// | ||
let DefaultPortalName = 0 | ||
// | ||
// | ||
const renderContentProxy = (portal, ContentRenderer, { children, preserveContexts, ...proxyProps }) => | ||
(function render(index, content) { | ||
if (preserveContexts && index < preserveContexts.length) { | ||
const Context = preserveContexts[index] | ||
return ( | ||
<Context.Consumer> | ||
{value => render(index + 1, <Context.Provider value={value}>{content}</Context.Provider>)} | ||
</Context.Consumer> | ||
) | ||
} else { | ||
return ( | ||
<ContentProxy {...{ content, proxyProps, portal, ContentRenderer }} /> | ||
) | ||
} | ||
})(0, children) | ||
// | ||
// | ||
const ContentProxy = ({ content, proxyProps, portal, ContentRenderer }) => { | ||
const [id] = useState(createID) | ||
const markup = <ContentRenderer {...proxyProps} key={id}>{content}</ContentRenderer> | ||
portal.data.set(id, markup) | ||
useLayoutEffect(() => { | ||
portal.data.set(id, markup) | ||
portal.dataChanged() | ||
}) | ||
useEffect(() => () => { | ||
portal.data.delete(id) | ||
portal.dataChanged() | ||
// eslint-disable-next-line | ||
}, []) | ||
return null | ||
} | ||
// | ||
// | ||
const usePortalRender = (portal, children = defaultRender) => { | ||
const [id] = useState(createID) | ||
const [, subscription] = useState() | ||
portal.subscriptions.set(id, subscription) | ||
// eslint-disable-next-line | ||
useLayoutEffect(() => { | ||
portal.subscriptions.set(id, subscription) | ||
}) | ||
useEffect(() => () => { | ||
portal.subscriptions.delete(id) | ||
// eslint-disable-next-line | ||
}, []) | ||
return children([...portal.data.values()]) | ||
} | ||
// | ||
// | ||
const createCustomPortal = () => new CustomPortal() | ||
@@ -73,3 +146,5 @@ | ||
dataChanged() { | ||
[...this.subscriptions.values()].forEach(subscription => subscription({})) | ||
for (const subscription of this.subscriptions.values()) { | ||
subscription({}) | ||
} | ||
} | ||
@@ -76,0 +151,0 @@ } |
@@ -1,1 +0,1 @@ | ||
export { createPortal } from './createPortal' | ||
export { createPortal, createGlobalPortal } from './createPortal' |
@@ -1,24 +0,48 @@ | ||
import React, { useState } from 'react' | ||
import { createPortal } from './components' | ||
import React, { useState, createContext, useContext } from "react" | ||
import { createPortal, createGlobalPortal } from "./components" | ||
export const Portal = createPortal('OnOff') | ||
const Portal = createPortal({ displayName: "OnOff" }) | ||
const GlobalPortal = createGlobalPortal({ displayName: "OnOff" }) | ||
const ExampleContext = createContext() | ||
export const Switch = () => { | ||
const [state, setState] = useState(false) | ||
return <> | ||
<input id="switch" type="checkbox" value={state} onClick={() => setState(!state)} /> | ||
<label htmlFor="switch">switch</label> | ||
<Portal.Content>{state ? "on" : "off"}</Portal.Content> | ||
</> | ||
const Label = ({ text }) => { | ||
const context = useContext(ExampleContext) | ||
return <>{text}<br/>({context})</> | ||
} | ||
const Switch = () => { | ||
const [state, setState] = useState(false) | ||
return <> | ||
<input id="switch" type="checkbox" value={state} onClick={() => setState(!state)} /> | ||
<label htmlFor="switch">switch</label> | ||
<Portal.Content preserveContexts={[ExampleContext]}> | ||
<Label text={state ? "on" : "off"} /> | ||
</Portal.Content> | ||
</> | ||
} | ||
const SwitchGlobal = () => { | ||
const [state, setState] = useState(false) | ||
return <> | ||
<input id="global-switch" type="checkbox" value={state} onClick={() => setState(!state)} /> | ||
<label htmlFor="global-switch">global switch</label> | ||
<GlobalPortal.Content>{state ? "global-on" : "global-off"}</GlobalPortal.Content> | ||
</> | ||
} | ||
const ExampleApp = () => { | ||
return ( | ||
<Portal.Root> | ||
<div style={{padding: 20}} ><Switch /></div> | ||
<div style={{padding: 20, backgroundColor: "tomato"}} ><Portal.Render /></div> | ||
</Portal.Root> | ||
) | ||
return <> | ||
<Portal.Root> | ||
<ExampleContext.Provider value="context value is right"> | ||
<div style={{padding: 20}} ><Switch /></div> | ||
</ExampleContext.Provider> | ||
<ExampleContext.Provider value="context value is wrong"> | ||
<div style={{padding: 20, backgroundColor: "tomato"}} ><Portal.Render /></div> | ||
</ExampleContext.Provider> | ||
</Portal.Root> | ||
<div style={{padding: 20}} ><SwitchGlobal /></div> | ||
<div style={{padding: 20, backgroundColor: "tomato"}} ><GlobalPortal.Render /></div> | ||
</> | ||
} | ||
export default ExampleApp |
@@ -1,6 +0,6 @@ | ||
import React from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
import './index.css'; | ||
import ExampleApp from './ExampleApp'; | ||
import React from "react" | ||
import ReactDOM from "react-dom" | ||
import "./index.css" | ||
import ExampleApp from "./ExampleApp" | ||
ReactDOM.render(<ExampleApp />, document.getElementById('root')); | ||
ReactDOM.render(<ExampleApp />, document.getElementById("root")) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
14502
341
63
1
1