How to access ReactQuill Ref when using dynamic import in NextJS? - node.js

I'm running into a funny problem. I'm using NextJS for its server-side rendering capabilities and am using ReactQuill as my rich-text editor. To get around ReactQuill's tie to the DOM, I'm dynamically importing it. However, that presents another problem which is that when I try to attach a ref to the ReactQuill component, it's treated as a loadable component instead of the ReactQuill component. I need the ref in order to customize how images are handled when uploaded into the rich-text editor. Right now, the ref returns current:null instead of the function I can use .getEditor() on to customize image handling.
Anybody have any thoughts on how I can address this? I tried ref-forwarding, but it's still applying refs to a loadable component, instead of the React-Quill one. Here's a snapshot of my code.
const ReactQuill = dynamic(import('react-quill'), { ssr: false, loading: () => <p>Loading ...</p> }
);
const ForwardedRefComponent = React.forwardRef((props, ref) => {return (
<ReactQuill {...props} forwardedRef={(el) => {ref = el;}} />
)})
class Create extends Component {
constructor() {
super();
this.reactQuillRef = React.createRef();
}
imageHandler = () => {
console.log(this.reactQuillRef); //this returns current:null, can't use getEditor() on it.
}
render() {
const modules = {
toolbar: {
container: [[{ 'header': [ 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'script': 'sub'}, { 'script': 'super' }],
['link', 'image'],
[{ 'indent': '-1'}, { 'indent': '+1' }],
[{ 'align': [] }],
['blockquote', 'code-block'],],
handlers: {
'image': this.imageHandler
}
}
};
return(
<ForwardedRefComponent
value={this.state.text}
onChange={this.handleChange}
modules={modules}
ref={this.reactQuillRef}/> //this.reactQuillRef is returning current:null instead of the ReactQuill function for me to use .getEditor() on
)
}
}
const mapStateToProps = state => ({
tutorial: state.tutorial,
});
export default connect(
mapStateToProps, {createTutorial}
)(Create);

I share my solution with hope that it helps you too.
Helped from https://github.com/zenoamaro/react-quill/issues/642#issuecomment-717661518
const ReactQuill = dynamic(
async () => {
const { default: RQ } = await import("react-quill");
return ({ forwardedRef, ...props }) => <RQ ref={forwardedRef} {...props} />;
},
{
ssr: false
}
);
export default function QuillWrapper() {
const quillRef = React.useRef(false)
return <>
<ReactQuill forwardedRef={quillRef} />
</>
}
for example you can use the ref to upload image with custom hanlder
import React, { useMemo } from "react";
import dynamic from "next/dynamic";
const ReactQuill = dynamic(
async () => {
const { default: RQ } = await import("react-quill");
return ({ forwardedRef, ...props }) => <RQ ref={forwardedRef} {...props} />;
},
{
ssr: false,
}
);
export default function QuillWrapper({ value, onChange, ...props }) {
const quillRef = React.useRef(false);
// Custom image upload handler
function imgHandler() {
// from https://github.com/quilljs/quill/issues/1089#issuecomment-318066471
const quill = quillRef.current.getEditor();
let fileInput = quill.root.querySelector("input.ql-image[type=file]");
// to prevent duplicate initialization I guess
if (fileInput === null) {
fileInput = document.createElement("input");
fileInput.setAttribute("type", "file");
fileInput.setAttribute(
"accept",
"image/png, image/gif, image/jpeg, image/bmp, image/x-icon"
);
fileInput.classList.add("ql-image");
fileInput.addEventListener("change", () => {
const files = fileInput.files;
const range = quill.getSelection(true);
if (!files || !files.length) {
console.log("No files selected");
return;
}
const formData = new FormData();
formData.append("file", files[0]);
formData.append("uid", uid);
formData.append("img_type", "detail");
quill.enable(false);
console.log(files[0]);
axios
.post("the/url/for/handle/uploading", formData)
.then((response) => {
// after uploading succeed add img tag in the editor.
// for detail visit https://quilljs.com/docs/api/#editor
quill.enable(true);
quill.insertEmbed(range.index, "image", response.data.url);
quill.setSelection(range.index + 1);
fileInput.value = "";
})
.catch((error) => {
console.log("quill image upload failed");
console.log(error);
quill.enable(true);
});
});
quill.root.appendChild(fileInput);
}
fileInput.click();
}
I don't know much about useMemo
but if i don't use the hook,
the editor keeps rerendered resulting in losing focus and I guess perfomance trouble too.
const modules = useMemo(
() => ({
toolbar: {
container: [
[{ font: [] }],
[{ size: ["small", false, "large", "huge"] }], // custom dropdown
["bold", "italic", "underline", "strike"], // toggled buttons
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
[{ script: "sub" }, { script: "super" }], // superscript/subscript
[{ header: 1 }, { header: 2 }], // custom button values
["blockquote", "code-block"],
[{ list: "ordered" }, { list: "bullet" }],
[{ indent: "-1" }, { indent: "+1" }], // outdent/indent
[{ direction: "rtl" }], // text direction
[{ align: [] }],
["link", "image"],
["clean"], // remove formatting button
],
handlers: { image: imgHandler }, // Custom image handler
},
}),
[]
);
return (
<ReactQuill
forwardedRef={quillRef}
modules={modules}
value={value}
onChange={onChange}
{...props}
/>
);
}

In NextJS, React.useRef or React.createRef do not work with dynamic import.
You should Replace
const ReactQuill = dynamic(import('react-quill'), { ssr: false, loading: () => <p>Loading ...</p> }
);
with
import ReactQuill from 'react-quill';
and render after when window is loaded.
import ReactQuill from 'react-quill';
class Create extends Component {
constructor() {
super();
this.reactQuillRef = React.createRef();
this.state = {isWindowLoaded: false};
}
componentDidMount() {
this.setState({...this.state, isWindowLoaded: true});
}
.........
.........
render(){
return (
<div>
{this.isWindowLoaded && <ReactQuil {...this.props}/>}
</div>
)
}
}

Use onChange and pass all the arguments, here one example to use the editor.getHTML()
import React, { Component } from 'react'
import dynamic from 'next/dynamic'
import { render } from 'react-dom'
const QuillNoSSRWrapper = dynamic(import('react-quill'), {
ssr: false,
loading: () => <p>Loading ...</p>,
})
const modules = {
toolbar: [
[{ header: '1' }, { header: '2' }, { font: [] }],
[{ size: [] }],
['bold', 'italic', 'underline', 'strike', 'blockquote'],
[
{ list: 'ordered' },
{ list: 'bullet' },
{ indent: '-1' },
{ indent: '+1' },
],
['link', 'image', 'video'],
['clean'],
],
clipboard: {
// toggle to add extra line breaks when pasting HTML:
matchVisual: false,
},
}
/*
* Quill editor formats
* See https://quilljs.com/docs/formats/
*/
const formats = [
'header',
'font',
'size',
'bold',
'italic',
'underline',
'strike',
'blockquote',
'list',
'bullet',
'indent',
'link',
'image',
'video',
]
class BlogEditor extends Component {
constructor(props) {
super(props)
this.state = { value: null } // You can also pass a Quill Delta here
this.handleChange = this.handleChange.bind(this)
this.editor = React.createRef()
}
handleChange = (content, delta, source, editor) => {
this.setState({ value: editor.getHTML() })
}
render() {
return (
<>
<div dangerouslySetInnerHTML={{ __html: this.state.value }} />
<QuillNoSSRWrapper ref={this.editor} onChange={this.handleChange} modules={modules} formats={formats} theme="snow" />
<QuillNoSSRWrapper value={this.state.value} modules={modules} formats={formats} theme="snow" />
</>
)
}
}
export default BlogEditor

If you want to use ref in Next.js with dynamic import
you can use React.forwardRef API
more info

Related

How to change default dropdown placeholder in adminjs

I created a drop down list. It works fine, but I'm having trouble translating the "Select..." and "No options" placeholders. The photos show what I mean.
There is an example of creating this drop-down list:
Student entity:
#Column({ type: 'varchar' })
studyStatus!: string;
StudyStatusEnum
enum StudyStatusEnum {
studying = 'studying',
completed = 'completed studies',
academic = 'academic leave',
deducted = 'deducted',
}
export default StudyStatusEnum;
admin-panel/student/translations
export default {
[StudyStatusEnum.academic]: 'MyTranslatedText',
[StudyStatusEnum.completed]: 'MyTranslatedText1',
[StudyStatusEnum.deducted]: 'MyTranslatedText2',
[StudyStatusEnum.studying]: 'MyTranslatedText3',
};
admin-panel/student/utils
const getStudentStudyStatus = (): IAvailableValues[] => {
return Object.values(StudyStatusEnum).map((value) => {
const label = studentStudyStatusTranslation[value as StudyStatusEnum];
return { label, value };
});
};
export default getStudentStudyStatus;
student.constants
const FIELDS = {
STUDY_STATUS: 'studyStatus',
};
export default Object.freeze({
FIELDS,
LIST_PROPERTIES: [
FIELDS.STUDY_STATUS, ],
SHOW_PROPERTIES: [],
EDIT_PROPERTIES: [],
FILTER_PROPERTIES: [],
});
student.resource
export default () => ({
resource: StudentEntity,
options: {
navigation: { icon: 'Education' },
properties: {
[StudentConstants.FIELDS.STUDY_STATUS]: {
availableValues: studyStatus,
},
},
listProperties: StudentConstants.LIST_PROPERTIES,
},
} as ResourceWithOptions);

Algolia autocomplete: How do I highlight items?

I use Algolia autocomplete in "standalone" mode, meaning without the commercial Algolia service to return the search results; I have my own server respond with an array of search results.
How do I highlight matches in my returned items/strings (search results)?
The backend must return results that are already highlighted using a tag such as: <mark>this-is-to-be-highlighted</mark>. Here's an example result array for an search for "pie":
const items = [
{ some_attribute: 'Apple <mark>pie</mark>' },
{ some_attribute: 'American <mark>pie</mark>' },
{ some_attribute: 'Chocolate <mark>pie</mark>' }
]
The complete javascript code would then be something like this:
import { autocomplete } from "#algolia/autocomplete-js"
autocomplete({
container: '#search_input',
// ...
getSources({ query }) {
// This would be an example response from your server
const items = [
{ some_attribute: 'Apple <mark>pie</mark>' },
{ some_attribute: 'American <mark>pie</mark>' },
{ some_attribute: 'Chocolate <mark>pie</mark>' }
]
return [
{
sourceId: 'pies',
getItems({ query }) {
const HIGHLIGHT_PRE_TAG = '__aa-highlight__'
const HIGHLIGHT_POST_TAG = '__/aa-highlight__'
return items.map((item) => ({
item,
_highlightResult: {
some_attribute: {
value: item.some_attribute
.replace(/<mark>/g, HIGHLIGHT_PRE_TAG)
.replace(/<\/mark>/g, HIGHLIGHT_POST_TAG)
}
}
}))
},
templates: {
// ...
item({ item, components, html }) {
return html`<div className="aa-ItemWrapper">
<div className="aa-ItemContent">
<div className="aa-ItemContentBody">
<div className="aa-ItemContentTitle">
${components.Highlight({ hit: item, attribute: 'some_attribute' })}
</div>
</div>
</div>
</div>`
},
// ...
}
},
// ...
]
},
// ...
})

Strange behavior on when I press button many times very quick in NODE.JS

I have got 2 cards with 2 candidates, every candidate has a button where user can have a unique vote, the card have a box with total votes also, if the user make a vote for the first candidate, then the value of total in both cards is updated (in a card is substracted in another is added), . But when I press the button many times and very fast, the data returned by the backend is incorrect. Someone can explain what is happening please.
TEMPLATE OF COMPONENT IN ANGULAR
<mat-card class="example-card" *ngFor="let candidate of candidates; let i = index">
<mat-card-actions>
<button mat-raised-button class="button-vote" (click)="saveVote(candidate.userVotes[0], candidate)">
<mat-icon color="accent" aria-hidden="false" aria-label="Example home icon">pan_tool</mat-icon>
</button>
<div class="result" mat-fab>{{candidate.totalVotes}} votos</div>
</mat-card-actions>
</mat-card>
COMPONENENT.TS
export class CandidatesContainerComponent implements OnInit {
candidates!: any[]
constructor(
private candidatesService: CandidatesService,
) { }
ngOnInit(): void {
this.obtainCandidates()
}
obtainCandidates() {
this.candidatesService.getCandidates().subscribe((response: any) => {
this.candidates = response
})
}
saveVote(userVote: any, candidate: object | any) {
this.candidatesService.updateVote(candidate._id, userVote.thisChoosed).subscribe(response => {
if (response) {
this.obtainCandidates()
}
})
}
}
CONTROLLER IN NODE.JS
const updateVote = async ( req = request, res = response ) => {
const { candidateId, thisChoosed } = req.params
const query = thisChoosed === 'true' ? candidateId: { $ne:candidateId }
const previousVote = await Candidate.findOneAndUpdate(
{ _id:query },
{
'$set': { 'userVotes.$[el].thisChoose': false },
'$inc': { 'totalVotes': -1 }
},
{ arrayFilters: [
{"el.user": req.user._id }]
}
)
const newVote = await Candidate.findOneAndUpdate(
{ _id:candidateId },
{
'$set': { 'userVotes.$[el].thisChoose': true },
'$inc': { 'totalVotes': 1 }
},
{
arrayFilters: [ {"el.user": req.user._id }, ], new:true
}
)
res.json( newVote )
}
DATABASE
const VoteSchema = Schema({
user:{
type:Schema.ObjectId,
ref:'User',
required:[true, 'User is required - vote user']
},
thisChoosed:{
type:Boolean,
default:false
},
firstVote:{
type:Boolean,
default:true
}
})
const CandidateSchema = Schema({
names:{
type:String,
required:[true]
},
surnames:{
type:String,
required:[true]
},
userVotes:[VoteSchema],
totalVotes:{
type:Number,
default:0
},
})

Dynamic testing with Jest, useRouteMatch, react-route-dom

I have a Switch component that renders Routes based on a switch statement that takes useRouteMatch().path as the argument and returns a string.
In my unit test I want to dynamically mock this but it seems I can only set one jest.mock per file.
SectionComponent.tsx:
import React from 'react'
import { useRouteMatch } from 'react-router-dom'
import * as strings from '../../../strings'
import Header from '../../shared/header/Header'
const SectionComponent: React.FC = ({ children }) => {
const { path } = useRouteMatch()
const sectionTitle = () => {
switch (path) {
case '/header':
return strings.demo.header.title
case '/navbars':
return strings.demo.navbar.title
case '/button':
// ...etc
default:
return ''
}
}
return (
<div>
<Header
title={sectionTitle()}
titleColor={strings.theme.orange}
titleSize="S"
bgColor={strings.theme.white}
size="S"
/>
{children}
</div>
)
}
export default SectionComponent
SectionComponent.spec.tsx:
import React from 'react'
import { render } from '#testing-library/react'
import SectionComponent from './SectionComponent'
import * as strings from '../../../strings'
// list I want to dynamically generate useRouteMatch().path and expected result from
const options = [
{ name: 'header', to: '/header' },
{ name: 'navbars', to: '/navbars' },
{ name: 'button', to: '/button' },
{ name: 'typography', to: '/typography' },
{ name: 'form', to: '/form' },
{ name: 'navs', to: '/navs' },
{ name: 'indicators', to: '/indicators' },
{ name: 'list-group', to: '/list-group' },
{ name: 'cards', to: '/cards' },
]
// Can only do this once per file
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), // use actual for all non-hook parts
useRouteMatch: () => ({ path: '/header' }), // return value can't access outside variables eg option.to ('/header')
}))
describe('SectionComponent', () => {
// ideally want to map thorugh options and test here:
it('should render with each Title', () => {
const { getByText } = render(<SectionComponent />)
expect(getByText(strings.demo.header.title)).toBeInTheDocument()
})
})
Does anyone have any suggestions? I'm happy to provide more info if necessary
Figured a workaround using jest.fn().mockReturnValueOnce() multiple times
import React from 'react'
import { render } from '#testing-library/react'
import SectionComponent from './SectionComponent'
const options = [
{ name: 'Headers' },
{ name: 'Navbars' },
{ name: 'Button' },
{ name: 'Typography' },
{ name: 'Forms' },
{ name: 'Navs' },
{ name: 'Indicators' },
{ name: 'List Group' },
{ name: 'Cards' },
]
jest.mock('react-router', () => ({
useRouteMatch: jest
.fn()
.mockReturnValueOnce({ path: '/header' })
.mockReturnValueOnce({ path: '/navbars' })
.mockReturnValueOnce({ path: '/button' })
.mockReturnValueOnce({ path: '/typography' })
.mockReturnValueOnce({ path: '/form' })
.mockReturnValueOnce({ path: '/navs' })
.mockReturnValueOnce({ path: '/indicators' })
.mockReturnValueOnce({ path: '/list-group' })
.mockReturnValueOnce({ path: '/cards' }),
}))
options.forEach(option => {
describe('SectionComponent', () => {
it('should render with each Title', () => {
const { getByText } = render(<SectionComponent />)
expect(getByText(option.name)).toBeInTheDocument()
})
})
})
Maybe not the most efficient but it works

