Dynamic tag name in Solid JSX - solid-js

I would like to set JSX tag names dynamically in SolidJS. I come from React where it is fairly simple to do:
/* Working ReactJS Code: */
export default MyWrapper = ({ children, ..attributes }) => {
const Element = "div";
return (
<Element {...attributes}>
{children}
</Element>
)
}
but when I try to do the same thing in SolidJS, I get the following error:
/* Console output when trying to do the same in SolidJS: */
dev.js:530 Uncaught (in promise) TypeError: Comp is not a function
at dev.js:530:12
at untrack (dev.js:436:12)
at Object.fn (dev.js:526:37)
at runComputation (dev.js:706:22)
at updateComputation (dev.js:691:3)
at devComponent (dev.js:537:3)
at createComponent (dev.js:1236:10)
at get children [as children] (Input.jsx:38:5)
at _Hot$$Label (Input.jsx:7:24)
at #solid-refresh:10:42
I would like to know if I miss something here, or whether it is possible to achieve this in SolidJS in any other way.

Solid has a <Dynamic> helper component for that use.
import {Dynamic} from "solid-js/web";
<Dynamic component="div" {...attributes}>
{props.children}
</Dynamic>

Here is an alternative implementation covering simple cases like strings and nodes although you can extend it to cover any JSX element:
import { Component, JSXElement} from 'solid-js';
import { render, } from 'solid-js/web';
const Dynamic: Component<{ tag: string, children: string | Node }> = (props) => {
const el = document.createElement(props.tag);
createEffect(() => {
if(typeof props.children === 'string') {
el.innerText = String(props.children);
} else if (props.children instanceof Node){
el.appendChild(props.children);
} else {
throw Error('Not implemented');
}
});
return el;
};
const App = () => {
return (
<div>
<Dynamic tag="h2">This is an H2!</Dynamic>
<Dynamic tag="p">This is a paragraph!</Dynamic>
<Dynamic tag="div"><div>Some div element rendering another div</div></Dynamic>
</div>
)
}
render(App, document.body);
This works because Solid components are compiled into native DOM elements, however since we do not escape the output, it is dangerous to render any children directly, given that you have no control over the content.
This alternative comes handy when you need to render rich text from a content editable or a textarea, text that includes tags like em, strong etc. Just make sure you use innerHTML attribute instead of innerText.

Related

How to convert draft-js mentions into html?

I am using "#draft-js-plugins/mention" plugin together with rich text functionality provided by draft-js.
Also, I'm using "draft-js-export-html" library to convert the editor state into html.
However, this library converts only rich text styling to html, mentions are converted as plain text.
How can I convert mentions into html tags, something like anchor tags ?
Do I have to manually manipulate JSON editorState after getting it through the convertToRaw function?
the "draft-js-export-html" library has a second argument for its stateToHtml function where options can be provided, such as entityStyleFn:
const options = {
entityStyleFn: (entity) => {
const entityType = entity.get("type").toLowerCase();
if (entityType === "mention") {
const { mention } = entity.get("data");
return {
element: "a",
attributes: {
userid: mention.userId,
},
};
}
},
};
let html = stateToHTML(contentState, options);
https://github.com/sstur/draft-js-utils/tree/master/packages/draft-js-export-html#entitystylefn
Or, draft-js' entire state can be sent to backend as JSON with the convertToRaw function, then, after getting back the data from the backend, that state can be fed back to draft-js and #draft-js-plugins/mention's mentionComponent function can be used for rendering custom html tags.
const mentionPlugin = createMentionPlugin({
mentionComponent(mentionProps) {
return (
<span
className={mentionProps.className}
// eslint-disable-next-line no-alert
onClick={() => alert('Clicked on the Mention!')}
>
{mentionProps.children}
</span>
);
},
});

How can I make StencilJS component to render without component tag itself with TSX?

