EditorJS in NextJS not able to load plugins - javascript

I am trying to get EditorJS working in NextJS. The editor loads fine without plugins, having the only paragraph as a block option. However, when I attempt to add plugins via tools prop console throws the following warning:
editor.js?9336:2 Module Tools was skipped because of TypeError: Cannot read property 'prepare' of undefined
When I click on the editor in the browser, it is throwing:
Uncaught TypeError: Cannot read property 'holder' of undefined
I have tested editor plugins in the normal React app, and they load fine. Meaning that the problem is in EditorJS and NextJS import and handling of plugins. I have tried to import editor and plugins in componentDidMount hook using require but had the same problem as with NextJS dynamic imports. Attempted to get component using React ref but found that currently NextJS has problems with getting components' refs, Tried suggested workaround but still had no result. The instance of the editor is not available until onChange is triggered, so plugins just cannot hook into the editor due to that 'prepare' property or the whole editor are being undefined until an event on editor has happened, but the editor outputs into the console that it is ready.
My component's code:
import React from "react";
import dynamic from "next/dynamic";
const EditorNoSSR = dynamic(() => import("react-editor-js"), { ssr: false });
const Embed = dynamic(() => import("#editorjs/embed"), { ssr: false });
class Editor extends React.Component {
state = {
editorContent: {
blocks: [
{
data: {
text: "Test text",
},
type: "paragraph",
},
],
},
};
constructor(props) {
super(props);
this.editorRef = React.createRef();
}
componentDidMount() {
console.log(this.editorRef.current);
console.log(this.editorInstance);
}
onEdit(api, newData) {
console.log(this.editorRef.current);
console.log(this.editorInstance);
this.setState({ editorContent: newData });
}
render() {
return (
<EditorNoSSR
data={this.state.editorContent}
onChange={(api, newData) => this.onEdit(api, newData)}
tools={{ embed: Embed }}
ref={(el) => {
this.editorRef = el;
}}
instanceRef={(instance) => (this.editorInstance = instance)}
/>
);
}
}
export default Editor;
Is there any solution to this problem? I know SSR is challenging with client side rendering of components that access DOM, but there was condition used that checked whether window object is undefined, however, it does not look like an issue in my situation.
UPDATE:
I have found a solution but it is rather not a NextJS way of solving the problem, however, it works. It does not require a react-editorjs and implemented as creation of EditorJS instance as with normal EditorJS.
class Editor extends React.Component {
constructor(props) {
super(props);
this.editor = null;
}
async componentDidMount() {
this.initEditor();
}
initEditor = () => {
const EditorJS = require("#editorjs/editorjs");
const Header = require("#editorjs/header");
const Embed = require("#editorjs/embed");
const Delimiter = require("#editorjs/delimiter");
const List = require("#editorjs/list");
const InlineCode = require("#editorjs/inline-code");
const Table = require("#editorjs/table");
const Quote = require("#editorjs/quote");
const Code = require("#editorjs/code");
const Marker = require("#editorjs/marker");
const Checklist = require("#editorjs/checklist");
let content = null;
if (this.props.data !== undefined) {
content = this.props.data;
}
this.editor = new EditorJS({
holder: "editorjs",
logLevel: "ERROR",
tools: {
header: Header,
embed: {
class: Embed,
config: {
services: {
youtube: true,
coub: true,
},
},
},
list: List,
inlineCode: InlineCode,
code: Code,
table: Table,
quote: Quote,
marker: Marker,
checkList: Checklist,
delimiter: Delimiter,
},
data: content,
});
};
async onSave(e) {
let data = await this.editor.saver.save();
this.props.save(data);
}
render() {
return (
<>
<button onClick={(e) => this.onSave(e)}>Save</button>
<div id={"editorjs"} onChange={(e) => this.onChange(e)}></div>
</>
);
}
}
This implementation works in NextJS
I will update code if I find a better solution.
UPDATE 2:
The answer suggested by Rising Odegua is working.

