Draft.js adding hyperlink using applyEntity doesn't seem to work - reactjs

I've been working on this problem for a while, and I hope it's not a bug.
I'm testing a text editor using Draft.js, and I'd simply like my users to add a hyperlink to their articles, so I create a function for that to happen by modifying the editor state's content.
const addLlink = (e) => {
e.preventDefault();
const contentState = editorState.getCurrentContent();
const contentStateWithEntity = contentState.createEntity(
'LINK', 'MUTABLE', {url: 'https://www.amazon.com'} // link for testing only
);
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
const contentStateWithLink = Modifier.applyEntity(
contentStateWithEntity,
editorState.getSelection(),
entityKey
);
// tried with and without styling
const styledContentStateWithLink = Modifier.applyInlineStyle(
contentStateWithLink,
editorState.getSelection(),
'HYPERLINK'
);
const newState = EditorState.set(editorState, {
currentContent: styledContentStateWithLink
});
setEditorState(newState);
// Also tried: setEditorState(RichUtils.toggleLink(newState, newState.getSelection(), entityKey));
}
When I trigger it, I just use an Evergreen-ui button:
<Button onMouseDown={addLlink} appearance="minimal">Link</Button>
The styling I implement using the Modifier object works, but I can't seem to get the link to actually work. It should be noted that the link plugin as a package, which works great, but that's only for detecting typed out/pasted URLs (not embedded into text).
Does anyone have an actual working example, or suggestions of what I may be doing wrong, for links that use React functional programming?

It turns out that I needed to add a Decorator in order for the entity to be detected. I placed this code above/outside of my Editor component:
const findLinkEntities = (contentBlock, callback, contentState) => {
contentBlock.findEntityRanges(
(character) => {
const entityKey = character.getEntity();
return (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === 'LINK'
);
},
callback
);
}
const Link = (props) => {
const {url} = props.contentState.getEntity(props.entityKey).getData();
return (
<a href={url} >
{props.children}
</a>
);
};
const strategyDecorator = new CompositeDecorator([
{
strategy: findLinkEntities,
component: Link,
},
]);
Basically, it detects link entities and converts them into elements when you set the new content using EditorState:
const newState = EditorState.set(editorState, {
currentContent: styledContentStateWithLink,
decorator: strategyDecorator
});

According to the official example you need to add a decorator to find the entities and apply the Link component
const decorator = new CompositeDecorator([
{
strategy: findLinkEntities,
component: Link,
},
]);
And as you're using the linkify plugin you have to pass the decorator to plugins editor
import Editor from "draft-js-plugins-editor";
import createLinkifyPlugin from "draft-js-linkify-plugin";
import "draft-js-linkify-plugin/lib/plugin.css";
...
<Editor
decorators={[decorator]}
editorState={editorState}
onChange={setEditorState}
plugins={[linkifyPlugin]}
/>
...
you can check out the working example here https://codesandbox.io/s/test-selection-decorator-draft-js-link-0lblg?file=/src/ExtendedEditor.js

Related

How do I parse the HTML from the Lexical editorState without an extra Lexical editor

I have a posts system and when someone submits a post I am saving the editorState as a JSON string in a Postgres database.
Then, when I need to show the HTML, I am using a custom hook which loads an auxiliary editor just to obtain the HTML.
Is this the proper way? Looks to me a bit overengineering 🤔
I don't want to load other editor, and render it too just to obtain the HTML.
Any idea of how to improve this or suggestion for a different approach? 😄
The render post component:
export const PostDetails = () => {
const postSlug = useParam("postSlug", "string")
const [postDetails] = useQuery(getPostPageDetails, { slug: postSlug })
const { html, AuxEditor } = useGetHtmlFromState(postDetails.content as unknown as EditorState)
return (
<>
<AuxEditor />
<Text dangerouslySetInnerHTML={{ __html: html }} />
</>
)
}
The hook to get the HTML useGetHtmlFromState (it uses the same config as the input)
export const useGetHtmlFromState = (state: EditorState) => {
const [html, setHtml] = useState("")
function MyCustomStateToHtmlPlugin() {
const [editor] = useLexicalComposerContext()
editor.update(() => {
const html = $generateHtmlFromNodes(editor, null)
setHtml(html)
})
return null
}
const AuxEditor = () => {
return (
<LexicalComposer
initialConfig={{
namespace: "MyEditor",
onError: console.error,
editorState: state,
theme: exampleTheme,
nodes: [
HeadingNode,
ListNode,
ListItemNode,
QuoteNode,
CodeNode,
CodeHighlightNode,
TableNode,
TableCellNode,
TableRowNode,
AutoLinkNode,
LinkNode,
],
}}
>
<MyCustomStateToHtmlPlugin />
</LexicalComposer>
)
}
return { html, AuxEditor }
}
I think what you have is a creative way to turn SerializedEditorState into HTML. You could experiment with #lexical/headless, but it's essentially the same approach. Alternatively, there are a couple of other ways to solve the underlying problem of displaying saved state.
(1) You can generate the HTML up front and save it to the DB alongside the lexical state. It's a bit duplicative, but it works for the "write once, read many times" use case you've described.
(2) You can use a read-only lexical editor to display the saved state instead of converting it into HTML. You can configure the editor with a different theme if you need more control over the styling of specific elements.
I hope that helps!