While I understand this is probably a terrible practice, I need to build StencilJS component such that inside render(), I don't want to render component tag itself due to already existing style guide and it expect DOM to be constructed in certain way. Here is what I'm trying to achieve - component code (from HTML or within another component):
<tab-header-list>
<tab-header label="tab 1"></tab-header>
<tab-header label="tab 2"></tab-header>
</tab-header-list>
when rendered, I want generated DOM to be something like:
<tab-header-list>
<ul>
<li>tab 1</li>
<li>tab 2</li>
</ul>
</tab-header-list>
so inside tab-header-list render() function, I'm doing
return (
<ul>
<slot/>
</ul>
);
and I can do this inside tab-header render() function
#Element() el: HTMLElement;
#Prop() label: string;
render() {
this.el.outerHTML = `<li>${this.label}</li>`;
}
to get what I want but how can I do this with TSX? (for simplicity sake, above code is really simple but what I really need to build is lot more complicated li tag with events etc so I would like to use TSX)
Tried to store DOM to variable but I'm not sure how I can assign it as this.el (outerHTML seem to be only way I can come up with, but I feel there must be better way)
#Element() el: HTMLElement;
#Prop() label: string;
render() {
var tabheaderDOM = (<li>{this.label}</li>);
// how can I assign above DOM to this.el somehow?
//this.el.outerHTML = ?
}
I appreciate any help I can get - thanks in advance for your time!
Unfortunately, you can't use custom elements without tags, but there is a workaround for it:
You can use Host element as reference to the result tag.
render () {
return (
<Host>....</Host>
)
}
Then in your stylesheet you can set the display property for it:
:host {
display: contents;
}
display: contents causes an element's children to appear as if they were direct children of the element's parent, ignoring the element itself
Beware: it doesn't work in IE, opera mini... https://caniuse.com/#feat=css-display-contents
UPD:
If you are not using the shadowDOM then you need to replace :host by the tag name like:
tab-header {
display: contents;
}
Functional components might be able to help you achieve this. They are merely syntactic sugar for a function that returns a TSX element, so they are completely different to normal Stencil components. The main difference is that they don't compile to web components, and therefore only work within TSX. But they also don't result in an extra DOM node because they simply return the template that the function returns.
Let's take your example:
#Element() el: HTMLElement;
#Prop() label: string;
render() {
this.el.outerHTML = `<li>${this.label}</li>`;
}
you could write it as a functional component:
import { FunctionalComponent } from '#stencil/core';
interface ListItemProps {
label: string;
}
export const ListItem: FunctionalComponent<ListItemProps> = ({ label }) => (
<li>{label}</li>
);
and then you can use it like
import { ListItem } from './ListItem';
#Component({ tag: 'my-comp' })
export class MyComp {
render() {
return (
<ul>
<ListItem label="tab 1" />
<ListItem label="tab 2" />
</ul>
);
}
}
Which will render as
<ul>
<li>tab 1</li>
<li>tab 2</li>
</ul>
Instead of a label prop you could also write your functional component to accept the label as a child instead:
export const ListItem: FunctionalComponent = (_, children) => (
<li>{children}</li>
);
and use it like
<ListItem>tab 1</ListItem>
BTW Host is actually a functional component. To find out more about functional components (and there limitations), see https://stenciljs.com/docs/functional-components.

Extract function to standalone custom React component in react-leaflet