You have to create a seperate component and then import all your tools there:
import EditorJs from "react-editor-js";
import Embed from "#editorjs/embed";
import Table from "#editorjs/table";
import List from "#editorjs/list";
import Warning from "#editorjs/warning";
import Code from "#editorjs/code";
import LinkTool from "#editorjs/link";
import Image from "#editorjs/image";
import Raw from "#editorjs/raw";
import Header from "#editorjs/header";
import Quote from "#editorjs/quote";
import Marker from "#editorjs/marker";
import CheckList from "#editorjs/checklist";
import Delimiter from "#editorjs/delimiter";
import InlineCode from "#editorjs/inline-code";
import SimpleImage from "#editorjs/simple-image";
const CustomEditor = () => {
const EDITOR_JS_TOOLS = {
embed: Embed,
table: Table,
marker: Marker,
list: List,
warning: Warning,
code: Code,
linkTool: LinkTool,
image: Image,
raw: Raw,
header: Header,
quote: Quote,
checklist: CheckList,
delimiter: Delimiter,
inlineCode: InlineCode,
simpleImage: SimpleImage
};
return (
<EditorJs tools={EDITOR_JS_TOOLS} />
);
}
export default CustomEditor;
Then in your NextJS page, use a dynamic import like this:
let CustomEditor;
if (typeof window !== "undefined") {
CustomEditor = dynamic(() => import('../src/components/CustomEditor'));
}
And you can use your component:
return (
{CustomEditor && <CustomEditor />}
)
Source : https://github.com/Jungwoo-An/react-editor-js/issues/31

Related

Load function from external script using #loadable/component in React

I have a JSON file with several filepaths to scripts that I want to be able to load dynamically into my React app, to build each component based on specifications that are in the metadata. Currently I have the metadata in my app as a Metadata data object.
metadata.json:
{
"component1": { "script": "./createFirstLayer.js" },
"component2": { "script": "./createSecondLayer.js" }
}
Each script exports a function that I want to be able to use to construct the component. For troubleshooting purposes, it currently only returns a simple message.
function createFirstLayer(name) {
return name + " loaded!";
}
export default createFirstLayer;
I did some research and identified the #loadable/component package. Using this package as import loadable from "#loadable/component";, I attempted to load my script into App.js like this:
async componentDidMount() {
Object.keys(Metadata).forEach(function(name) {
console.log(Metadata[name].script);
var createLayer = loadable(() => import(Metadata[name].script));
var message = createLayer(name);
console.log(message);
});
}
Everything I have tried throws the TypeError createLayer is not a function. How can I get the function loaded?
I have also attempted the lazy method.
I have recreated a working demo of my problem here.
EDIT: I have tried to put this at the top of my app
const scripts = {};
Object.keys(Metadata).forEach(async function(name) {
import(Metadata[name].script).then((cb) => scripts[name] = cb);
});
This causes the TypeError Unhandled Rejection (Error): Cannot find module './createFirstLayer.js'. (anonymous function)
src/components lazy /^.*$/ groupOptions: {} namespace object:66
I have also attempted
const scripts = {};
Object.keys(Metadata).forEach(async function(name) {
React.lazy(() => import(Metadata[name].script).then((cb) => scripts[name] = cb));
});
My goal is to be able to call the appropriate function to create particular layer, and match them up in the metadata.
You don't need #loadable/component for two reasons.
You can accomplish your goal with dynamic imports
'#loadable/component' returns a React Component object, not your function.
To use dynamic imports simply parse your JSON the way you were, but push the call to the import's default function into state. Then all you have to do is render the "layers" from within the state.
Like this:
import React, { Component } from "react";
import Metadata from "./metadata.json";
class App extends Component {
constructor(props) {
super(props);
this.state = { messages: [] };
}
async componentDidMount() {
Object.keys(Metadata).forEach(name=> import(`${Metadata[name].script}`).then(cb =>
this.setState((state, props) => ({ messages: [...state.messages, cb.default(cb.default.name)] }))));
}
render() {
return (
<div className="App">
{this.state.messages.map((m, idx) => (
<h1 key={idx}>{m}</h1>
))}
</div>
);
}
}
export default App;
Here is the working example

How do I load and run external Javascript code in React that have their definitions in the application and not in the imported file?