Like Button with Local Storage in ReactJS

I developed a Simple React Application that read an external API and now I'm trying to develop a Like Button from each item. I read a lot about localStorage and persistence, but I don't know where I'm doing wrong. Could someone help me?
1-First, the component where I put item as props. This item bring me the name of each character
<LikeButtonTest items={item.name} />
2-Then, inside component:
import React, { useState, useEffect } from 'react';
import './style.css';
const LikeButtonTest = ({items}) => {
const [isLike, setIsLike] = useState(
JSON.parse(localStorage.getItem('data', items))
);
useEffect(() => {
localStorage.setItem('data', JSON.stringify(items));
}, [isLike]);
const toggleLike = () => {
setIsLike(!isLike);
}
return(
<div>
<button
onClick={toggleLike}
className={"bt-like like-button " + (isLike ? "liked" : "")
}>
</button>
</div>
);
};
export default LikeButtonTest;
My thoughts are:
First, I receive 'items' as props
Then, I create a localStorage called 'data' and set in a variable 'isLike'
So, I make a button where I add a class that checks if is liked or not and I created a toggle that changes the state
The problem is: I need to store the names in an array after click. For now, my app is generating this:
App item view
localStorage with name of character
You're approach is almost there. The ideal case here is to define your like function in the parent component of the like button and pass the function to the button. See the example below.
const ITEMS = ['item1', 'item2']
const WrapperComponent = () => {
const likes = JSON.parse(localStorage.getItem('likes'))
const handleLike = item => {
// you have the item name here, do whatever you want with it.
const existingLikes = likes
localStorage.setItem('likes', JSON.stringify(existingLikes.push(item)))
}
return (<>
{ITEMS.map(item => <ItemComponent item={item} onLike={handleLike} liked={likes.includes(item)} />)}
</>)
}
const ItemComponent = ({ item, onLike, liked }) => {
return (
<button
onClick={() => onLike(item)}
className={liked ? 'liked' : 'not-liked'}
}>
{item}
</button>
)
}
Hope that helps!
note: not tested, but pretty standard stuff

React captcha v3 is slowing my page significantly

I'm using react-google-recaptcha-v3 in my React project. I have used defer prop attribute but it is still slow.
How can I improve the speed with the use of this plugin? It only affects significantly on mobile view. Is there a way to not load the form portion during page load? Or any better solution for this issue?
<GoogleReCaptchaProvider
reCaptchaKey="KEY_HERE"
scriptProps={{
defer: true,
}}
>
//Form here
</GoogleRecaptchaProvider>
I had this same issue and I managed to fix it by wrapping the provider on my custom GoogleRecaptchaWrapper component, so the component will only inject the recaptcha.js after some sort of interaction happens in the page (and not on page load). Here is the code:
const RECAPTCHA_EVENT_LIST = ["mousemove", "mousedown", "keydown", "touchstart"]
export const GoogleRecaptchaWrapper = props => {
const { children } = props
const [recaptchaProps] = useState({ defer: true })
const [recaptchaNeeded, setRecaptchaNeeded] = useState(false)
const initRecaptcha = e => {
RECAPTCHA_EVENT_LIST.forEach(ev =>
document.removeEventListener(ev, initRecaptcha)
)
setRecaptchaNeeded(true)
}
ifBrowser(() => {
RECAPTCHA_EVENT_LIST.forEach(ev =>
document.addEventListener(ev, initRecaptcha)
)
})
const wrapperProps = recaptchaNeeded
? {
reCaptchaKey: AppConfig.recaptchaSiteKey,
scriptProps: recaptchaProps
}
: {}
return (
<GoogleReCaptchaProvider {...wrapperProps}>
{children}
</GoogleReCaptchaProvider>
)
}
As I say, this only injects the JS when recaptchaNeeded is true. I hope it helps!

Lazy Hydrate + Code Splitting on NextJS app