My primary goal is to call fitBounds whenever a FeatureGroup is rendered in react-leaflet on initial load.
This renders correctly -
<Map>
<LayersControl>
{getLayers(groups)}
</LayersControl>
</Map>
function getLayers(featureGroups: MyFeatureGroup[]){
const showOnLoad = true;
return featureGroups.map((group: MyFeatureGroup) => {
const groupRef = createRef<FeatureGroup>();
const { id, name, } = group;
return (
<LayersControl.Overlay checked={showOnLoad} key={id} name={name}>
<FeatureGroup ref={groupRef}>
<Layer {...group} />
</FeatureGroup>
</LayersControl.Overlay>
);
});
}
However, because it is using a function instead of React component, I don't have access to using React hooks.
The alternative that I tried does not work, even though it is the same code wrapped in a React component -
...same as above...
return featureGroups.map((group: MyFeatureGroup) => (
<ControlledGroup {...group} showOnLoad={showOnLoad} /> ///----- > ADDED THIS HERE
));
const ControlledGroup: React.FC<ControlledGroupProps> = (props) => {
const groupRef = createRef<FeatureGroup>();
const { map } = useLeaflet();
/// -----> map is correctly defined here - injecting to all of the layers (LayersControl, FeatureGroup) does not solve the problem
const { showOnLoad, ...group } = props;
useEffect(() => fitBounds(map, groupRef)); ///-----> Primary Goal of what I am trying to accomplish
return (
<LayersControl.Overlay
checked={showOnLoad}
key={group.id}
name={name}
>
<FeatureGroup ref={groupRef}>
<Layer map={map} {...group} />
</FeatureGroup>
</LayersControl.Overlay>
);
};
I am a bit stumped, since this is the same code. The getLayers function returns a ReactNode in both cases. However, when moving to a standalone ControlledGroup component, it throws an error on render -
addOverlay is not a function
I tried creating a custom class component for react-leaflet, but the difficulty that I ran into there is that createLeafletElement returns a Leaflet.Element, whereas I am simply looking to return a ReactNode. That is, all of these are valid react-leaflet components already.
My questions - why does one work and the other does not? What is the correct/recommended way to convert this function to a renderable stand-alone React component?
Further, if there is an alternative pattern to calling fitBounds, that would be helpful as well.
Any insight would be appreciated.
Since the Layers share an inheritance with Layers.Overlay, the solution to the render error is to keep the Layers together and move the feature group to a standalone component.
This works as expected and allows me to call useEffect on the groupRef -
function getLayers(groups: MyFeatureGroup[]){
return featureGroups.map((group: MyFeatureGroup) => {
const { id, name, } = group;
return (
///---> Keep the Overlay in the function here and extract just the FeatureGroup out
<LayersControl.Overlay checked={showOnLoad} key={id} name={name}>
<ControlledGroup {...group}></ControlledGroup>
</LayersControl.Overlay>
);
}

Can you manipulate the DOM directly while using Preactjs?

I am looking into Preact for my next project.
Since it has no virtual DOM I am wondering if it, like React, prefers you to let the framework manipulate the DOM instead of doing so yourself directly.
Would Preact bump heads with another library that manipulates the DOM such as SVGjs?
Preact is non-destructive when it comes to DOM updates. The official guide already explains how to integrate external DOM manipulations into the preact component:
If using class-based component:
import { h, Component } from 'preact';
class Example extends Component {
shouldComponentUpdate() {
// IMPORTANT: do not re-render via diff:
return false;
}
componentWillReceiveProps(nextProps) {
// you can do something with incoming props here if you need
}
componentDidMount() {
// now mounted, can freely modify the DOM:
const thing = document.createElement('maybe-a-custom-element');
this.base.appendChild(thing);
}
componentWillUnmount() {
// component is about to be removed from the DOM, perform any cleanup.
}
render() {
return <div class="example" />;
}
}
If using hooks, then use memo function from preact/compat:
import { h } from 'preact';
import { useEffect } from 'preact/hooks';
import { memo } from 'preact/compat';
function Example(props) {
const [node, setNode] = setState(null);
useEffect(() => {
const elm = document.createElement('maybe-a-custom-element');
setNode(elm);
// Now do anything with the elm.
// Append to body or <div class="example"></div>
}, []);
return <div class="example" />;
}
// Usage with default comparison function
const Memoed = memo(Example);
// Usage with custom comparison function
const Memoed2 = memo(Example, (prevProps, nextProps) => {
// Only re-render when `name' changes
return prevProps.name === nextProps.name;
});
Also, note that Preact's render() function always diffs DOM children inside of the container. So if your container contains DOM that was not rendered by Preact, Preact will try to diff it with the elements you pass it. - Thus the meaning non-destructive.

Popup always open in the marker

Is there any way the popup always stays open and does not need to click on it to open?
Expected behaviour
Actual behavior
With the introduction of react-leaflet version 2 which brings breaking changes in regard of creating custom components, it is no longer supported to extend components via inheritance (refer this thread for a more details)
In fact React official documentation also recommends to use composition instead of inheritance:
At Facebook, we use React in thousands of components, and we haven’t
found any use cases where we would recommend creating component
inheritance hierarchies.
Props and composition give you all the flexibility you need to
customize a component’s look and behavior in an explicit and safe way.
Remember that components may accept arbitrary props, including
primitive values, React elements, or functions.
The following example demonstrates how to extend marker component in order to keep popup open once the marker is displayed:
const MyMarker = props => {
const initMarker = ref => {
if (ref) {
ref.leafletElement.openPopup()
}
}
return <Marker ref={initMarker} {...props}/>
}
Explanation:
get access to native leaflet marker object (leafletElement) and open popup via Marker.openPopup method
Here is a demo
What you can do is to make your own Marker class from the react-leaflet marker, and then call the leaflet function openPopup() on the leaflet object after it has been mounted.
// Create your own class, extending from the Marker class.
class ExtendedMarker extends Marker {
componentDidMount() {
// Call the Marker class componentDidMount (to make sure everything behaves as normal)
super.componentDidMount();
// Access the marker element and open the popup.
this.leafletElement.openPopup();
}
}
This will make the popup open once the component has been mounted, and will also behave like a normal popup afterwards, ie. on close/open.
I threw together this fiddle that shows the same code together with the basic example.
You can use permanent tooltips, or React provides refs for this type of thing... you can do this:
https://jsfiddle.net/jrcoq72t/121/
const React = window.React
const { Map, TileLayer, Marker, Popup } = window.ReactLeaflet
class SimpleExample extends React.Component {
constructor () {
super()
this.state = {
lat: 51.505,
lng: -0.09,
zoom: 13
}
}
openPopup (marker) {
if (marker && marker.leafletElement) {
window.setTimeout(() => {
marker.leafletElement.openPopup()
})
}
}
render () {
const position = [this.state.lat, this.state.lng]
return (
<Map center={position} zoom={this.state.zoom}>
<TileLayer attribution='© OpenStreetMap contributors' url="http://{s}.tile.osm.org/{z}/{x}/{y}.png" />
<Marker position={position} ref={this.openPopup}>
<Popup>
<span>
A pretty CSS3 popup. <br /> Easily customizable.
</span>
</Popup>
</Marker>
</Map>
)
}
}
window.ReactDOM.render(<SimpleExample />, document.getElementById('container'))
References:
https://reactjs.org/docs/refs-and-the-dom.html
React.js - access to component methods
Auto open markers popup on react-leaflet map
The above no longer works with react-leaflet version 3. In your custom marker component, to get a reference to the leaflet element you should now use useRef() and then open up the popup in useEffect() once the component is mounted.
const MyMarker = (props) => {
const leafletRef = useRef();
useEffect(() => {
leafletRef.current.openPopup();
},[])
return <Marker ref={leafletRef} {...props} />
}
For the new react-leaflet v4 you will need to do some changes
const CustomMarker = ({ isActive, data, map }) => {
const [refReady, setRefReady] = useState(false);
let popupRef = useRef();
useEffect(() => {
if (refReady && isActive) {
map.openPopup(popupRef);
}
}, [isActive, refReady, map]);
return (
<Marker position={data.position}>
<Popup
ref={(r) => {
popupRef = r;
setRefReady(true);
}}
>
{data.title}
</Popup>
</Marker>
);
};
And then use MapContainer like this
const MapComponent = () => {
const [map, setMap] = useState(null);
return (
<div>
<MapContainer
ref={setMap}
center={[45.34416, 15.49005]}
zoom={15}
scrollWheelZoom={true}
>
<CustomMarker
isActive
map={map}
data={{
position: [45.34416, 15.49005],
title: "Text displayed in popup",
}}
/>
</MapContainer>
</div>
);
};
Edited
Notice that my old solution would try to open the popup every time it renders.
Found another solution that fit my needs to open it when the position changed. Notice that I look at position.lat, position.lng since it will think it always changes if you pass on the object.
And yes it is not perfect typescript but it is the best solution I could come up with.
const CustomMarker: React.FC<CustomMarkerProps> = ({ position, children }) => {
const map = useMap();
const markerRef = useRef(null);
useEffect(() => {
try {
// #ts-ignore
if (markerRef.current !== null && !markerRef.current.isPopupOpen()) {
// #ts-ignore
markerRef.current.openPopup();
}
} catch (error) {}
}, [position.lat, position.lng]);
return (
<Marker ref={markerRef} position={position}>
<Popup>{children}</Popup>
</Marker>
);
};
export default CustomMarker;
Old solution
Could not get it to work using useRef and useEffect. However, got it to work with calling openPopup() directly from the ref.
import { LatLngLiteral, Marker as LMarker } from "leaflet";
import React from "react";
import { Marker } from "react-leaflet";
export interface CustomMarkerProps {
position: LatLngLiteral;
open?: boolean;
}
const CustomMarker: React.FC<CustomMarkerProps> = ({
position,
open = false,
children,
}) => {
const initMarker = (ref: LMarker<any> | null) => {
if (ref && open) {
ref.openPopup();
}
};
return (
<Marker ref={initMarker} position={position}>
{children}
</Marker>
);
};
export default CustomMarker;