Abstract type Node must resolve to an Object type at runtime for field Root.node with value \"\",received \"null\"."

I am implementing a search feature with react and relay.
Below is my schema.js
var { nodeInterface, nodeField } = nodeDefinitions(
(globalId) => {
var { type, id } = fromGlobalId(globalId);
if (type === 'User') {
return getUser(id);
}else if (type === 'Post') {
return getPost(id);
}else if (type === 'Setting') {
return getSetting(id);
}
return null;
},
(obj) => {
if (obj instanceof User) {
return userType;
}else if (obj instanceof Post) {
return postType;
}else if (obj instanceof Setting) {
return settingType;
}
return null;
}
);
var postType = new GraphQLObjectType({
name: 'Post',
fields: {
_id: {
type: new GraphQLNonNull(GraphQLID)
},
createdAt: {
type: GraphQLString
},
id: globalIdField('Post'),
title: {
type: GraphQLString
},
color: {
type: GraphQLString
},
userId: globalIdField('User'),
username: {
type: GraphQLString,
resolve: (post) => getUserById(post.userId),
},
content: {
type: GraphQLString
},
images: {
type: postImageType,
description: "Post's main image links"
}
},
interfaces: [nodeInterface]
});
const {
connectionType: postConnection,
} = connectionDefinitions({name: 'Post', nodeType: postType});
var settingType = new GraphQLObjectType({
name: 'Setting',
fields: {
_id: {
type: new GraphQLNonNull(GraphQLID)
},
id: globalIdField('Setting'),
amount: {
type: GraphQLString
},
all_posts: {
type: postConnection,
args: {
...connectionArgs,
query: {type: GraphQLString}
},
resolve: (rootValue, args) => connectionFromPromisedArray(
getAllPosts(rootValue, args),
args
),
},
},
interfaces: [nodeInterface]
});
var Root = new GraphQLObjectType({
name: 'Root',
fields: () => ({
node: nodeField,
setting: {
type: settingType,
args: {
...connectionArgs,
currency: {type: GraphQLString}
},
resolve: (rootValue, args) => {
return getSetting(args.currency).then(function(data){
return data[0];
}).then(null,function(err){
return err;
});
}
},
})
});
Below is my database.js
export function getAllPosts(params,args) {
let findTitle = {};
let findContent = {};
if (args.query) {
findTitle.title = new RegExp(args.query, 'i');
findContent.content = new RegExp(args.query, 'i');
}
console.log("getAllPosts",args)
return new Promise((resolve, reject) => {
Post.find({$or: [findTitle,findContent]}).sort({createdAt: 'descending'}).exec({}, function(err, posts) {
if (err) {
resolve({})
} else {
resolve(posts)
}
});
})
}
Now I want to fetch all posts by $query variable
So in view I wrote like this
import React, { Component } from 'react';
import Relay from 'react-relay';
class BlogList extends Component {
constructor(props) {
super(props);
this.state = {
query: '',
};
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(){
this.props.relay.setVariables({query: this.state.query});
}
render() {
return (
<div className="input-group col-md-12">
<input type="text" onChange={this.handleChange.bind(this,"query")} value={this.state.query} name="query" placeholder="Enter Title or content"/><br/>
<span className="input-group-btn">
<button type="button" onClick={this.handleSubmit} className="btn btn-info btn-lg">
<i className="glyphicon glyphicon-search"></i>
</button>
</span>
</div>
)
}
};
export default Relay.createContainer(BlogList, {
initialVariables: {
query: ''
},
fragments: {
viewer: () => Relay.QL`
fragment on Setting {
id,
all_posts(first: 10000000,query: $query) {
edges {
node {
id,
_id,
title,
content,
createdAt,
username,
color,
images{
full
}
}
}
}
}
`,
},
});
And in routes I have
const SettingQueries = {
viewer: () => Relay.QL`query{
setting(currency: "USD")
}`,
}
export default [{
path: '/',
component: App,
queries: UserQueries,PostQueries,SettingQueries,
indexRoute: {
component: IndexBody,
},
childRoutes: [
,{
path: 'settings',
component: Setting,
queries: SettingQueries,
}]
}]
Things are working on /graphql as
but when I search from website it generates error in response
{
"data": {
"node": null
},
"errors": [
{
"message": "Abstract type Node must resolve to an Object type at runtime for field Root.node with value \"\",received \"null\".",
"locations": [
{
"line": 2,
"column": 3
}
]
}
]
}
as my web-browser is sending requests as below
Please suggest me what am I missing?
Also If I need to add some additional information please let me know.
The problem might be in your nodeDefinitions() function. First callback, also named idFetcher must return a single object. However, i see in your definition that you return a collection
var { nodeInterface, nodeField } = nodeDefinitions(
(globalId) => {
var { type, id } = fromGlobalId(globalId);
...
}else if (type === 'Post') {
return getPosts(); // this should be getPost(id)
}
);
And thats why your next callback, known as typeResolver fails and returns you a null.
var { nodeInterface, nodeField } = nodeDefinitions(
...
(obj) => {
...
// here you get Promise/Collection instead of single Post instance, therefore condition failed
}else if (obj instanceof Post) {
return postType;
}
return null;
}
);
LordDave's answer revealed one problem in your code. As you commented in his answer, all_posts field of settingType was not working.
If you used mongoose library in your DB code, I see a problem with your query:
Post.find({$or: [findTitle,findContent]}).sort({createdAt: 'descending'}).exec({}, function(err, posts) {
if (err) {
resolve({})
} else {
resolve(posts)
}
});
Based on documentation of exec, change your query to
return Post.find({$or: [findTitle,findContent]}).sort({createdAt: 'descending'}).exec(function(err, posts) {
if (err) {
resolve({})
} else {
resolve(posts)
}
});
As exec returns a promise, you can even do
return Post.find({$or: [findTitle,findContent]}).sort({createdAt: 'descending'}).exec();
Finally I got it working by creating a new type 'postList' and defined it as below
var { nodeInterface, nodeField } = nodeDefinitions(
(globalId) => {
var { type, id } = fromGlobalId(globalId);
if (type === 'User') {
return getUser(id);
}else if (type==='postList') {
return getpostList(id);
} else{
return null;
}
},
(obj) => {
if (obj instanceof User) {
return userType;
}else if (obj instanceof postList) {
return postListType;
}else{
return null;
}
}
);
In database.js
class postList {}
postList.id = "Post_id";
export {postList}
export function getpostList(id) {
return new postList
}
and under root fields as below
var postListType = new GraphQLObjectType({
name: 'postList',
description: 'List of posts',
  fields: () => ({
  id: globalIdField('postList'),
  posts: {
  type: postConnection,
  description: 'List of posts',
  args: {
...connectionArgs,
query: {type: GraphQLString}
},
  resolve: (_, args) => connectionFromPromisedArray(getAllPosts(_,args), args),
  },
}),
interfaces: [nodeInterface],
});
var Root = new GraphQLObjectType({
name: 'Root',
fields: () => ({
node: nodeField,
postList: {
type: postListType,
resolve:(rootValue)=> {
return getpostList()
}
},
})
});
I ran into this issue when I was using an InterfaceType and checked for the InterfaceType before the specialized ObjectType in the if-elseif-else of my TypeResolver

Resources