Basically, I'm trying to run a function that creates and adds a Recipe class to an array in React based on an external javascript file that is hosted online - but all the definitions are inside my React app.
The external file looks like (Recipes.js) this:
function LoadRecipes(){
AddToRecipes(new Recipe({
name: "Kronyxium Core",
components: [],
requirements: [],
craftedAt: "Frost Temple Smithy"
}));
}
The way I attempt to go on with this follows:
import React, {useState, useEffect} from 'react';
import RecipeManager from "../logic/RecipeManager.js";
const Recipe = RecipeManager.Recipe;
const recipesList = RecipeManager.recipesList;
const AddToRecipes = RecipeManager.AddToRecipes;
function RecipeController() {
const [loadingRecipes, setLoadingRecipes] = useState(true);
useEffect(() => {
const script = document.createElement("script");
script.src = "https://raw.githack.com/Soralei/extern/main/Recipes.js";
script.async = true;
script.onload = () => {
setLoadingRecipes(false);
}
document.body.appendChild(script);
}, []);
useEffect(() => {
if(!loadingRecipes){
window.LoadRecipes();
}
}, [loadingRecipes]);
return (
<div>
{loadingRecipes ? <p>Loading recipes...</p> : (
<>
<p>Recipes:</p>
{/*recipesList.map((a, index) => <p key={"r"+index}>{a.name}</p>)*/}
</>
)}
</div>
)
}
export default RecipeController
Note that I try to run the function using window.LoadRecipes() once the script has been imported. However, I get undefined errors when the function is run:
Recipes.js:3 Uncaught ReferenceError: AddToRecipes is not defined
at LoadRecipes (Recipes.js:3)
I'm also adding the content of RecipeManager.js for clarity. This is local logic, and the goal is to have the external function make use of it:
class Recipe{
constructor(options = {}){
this.name = options.name || "Unnamed Recipe";
this.components = options.components || [];
this.requirements = options.requirements || [];
this.craftedAt = options.craftedAt || "handcrafted";
}
}
const recipesList = [];
function AddToRecipes(Recipe){
recipesList.push(Recipe);
console.log(Recipe.name, "was added to the recipes list.");
}
const exported = {
Recipe: Recipe,
recipesList: recipesList,
AddToRecipes: AddToRecipes
}
export default exported;
Is this not possible, or am I just doing this entirely wrong?
Why am I doing this? The idea is to host the recipes online in a way that allows for other people to easily view, edit, and have the changes affect my app directly, while keeping most of the work in the React app.
You have to export the function to be able to access it.
Default export (only one per file):
function LoadRecipes(){
AddToRecipes(new Recipe({
name: "Kronyxium Core",
components: [],
requirements: [],
craftedAt: "Frost Temple Smithy"
}));
}
export default LoadRecipes; // export
You should import it like this:
import LoadRecipes from 'pathtofile';
Named export (multiple ones):
export function LoadRecipes() {
AddToRecipes(new Recipe({
name: "Kronyxium Core",
components: [],
requirements: [],
craftedAt: "Frost Temple Smithy"
}));
}
export const add (a, b) => a + b; // another one
Import like this (using { }):
import {
LoadRecipes,
add
} from 'pathtofile';
Named exports are useful to export several values. During the import, one will be able to use the same name to refer to the corresponding value. Concerning the default export, there is only a single default export per module. A default export can be a function, a class, an object or anything else. This value is to be considered as the “main” exported value since it will be the simplest to import.
You can read about JavaScript modules here

Duplicate componentDidMount logic with useEffect() to load external JavaScript on client side

