Skip to content

Commit

Permalink
Merge pull request #28 from renatasva/add-attributes-option
Browse files Browse the repository at this point in the history
  • Loading branch information
pimterry committed Jun 18, 2021
2 parents 7cba832 + e3cc21b commit caef79f
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 6 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,23 @@ Normally in `ComponentA`/`ComponentB` examples like the above, switching from `C

How does it work under the hood?

### `portals.createHtmlPortalNode`
### `portals.createHtmlPortalNode([options])`

This creates a detached DOM node, with a little extra functionality attached to allow transmitting props later on.

This node will contain your portal contents later, within a `<div>`, and will eventually be attached in the target location. Its plain DOM node is available at `.element`, so you can mutate that to set any required props (e.g. `className`) with the standard DOM APIs.
This node will contain your portal contents later, within a `<div>`, and will eventually be attached in the target location.

### `portals.createSvgPortalNode`
An optional options object parameter can be passed to configure the node. The only supported option is `attributes`: this can be used to set the HTML attributes (style, class, etc.) of the intermediary, like so:

```javascript
const portalNode = portals.createHtmlPortalNode({
attributes: { id: "div-1", style: "background-color: #aaf; width: 100px;" }
});
```

The div's DOM node is also available at `.element`, so you can mutate that directly with the standard DOM APIs if preferred.

### `portals.createSvgPortalNode([options])`

This creates a detached SVG DOM node. It works identically to the node from `createHtmlPortalNode`, except it will work with SVG elements. Content is placed within a `<g>` instead of a `<div>`.

Expand Down
19 changes: 16 additions & 3 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ const ELEMENT_TYPE_SVG = 'svg';

type ANY_ELEMENT_TYPE = typeof ELEMENT_TYPE_HTML | typeof ELEMENT_TYPE_SVG;

type Options = {
attributes: { [key: string]: string };
};

// ReactDOM can handle several different namespaces, but they're not exported publicly
// https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/shared/DOMNamespaces.js#L8-L10
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
Expand Down Expand Up @@ -50,7 +54,10 @@ const validateElementType = (domElement: Element, elementType: ANY_ELEMENT_TYPE)
};

// This is the internal implementation: the public entry points set elementType to an appropriate value
const createPortalNode = <C extends Component<any>>(elementType: ANY_ELEMENT_TYPE): AnyPortalNode<C> => {
const createPortalNode = <C extends Component<any>>(
elementType: ANY_ELEMENT_TYPE,
options?: Options
): AnyPortalNode<C> => {
let initialProps = {} as ComponentProps<C>;

let parent: Node | undefined;
Expand All @@ -65,6 +72,12 @@ const createPortalNode = <C extends Component<any>>(elementType: ANY_ELEMENT_TYP
throw new Error(`Invalid element type "${elementType}" for createPortalNode: must be "html" or "svg".`);
}

if (options && typeof options === "object") {
for (const [key, value] of Object.entries(options.attributes)) {
element.setAttribute(key, value);
}
}

const portalNode: AnyPortalNode<C> = {
element,
elementType,
Expand Down Expand Up @@ -223,9 +236,9 @@ class OutPortal<C extends Component<any>> extends React.PureComponent<OutPortalP
}

const createHtmlPortalNode = createPortalNode.bind(null, ELEMENT_TYPE_HTML) as
<C extends Component<any> = Component<any>>() => HtmlPortalNode<C>;
<C extends Component<any> = Component<any>>(options?: Options) => HtmlPortalNode<C>;
const createSvgPortalNode = createPortalNode.bind(null, ELEMENT_TYPE_SVG) as
<C extends Component<any> = Component<any>>() => SvgPortalNode<C>;
<C extends Component<any> = Component<any>>(options?: Options) => SvgPortalNode<C>;

export {
createHtmlPortalNode,
Expand Down
29 changes: 29 additions & 0 deletions stories/html.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,35 @@ storiesOf('Portals', module)

return <ChangeLayoutWithoutUnmounting />;
})
.add('can pass attributes option to createHtmlPortalNode', () => {
return React.createElement(() => {
const [hasAttrOption, setHasAttrOption] = React.useState(false);

const portalNode = createHtmlPortalNode( hasAttrOption ? {
attributes: { id: "div-id-1", style: "background-color: #aaf; width: 204px;" }
} : null);

return <div>
<button onClick={() => setHasAttrOption(!hasAttrOption)}>
Click to pass attributes option to the intermediary div
</button>

<hr/>

<InPortal node={portalNode}>
<div style={{width: '200px', height: '50px', border: '2px solid purple'}} />
</InPortal>

<OutPortal node={portalNode} />

<br/>
<br/>
<br/>

<text>{!hasAttrOption ? `const portalNode = createHtmlPortalNode();` : `const portalNode = createHtmlPortalNode({ attributes: { id: "div-id-1", style: "background-color: #aaf; width: 204px;" } });`}</text>
</div>
});
})
.add('Example from README', () => {
const MyExpensiveComponent = () => 'expensive!';

Expand Down
28 changes: 28 additions & 0 deletions stories/svg.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,32 @@ storiesOf('SVG Portals', module)
</svg>
</div>
})
})
.add('can pass attributes option to createSvgPortalNode', () => {
return React.createElement(() => {
const [hasAttrOption, setHasAttrOption] = React.useState(false);

const portalNode = createSvgPortalNode(hasAttrOption ? {attributes: { stroke: 'blue' }} : null);
return <div>
<button onClick={() => setHasAttrOption(!hasAttrOption)}>
Click to pass attributes option to the intermediary svg
</button>

<hr/>

<svg>
<InPortal node={portalNode}>
<circle cx="50" cy="50" r="40" fill="lightblue" />
</InPortal>

<svg x={30} y={10}>
<OutPortal node={portalNode} />
</svg>
</svg>

<br/>

<text>{!hasAttrOption ? `const portalNode = createSvgPortalNode();` : `const portalNode = createSvgPortalNode({ attributes: { stroke: "blue" } });`}</text>
</div>
});
});

0 comments on commit caef79f

Please sign in to comment.