I know how to do Lazy Hydration and I know how to do Code Splitting, but how can I make the splitted chunck download only when the component is hydrating?
My code looks like this
import React from 'react';
import dynamic from 'next/dynamic';
import ReactLazyHydrate from 'react-lazy-hydration';
const MyComponent = dynamic(() => import('components/my-component').then((mod) => mod.MyComponent));
export const PageComponent = () => {
return (
...
<ReactLazyHydrate whenVisible>
<MyComponent/>
</ReactLazyHydrate>
...
);
};
MyComponent is rendered below the fold, which means that it is only gonna hydrate when the user scrolls. The problem is that the JS chunck for MyComponent will be downloaded right away when the page loads.
I was able to hack it by using the dynamic import only on client but this makes the component disappear for a second when it hydrates, because the html rendered on server will not be used by react. It will recreate the DOM element and it will be empty until the JS chunck loads.
When the element disappear for a sec it increases the page CLS and that's the main reason why I can not use this hack.
Here is the code for this hack
const MyComponent = typeof window === 'undefined'
? require('components/my-component').MyComponent
: dynamic(() => import('components/my-component').then((mod) => mod.MyComponent));
Note that I want to render the component's HTML on the server render. That't why I don't want to Lazy Load it. I want to Lazy Hydrate so I can have the component's HTML rendered on server but only download
and execute it's JS when it is visible.
Update:
In document:
// stops preloading of code-split chunks
class LazyHead extends Head {
getDynamicChunks(files) {
const dynamicScripts = super.getDynamicChunks(files);
try {
// get chunk manifest from loadable
const loadableManifest = __non_webpack_require__(
'../../react-loadable-manifest.json',
);
// search and filter modules based on marker ID
const chunksToExclude = Object.values(loadableManifest).filter(
manifestModule => manifestModule?.id?.startsWith?.('lazy') || false,
);
const excludeMap = chunksToExclude?.reduce?.((acc, chunks) => {
if (chunks.files) {
acc.push(...chunks.files);
}
return acc;
}, []);
const filteredChunks = dynamicScripts?.filter?.(
script => !excludeMap?.includes(script?.key),
);
return filteredChunks;
} catch (e) {
// if it fails, return the dynamic scripts that were originally sent in
return dynamicScripts;
}
}
}
const backupScript = NextScript.getInlineScriptSource;
NextScript.getInlineScriptSource = (props) => {
// dont let next load all dynamic IDS, let webpack manage it
if (props?.__NEXT_DATA__?.dynamicIds) {
const filteredDynamicModuleIds = props?.__NEXT_DATA__?.dynamicIds?.filter?.(
moduleID => !moduleID?.startsWith?.('lazy'),
);
if (filteredDynamicModuleIds) {
// mutate dynamicIds from next data
props.__NEXT_DATA__.dynamicIds = filteredDynamicModuleIds;
}
}
return backupScript(props);
};
in next config
const mapModuleIds = fn => (compiler) => {
const { context } = compiler.options;
compiler.hooks.compilation.tap('ChangeModuleIdsPlugin', (compilation) => {
compilation.hooks.beforeModuleIds.tap('ChangeModuleIdsPlugin', (modules) => {
const { chunkGraph } = compilation;
for (const module of modules) {
if (module.libIdent) {
const origId = module.libIdent({ context });
// eslint-disable-next-line
if (!origId) continue;
const namedModuleId = fn(origId, module);
if (namedModuleId) {
chunkGraph.setModuleId(module, namedModuleId);
}
}
}
});
});
};
const withNamedLazyChunks = (nextConfig = {}) => Object.assign({}, nextConfig, {
webpack: (config, options) => {
config.plugins.push(
mapModuleIds((id, module) => {
if (
id.includes('/global-brand-statement.js')
|| id.includes('signposting/signposting.js')
|| id.includes('reviews-container/index.js')
|| id.includes('why-we-made-this/why-we-made-this.js')
) {
return `lazy-${module.debugId}`;
}
return false;
}),
);
if (typeof nextConfig.webpack === 'function') {
return nextConfig.webpack(config, options);
}
return config;
},
});
In file, using next/dynamic
<LazyHydrate whenVisible style={null} className="col-xs-12">
<GlobalBrandStatement data={globalBrandData} />
</LazyHydrate>
Not sure if this is what you’re after, but I use lazy hydration mixed with webpack plugin and custom next head to preserve the html but strip out below the fold dynamic imported scripts. So I only download the JS and hydrate the component just before the user scrolls into view. Regardless of it the component was used during render - I don’t need the runtime unless a user is going to see it.
Currently in production and has reduced initial page load by 50%. No impact to SEO
Get me on twitter #scriptedAlchemy if you need the implementation, I’ve not yet written a post about it - but I can tell you it’s totally possible to achieve this “download as you scroll” design with very little effort.
import { useState } from "react";
import dynamic from "next/dynamic";
const MyComponent = dynamic(() => import("components/my-component"));
export const PageComponent = () => {
const [downloadComp, setDownloadComp] = useState(false);
return (
<>
<div className="some-class-name">
<button onClick={() => setDownloadComp(true)}>
Download the component
</button>
{downloadComp && <MyComponent />}
</div>
</>
);
};
The above code will download the once you hit the button. If you want it to download if your scroll to position in that case you can use something like react-intersection-observer to set the setDownloadComp. I don't have experience using react-lazy-hydration but I have been using react-intersection-observer and nextjs dynamic import to lazy load components that depends on user scroll.
I have made a library to make this thing simple. And you can benefit with:
Fully HTML page render (Better for SEO)
Only load JS and Hydrate when needed (Better for PageSpeed)
How to use it
import lazyHydrate from 'next-lazy-hydrate';
// Lazy hydrate when scroll into view
const WhyUs = lazyHydrate(() => import('../components/whyus'));
// Lazy hydrate when users hover into the view
const Footer = lazyHydrate(
() => import('../components/footer', { on: ['hover'] })
);
const HomePage = () => {
return (
<div>
<AboveTheFoldComponent />
{/* ----The Fold---- */}
<WhyUs />
<Footer />
</div>
);
};
Read more: https://github.com/thanhlmm/next-lazy-hydrate