I’m implementing a rich text editor into a NextJS project. There are no React components for it, and it runs only on the client side, so I have to load the JavaScript and CSS files from an external source and work around SSR. Please don't recommend to use another tool, as that is not an option.
The tool works fine as a class component, but I’d like to port it into a functional component. When I test the functional component, it works occasionally — namely, after I change my file and save (even if it's just adding a space). But as soon as I refresh the page, I lose the editor. I thought it was because the component hadn’t mounted, but now I check for that, yet the issue persists.
I’ve tried various approaches, including Next’s Dynamic import with SSR disabled, but so far only the class method below has worked (the editor works by binding to the <textarea> element):
import React from "react";
import Layout from "../components/Layout";
class Page extends React.Component {
state = { isServer: true };
componentDidMount() {
this.MyEditor = require("../public/static/cool-editor.js");
this.setState({ isServer: false }); // Trigger rerender.
var app = MyEditor("entry"); // Create instance of editr.
}
render(props) {
return (
<Layout>
<textarea id="entry"></textarea>
</Layout>
);
}
}
export default Page;
Last attempt at functional component:
import React, { useEffect } from "react";
import Layout from "../components/Layout";
function hasWindow() {
const [isWindow, setIsWindow] = React.useState(false);
React.useEffect(() => {
setIsWindow(true);
return () => setIsWindow(false);
}, []);
return isWindow;
}
const Editor = () => {
useEffect(() => {
const script = document.createElement("script");
script.src =
"http://localhost:3000/static/article-editor/cool-editor.js";
script.async = true;
document.body.appendChild(script);
return () => {
document.body.removeChild(script);
};
}, []);
var app = MyEditor("entry");
return (
<Layout>
<textarea id="entry"></textarea>
</Layout>
);
};
const Page = () => {
const isWindow = hasWindow();
if (isWindow) return <Editor />;
return null;
};
export default Page;
You can use useRef hook in <textarea> tag:
const refContainer = useRef(null);
return <textarea ref={refContainer}>
then useEffect to check if the the element has been mounted.
useEffect(() => {
if (refContainer.current) {
refContainer.current.innerHTML = "ref has been mounted";
console.log("hello");
}
}, []);
Check the code here: https://codesandbox.io/s/modest-dubinsky-7r3oz
Some of the things I could suggest changing:
var app = MyEditor("entry"); is being created on every render. Consider using useRef as a way to keep instance variable: https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables
In Editor, the MyEditor variable is not defined.
hasWindow includes a useEffect that runs once (with empty dependency array), I don't think it needs the clean up function. To check staying at browser or server, you could simply use const isServer = type of window === 'undefined'
Custom hook should be named with prefix use

Dynamically rendering react components from string array

Running into an issue rendering components dynamically as the come off the CMS in the react code.
Having no problem getting & parsing the variable names into an array to be utilized in the actual rendering - but receiving errors here no matter the method I'm using:
Warning: is using uppercase HTML. Always use lowercase
HTML tags in React.
Warning: is using uppercase HTML. Always use lowercase HTML tags in React.
Which clearly shows I'm using caps :)
import React, {
Component
} from 'react';
import {
createClient
} from 'contentful';
import CtaBlock from './CTABlock';
import DeviceList from './DeviceList';
class HomeContainer extends Component {
constructor(props) {
super(props);
this.state = {
pageCont: [],
entries: []
};
}
componentDidMount() {
const client = createClient({
// This is the space ID. A space is like a project folder in Contentful terms
space: '...',
// This is the access token for this space. Normally you get both ID and the token in the Contentful web app
accessToken: '...'
});
client.getEntries({
'sys.id': '5stYSyaM8gkiq0iOmsOyKQ'
}).then(response => {
this.setState({
mainCont: response
});
});
}
getEntries = pageCont => {
var linkedEntries = pageCont.includes.Entry;
console.log(linkedEntries);
return linkedEntries;
};
render() {
var formattedComponents = [];
var renderedComponents = [];
if (this.state.mainCont) {
//getting the type of component from the Contetful API (String)
var componentList = this.getEntries(this.state.mainCont).map(entries => entries.sys.contentType.sys.id);
//converting the component names to upper case
formattedComponents = componentList.map(comps => comps.charAt(0).toUpperCase() + comps.slice(1));
renderedComponents = formattedComponents.map(MyComponent => {
return <MyComponent / >
});
}
return (
<div>
<h1> Dynamically Generated Components Div </h1>
{renderedComponents}
</div>
);
}
}
export default HomeContainer;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
Appreciate any insight!
When I understand you right, what you want to archive is, to map a string key to a certain component, right?
So that entries.sys.contentType.sys.id contains a string like "ctaBlock"or "deviceList"?
I would suggest using a map as follows:
import CtaBlock from './CTABlock';
import DeviceList from './DeviceList';
import FallbackComponent from './FallbackComponent';
const keyMap = {
ctaBlock : CtaBlock,
deviceList : DeviceList,
default: FallbackComponent,
};
...
componentList.map( entries => {
const key = entries.sys.contentType.sys.id;
const Component = keyMap[ key ] || keyMap.default;
return <Component />;
} );
See an example on:
https://jsfiddle.net/v7do62hL/2/

Using Dygraph in React with redux?

I've been having a lot of trouble implementing Dygraph in React (I'm using redux): http://dygraphs.com/. The Dygraph wrapper packages on NPM don't seem to work.
Also I can't simply use:
<div id="graph"></div>.
I believe this is because in react your working in a state instead of an actual index.html file.
So the method I'm currently trying to use is to create the graph component:
import React, { Component } from 'react';
import Dygraph from 'dygraphs';
import myData from '../../mockdata/sample-data.json';
import 'dygraphs/dist/dygraph.min.css'
import './graphComponent.css';
class DyGraph extends Component {
constructor(props) {
super(props);
// mock json data for graph
const messages = myData;
var data = "";
messages.forEach((response) => {
data += response[0] + ',' + response[1] + "\n";
});
new Dygraph('graphContainer', data, {
title: 'Pressure Transient(s)',
titleHeight: 32,
ylabel: 'Pressure (meters)',
xlabel: 'Time',
gridLineWidth: '0.1',
width: 700,
height: 300,
connectSeparatedPoints: true,
axes: { "x": { "axisLabelFontSize": 9 }, "y": { "axisLabelFontSize": 9 } },
labels: ['Date', 'Tampines Ave10 (Stn 40)'],
});
}
render() {
return <div></div>
}
}
export default DyGraph;
and then import it into:
import React, { Component } from 'react';
import DyGraph from './components/graph/graphComponent';
import './App.css';
class DeviceDetails extends Component {
render() {
return (
<div >
<DyGraph />
</div>
);
}
}
export default DeviceDetails;
And there is a Display state that if you click something it will go to:
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import WarningView from '../warning/warningView'
import DirectoryView from '../directory/directoryView'
import DeviceDetailView from '../devicedetails/devicedetails'
export const Display = ({ currentPage }) => {
switch(currentPage) {
case 'WARNING_PAGE':
return <WarningView/>;
case 'DIRECTORY_PAGE':
return <DirectoryView/>;
case 'SENSOR_PAGE':
return <DeviceDetailView/>;
default:
return <WarningView/>;
}
};
Display.propTypes = {
currentPage: PropTypes.string.isRequired,
};
export default connect(
(state) => ({ currentPage: state.currentPage }),
(dispatch) => ({ })
)(Display)
When I build and run this locally I get an error in the console (when I try to see the graph):
Uncaught (in promise) Error: Constructing dygraph with a non-existent div!
at Dygraph.__init__ (dygraph.js:217)
at new Dygraph (dygraph.js:162)
at new DyGraph (graphComponent.js:19)
at ReactCompositeComponent.js:295
at measureLifeCyclePerf (ReactCompositeComponent.js:75)
at ReactCompositeComponentWrapper._constructComponentWithoutOwner (ReactCompositeComponent.js:294)
at ReactCompositeComponentWrapper._constructComponent (ReactCompositeComponent.js:280)
at ReactCompositeComponentWrapper.mountComponent (ReactCompositeComponent.js:188)
at Object.mountComponent (ReactReconciler.js:46)
at ReactDOMComponent.mountChildren (ReactMultiChild.js:238)
If anyone can figure out what's going on or even give me a hint that'd be SOOO APPRECIATED. I specifically want to use dygraph instead of google charts or other react charts (which I've gotten working very easily), however, there is little information about dygraph implementation in React and I don't really understand why it won't work.
The problem is that this line:
new Dygraph('graphContainer', data, { ... })
Tries to create a Dygraph in the element with ID graphContainer. But there is no element with that ID, hence the failure.
You need to wait until React creates a div in the DOM to create the dygraph. You'll want to instantiate the Dygraph in componentDidMount:
class Dygraph extends Component {
render() {
return <div ref="chart"></div>;
}
componentDidMount() {
const messages = myData;
var data = "";
messages.forEach((response) => {
data += response[0] + ',' + response[1] + "\n";
});
new Dygraph(this.refs.chart, data, {
/* options */
});
}
}

Categories

Resources