React hooks. Update component passed as param via onClick after one of it's prop was changed

Hi guys) I have a strange question may be, but I'm at a dead end.
I have my own custom hook.
const useModal = (Content?: ReactNode, options?: ModalOptions) => {
const { isOpen, close: contextClose, open: contextOpen, setContent } = useContext(
ModalContext,
)
const [customOpenContent, setCustomOpenContent] = useState<ReactNode>()
const showModal = useCallback(
(customContent?: ReactNode) => {
if (!isNil(customContent)) {
setCustomOpenContent(customContent)
contextOpen(customContent, options)
} else contextOpen(Content, options)
},
[contextOpen, Content, options],
)
const hideModal = useCallback(() => {
contextClose()
}, [contextClose])
return { isOpen, close: hideModal, open: showModal, setContent }
}
It is quite simple.
Also i have component which uses this hook
const App: React.FC = () => {
const [loading, setLoading] = useState(false)
const { open } = useModal(null, { deps: [loading] })
useEffect(() => {
setTimeout(() => {
setLoading(true)
}, 10000)
})
const buttonCallback = useCallback(() => {
open(<Button disabled={!loading}>Loading: {loading.toString()}</Button>)
}, [loading, open])
return (
<Page title="App">
<Button onClick={buttonCallback}>Open Modal</Button>
</Page>
)
}
Main problem is - Button didn't became enabled because useModal hook doesn't know anything about changes.
May be you have an idea how to update this component while it's props are updated? And how to do it handsomely ))
Context isn't the best solution to this problem. What you want is a Portal instead. Portals are React's solution to rendering outside of the current React component hierarchy. How to use React Portal? is a basic example, but as you can see, just going with the base React.Portal just gives you the location to render.
Here's a library that does a lot of the heavy lifting for you: https://github.com/wellyshen/react-cool-portal. It has typescript definitions and provides an easy API to work with.
Here's your example using react-cool-portal.
import usePortal from "react-cool-portal";
const App = () => {
const [loading, setLoading] = useState(false);
const { Portal, isShow, toggle } = usePortal({ defaultShow: false });
useEffect(() => {
setTimeout(() => {
setLoading(true);
}, 10000);
});
const buttonCallback = useCallback(() => {
toggle();
}, [toggle]);
return (
<div title="App" style={{ backgroundColor: "hotpink" }}>
<button onClick={buttonCallback}>
{isShow ? "Close" : "Open"} Modal
</button>
<Portal>
<button disabled={!loading}>Loading: {loading.toString()}</button>
</Portal>
<div>{loading.toString()}</div>
</div>
);
};
Basic CodeSandbox Example
There are more detailed ones within the react-cool-portal documentation.
For more detail of the issues with the Context solution you were trying, is that React Elements are just a javascript object. React then uses the object, it's location in the tree, and it's key to determine if they are the same element. React doesn't actually care or notice where you create the object, only it's location in the tree when it is rendered.
The disconnect in your solution is that when you pass the element to the open function in buttonCallback, the element is created at that point. It's a javascript object that then is set as the content in your context. At that point, the object is set and won't change until you called open again. If you set up your component to call open every time the relevant state changes, you could get it working that way. But as I mentioned earlier, context wasn't built for rendering components outside of the current component; hence why some really weird workarounds would be required to get it working.

Resources