How to test nested Router? (OLD: How to test '<select>' elements with PageLoader?) - angular-dart

I'm trying to test an application that navigates with the Router every time the user change the value of a <select> element. I've used [ngModel] and (ngModelChange) on the element to catch the selected value.
I've tried to use click() on the <option> PageLoaderElement, but with no results.
lib/src/line/lines_component.html:
<h2>Lines</h2>
<div *ngIf="lines?.isNotEmpty">
Select line:
<select [ngModel]="selectedLine" (ngModelChange)="changeLine($event)">
<option disabled selected [ngValue]="nullLine"> -- select an option -- </option>
<option *ngFor="let line of lines" [ngValue]="line">{{line.name}}</option>
</select>
</div>
<div *ngIf="lines?.isEmpty">
No lines available!
</div>
<router-outlet [routes]="Routes.all"></router-outlet>
lib/src/line/lines_component.dart:
Future<NavigationResult> changeLine(Line line) =>
_router.navigate(_lineUrl(line.id));
test/lines_po.dart:
#ByTagName('option')
List<PageLoaderElement> get _lines;
Future<void> selectLine(var target) async {
final lineOpt =
_lines.firstWhere((line) => line.visibleText == target.name);
await lineOpt.click();
}
test/lines.dart:
void selectedLineTests(InjectorProbe injector) {
final targetLine = Line(0, 'Line 0'); // id = 0, name = 'Line 0'
setUp(() async {
await po.selectLine(targetLine);
await fixture.update();
});
test('has navigated to detail', () async {
final mockLocation = injector.get<MockPlatformLocation>(PlatformLocation);
expect(
mockLocation.pathname,
RoutePaths.line
.toUrl(parameters: {idParam: '${targetLine.id}'}).toString()); // '/lines/0'
});
}
Test results (from console in browser):
Expected: '/lines/0'
Actual: ''
Which: is different. Both strings start the same, but the actual value is missing the following trailing characters: /lines/0
UPDATE:
The problem seems to be related with the router, not with the click() on <option> element.
I've followed the guide on Webdev Dartlang site.
Changing the code to print the navigation result:
void _selectLine(int id) async {
var res = await _router.navigate(_lineUrl(id));
print(res);
}
During the test it prints: NavigationResult.INVALID_ROUTE.
I don't know if it helps, but this is how my routes are organized:
lib/src/route_paths.dart:
import 'package:angular_router/angular_router.dart';
const idParam = 'id';
class RoutePaths {
static final lines = RoutePath(path: 'lines');
}
lib/src/routes.dart:
import 'not_found_component.template.dart' as not_found_template;
import 'line/lines_component.template.dart' as lines_template;
import 'route_paths.dart';
export 'route_paths.dart';
class Routes {
static final lines = RouteDefinition(
routePath: RoutePaths.lines,
component: lines_template.LinesComponentNgFactory,
);
static final all = <RouteDefinition>[
lines,
RouteDefinition.redirect(
path: '',
redirectTo: RoutePaths.lines.toUrl(),
),
RouteDefinition(
path: '.+',
component: not_found_template.NotFoundComponentNgFactory,
),
];
}
lib/src/line/route_paths.dart:
import 'package:angular_router/angular_router.dart';
import '../route_paths.dart' as _parent;
export '../route_paths.dart' show idParam;
class RoutePaths {
static final line = RoutePath(
path: ':${_parent.idParam}',
parent: _parent.RoutePaths.lines,
);
}
lib/src/line/routes.dart:
import 'package:angular_router/angular_router.dart';
import 'line_component.template.dart' as line_template;
import 'route_paths.dart';
export 'route_paths.dart';
class Routes {
static final line = RouteDefinition(
routePath: RoutePaths.line,
component: line_template.LineComponentNgFactory,
);
static final all = <RouteDefinition>[
line,
];
}

I had to import the parent Routes class to make the test works:
test/dart:
import 'package:frontend/src/line/routes.dart';
import 'package:frontend/src/routes.dart' as parent_routes;
[...]
#Component(
selector: 'test',
template: '''
<my-lines></my-lines>
<router-outlet [routes]="[parent_routes.Routes.lines]"></router-outlet>
''',
directives: [RouterOutlet, LinesComponent],
exports: [parent_routes.Routes],
)
class TestComponent {
final Router router;
TestComponent(this.router);
}
I don't know if this is the correct way to do this. Comments are welcome!!!

Related

Uncaught (in promise) TypeError: hobbies is not iterable at createHobby when using Prisma and Postgresql

So I'm very new to Prisma, and actually also to React. My Postgresql database works, but I'm trying to show the stored data in my application. My very simple table in the schema file looks like this:
model Hobby {
id Int #id #default(autoincrement())
title String
}
I'm using useContext to distribute my createHobby functionality, this is what the context file looks like.
export async function getServerSideProps() {
const hobbies: Prisma.HobbyUncheckedCreateInput[] = await prisma.hobby.findMany();
return {
props: {initialHobbies: hobbies},
};
}
export const HobbyContext = createContext({})
function Provider({ children, initialHobbies }){
const [hobbies, setHobbies] = useState<Prisma.HobbyUncheckedCreateInput[]>(initialHobbies);
const createHobby = async (title) => {
const body: Prisma.HobbyCreateInput = {
title,
};
await fetcher("/api/create-hobby", {hobby : body});
console.log(hobbies);
const updatedHobbies = [
...hobbies,
body
];
setHobbies(updatedHobbies);
const contextData = {
hobbies,
createHobby,
}
return (
<HobbyContext.Provider value={contextData}>
{children}
</HobbyContext.Provider>
);
};
export default HobbyContext;
export {Provider};
Here I get the following error Uncaught (in promise) TypeError: hobbies is not iterable at createHobby. Which refers to the const updatedHobbies = [...hobbies, body];
For more context, I have a HobbyCreate.tsx which creates a little hobby card that renders the title of the hobby, which is submitted with a form.
function HobbyCreate({updateModalState}) {
const [title, setTitle] = useState('');
const {createHobby} = useHobbiesContext();
const handleChange = (event) => {
setTitle(event.target.value)
};
const handleSubmit = (event) => {
event.preventDefault();
createHobby(title);
};
return (
...
<form onSubmit={handleSubmit}></form>
...
)
I can't really figure out what is going wrong, I assume somewhere when creating the const [hobbies, setHobbies] and using the initialHobbies.
I don't think you're using the Context API correctly. I've written working code to try and show you how to use it.
Fully typed hobby provider implementation
This is a fully typed implementation of your Provider:
import { createContext, useState } from 'react';
import type { Prisma } from '#prisma/client';
import fetcher from 'path/to/fetcher';
export type HobbyContextData = {
hobbies: Prisma.HobbyCreateInput[]
createHobby: (title: string) => void
};
// you could provide a meaningful default value here (instead of {})
const HobbyContext = createContext<HobbyContextData>({} as any);
export type HobbyProviderProps = React.PropsWithChildren<{
initialHobbies: Prisma.HobbyCreateInput[]
}>;
function HobbyProvider({ initialHobbies, children }: HobbyProviderProps) {
const [hobbies, setHobbies] = useState<Prisma.HobbyCreateInput[]>(initialHobbies);
const createHobby = async (title: string) => {
const newHobby: Prisma.HobbyCreateInput = {
title,
};
await fetcher("/api/create-hobby", { hobby: newHobby });
console.log(hobbies);
setHobbies((hobbies) => ([
...hobbies,
newHobby,
]));
};
const contextData: HobbyContextData = {
hobbies,
createHobby,
};
return (
<HobbyContext.Provider value={contextData}>
{children}
</HobbyContext.Provider>
);
}
export default HobbyContext;
export { HobbyProvider };
Using HobbyProvider
You can use HobbyProvider to provide access to HobbyContext for every component wrapped inside it.
For example, to use it in every component on /pages/hobbies your implementation would look like:
// /pages/hobbies.tsx
import { useContext, useState } from 'react';
import HobbyContext, { HobbyProvider } from 'path/to/hobbycontext';
export default function HobbiesPage() {
// wrapping the entire page in the `HobbyProvider`
return (
<HobbyProvider initialHobbies={[{ title: 'example hobby' }]}>
<ExampleComponent />
{/* page content */}
</HobbyProvider>
);
}
function ExampleComponent() {
const { hobbies, createHobby } = useContext(HobbyContext);
const [title, setTitle] = useState('');
return (
<div>
hobbies: {JSON.stringify(hobbies)}
<div>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button onClick={() => createHobby(title)}>Create hobby</button>
</div>
</div>
);
}
Similarly, to make the context available throughout your entire website, you can use HobbyProvider in
/pages/_app.tsx.
Using getServerSideProps
To retrieve the initialHobbies from the database, your getServerSideProps would look something like this:
// /pages/hobbies.tsx
import type { Hobby } from '#prisma/client';
export async function getServerSideProps() {
// note: there is no need to use `Hobby[]` as prisma will automatically give you the correct return
// type depending on your query
const initialHobbies: Hobby[] = await prisma.hobby.findMany();
return {
props: {
initialHobbies,
},
};
}
You would have to update your page component to receive the props from getServerSideProps and set initialHobbies on HobbyProvider:
// /pages/hobbies.tsx
import type { InferGetServerSidePropsType } from 'next';
export default function HobbiesPage({ initialHobbies }: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<HobbyProvider initialHobbies={initialHobbies}>
<ExampleComponent />
</HobbyProvider>
);
}
Note your page component and getServerSideProps function have to be exported from the same file

How to Decorate a text with Modifier in Draft.js

I'm trying to decorate some text, but instead of making the process managed by a regex strategy, it should be the results of an ajax call to specify which peace of text to decorate. Is it possible to use any method from Modifier library? My idea is to call something inside the onChange method and modify the editorstate.
Any idea would be appreciated.
My solution uses a custom decorator and a dynamic regex, this combination might help achieve the effect you are hoping for.
Code structure follows this example to decorate tweets in draftjs.
You can replace the string array (var arr = ["one", "two", "three"]) in the code with an ajax call.
import React, { Component } from 'react';
import { Editor, EditorState, CompositeDecorator } from 'draft-js';
const styles = {
handle: {
color: 'black',
backgroundColor: '#FF7F7F',
direction: 'ltr',
unicodeBidi: 'bidi-override',
},
};
// arr can be accessed from an ajax call
var arr = ["one", "two", "three"]
const HANDLE_REGEX = new RegExp("(?:[\\s]|^)(" + arr.join("|") + ")(?=[\\s]|$)", 'gi')
function handleStrategy(contentBlock, callback, contentState) {
findWithRegex(HANDLE_REGEX, contentBlock, callback);
}
function findWithRegex(regex, contentBlock, callback) {
const text = contentBlock.getText();
let matchArr, start;
while ((matchArr = regex.exec(text)) !== null) {
start = matchArr.index;
callback(start, start + matchArr[0].length);
}
}
const HandleSpan = (props) => {
return (
<span
style={styles.handle}
data-offset-key={props.offsetKey}
>
{props.children}
</span>
);
};
class App extends Component {
constructor(props) {
super(props);
const compositeDecorator = new CompositeDecorator([
{
strategy: handleStrategy,
component: HandleSpan,
}
]);
this.state = {
editorState: EditorState.createEmpty(compositeDecorator),
};
this.onChange = (editorState) => this.setState({editorState});
}
render() {
return (
<div className="container-root">
<Editor
editorState={this.state.editorState}
onChange={this.onChange}
placeholder="Write..."
ref="editor"
spellCheck={true}
/>
</div>
);
}
}
export default App;

ERROR TypeError: You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable

I'm working on autocomplete-search with angular 4. This search bar will get books information from Google Books API. It works fine when I input any search terms. But it causes an error if I remove the entire search term or input a space.This is the error I got
This is my SearchComponent.ts
import { Component, OnInit } from '#angular/core';
import { HttpClient, HttpHeaders } from '#angular/common/http';
import { Observable } from 'rxjs/Observable';
#Component({
selector: 'app-admin-search',
templateUrl: './admin-search.component.html',
styleUrls: ['./admin-search.component.css']
})
export class AdminSearchComponent implements OnInit {
books: any[] = [];
searchTerm$ = new Subject<string>();
constructor (private bookService: BookService,
private http: HttpClient
) {
this.bookService.search(this.searchTerm$)
.subscribe(results => {
this.books = results.items;
});
}
ngOnInit() {
}
This is my SearchComponent.html
<div>
<h4>Book Search</h4>
<input #searchBox id="search-box"
type="text"
placeholder="Search new book"
(keyup)="searchTerm$.next($event.target.value)"/>
<ul *ngIf="books" class="search-result">
<li *ngFor="let book of books">
{{ book.volumeInfo.title }}
</li>
</ul>
</div>
This is my BookService.ts
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { HttpClient, HttpHeaders } from '#angular/common/http';
import { Book } from './book';
import { BOOKS } from './mock-books';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/switchMap';
#Injectable()
export class BookService {
private GoogleBookURL: string = "https://www.googleapis.com/books/v1/volumes?q=";
constructor (private http: HttpClient) { }
search(terms: Observable<string>) {
return terms.debounceTime(300)
.distinctUntilChanged()
.switchMap(term => this.searchEntries(term));
}
searchEntries(searchTerm: string) {
if (searchTerm.trim()) {
searchTerm = searchTerm.replace(/\s+/g, '+');
let URL = this.GoogleBookURL + searchTerm;
return this.http.get(URL);
}
}
}
Can someone help me out? Thanks in advance!
Your method searchEntries returns value (Observable<Response>) only if searchTerm.trim() is true (so it must return non-empty string).
There can be situation that searchEntries will return undefined instead of Obervable<Response> if trim() returns '' (empty string which is false). You can't pass undefined returned from searchEntries into .switchMap(term => this.searchEntries(term));.
For that case your code will look like this:
.switchMap(term => undefined) which is not valid construction.

AsyncValidator in Angular 4 executes fine but could not get the response back to Reactive form

I am trying to implement async validator in my Reactive Form in angular 4.3.4 which will check that entered email exists or not in your system.
but this does not work properly, earlier it was invoking on every key up so I made some changes and make it Observable now only Checking after a given debounce time. checking...' text is displaying but the response comes but no error is being displayed on the page.
what can be the issue? I have very base knowledge of Observable and angular 4. please help me what is the issue. I have checked in the console and it is going and print the value in the asyncvalidator function.
here is the relevant code.
signup.component.html
<form [formGroup]="myForm" novalidate #formDir="ngForm" (ngSubmit)="doSignup()">
<input type="email" formControlName="email" pattern="{{email_pattern}}"/>
<div [hidden]="myForm.controls.email.valid || myForm.controls.email.pristine" class="text-danger">
<div *ngIf="myForm.controls.email.required">Please enter Email</div>
<div *ngIf="myForm.controls.email.pattern">Invalid Email</div>
<div *ngIf="myForm.controls.email.status === 'PENDING'">
<span>Checking...</span>
</div>
<div *ngIf="myForm.controls.email.errors && myForm.controls.email.errors.emailTaken">
Invitation already been sent to this email address.
</div>
</div>
<button type="submit" [disabled]="!myForm.valid">Invite</button>
</form>
signup.component.ts
import { FormBuilder, FormGroup, Validators, FormControl } from '#angular/forms';
import { ValidateEmailNotTaken } from './async-validator';
export class SignupComponent implements OnInit {
public myForm: FormGroup;
constructor(
private httpClient: HttpClient,
private fb: FormBuilder
) {
}
ngOnInit(): void {
this.buildForm();
}
private buildForm() {
this.inviteForm = this.fb.group({
firstname: [''],
lastname: [''],
email: [
'',
[<any>Validators.required, <any>Validators.email],
ValidateEmailNotTaken.createValidator(this.settingsService)
]
});
}
asyn-validator.ts
import { Observable } from 'rxjs/Observable';
import { AbstractControl } from '#angular/forms';
import { UserService } from './user.service';
export class ValidateEmailNotTaken {
static createValidator(service: UserService) {
return (control: AbstractControl): { [key: string]: any } => {
return Observable.timer(500).switchMapTo(service.checkEmailNotTaken(control.value))
.map((res: any) => {
const exist = res.item.exist ? { emailTaken: true } : { emailTaken: false };
console.log('exist: ', exist);
return Observable.of(exist);
})
.take(1);
};
}
}
user.service.ts
checkEmailNotTaken(email) {
const params = new HttpParams().set('email', email);
return this.httpClient.get(`API_END_POINT`, {
headers: new HttpHeaders({
'Content-type': 'application/json'
}),
params: params
});
}
You use Observable.timer(500) without a second argument, so after 500 milliseconds, it completes and never runs again. So first thing to do is to pass that argument - Observable.timer(0, 500).
switchMapTo cancels its previous inner Observable (service.checkEmailNotTaken(control.value) in your case) every time source Observable emits new value (so every 500 milliseconds). So if your http request lasts longer, you wont get its response. Thats why usually switchMap and switchMapTo are not suitable for http requests.
Here is an illustration:
const source = Rx.Observable.timer(0, 500);
const fail = source.switchMapTo(Rx.Observable.of('fail').delay(600))
const success = source.switchMapTo(Rx.Observable.of('success').delay(400))
const subscribe = fail.subscribe(val => console.log(val));
const subscribe2 = success.subscribe(val => console.log(val));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.js"></script>
So you should pick another flattening operator, like flatMap:
const source = Rx.Observable.timer(0, 500);
const success = source.flatMap(()=>Rx.Observable.of('success').delay(600))
const subscribe = success.subscribe(val => console.log(val));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.js"></script>
I know its too late for the answer but anyone facing same issue might find it useful:
apart from above answer the AsyncValidatorFn should return Promise<ValidationErrors | null> | Observable<ValidationErrors | null>.
Return value of ValidationErrors | null isn't correct.
Check out official docs

React DnD - "Cannot have two HTML5 backends at the same time."

I am trying to make a POC with Rails5, action Cable, React and Rails and React DnD.
The purpose is to make an app like trello but for an recruitment process.
My front is in ReactJS.
I have 3 components, first, the container call "Candidates", this component call 2 "CardBoard" components that call "Card" component.
I user react DnD library for draggable card and droppable CardBoard. when i drop card on cardboard, i use a post call and a websocket(action cable from rails5) for update my state. I don't understand why i have this message after the post call :
Uncaught Error: Cannot have two HTML5 backends at the same time.
at HTML5Backend.setup (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:4175), <anonymous>:87:15)
at DragDropManager.handleRefCountChange (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:3566), <anonymous>:52:22)
at Object.dispatch (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:4931), <anonymous>:186:19)
at HandlerRegistry.addSource (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:3594), <anonymous>:104:18)
at registerSource (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:4294), <anonymous>:9:27)
at DragDropContainer.receiveType (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:1793), <anonymous>:146:32)
at DragDropContainer.receiveProps (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:1793), <anonymous>:135:14)
at new DragDropContainer (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:1793), <anonymous>:102:13)
at eval (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:4399), <anonymous>:295:18)
at measureLifeCyclePerf (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:4399), <anonymous>:75:12)
Candidate.jsx =
import React, { PropTypes } from 'react';
import { DragDropContextProvider } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import CardBoard from './CardBoard.jsx';
export default class Candidates extends React.Component {
constructor(props, _railsContext) {
super(props);
this.state = {
candidates: this.props.candidates
}
this.filterByStatus = this.filterByStatus.bind(this)
}
componentDidMount() {
this.setupSubscription();
}
setupSubscription() {
App.candidate = App.cable.subscriptions.create("CandidateChannel", {
connected: () => {
console.log("User connected !")
},
received: (data) => {
this.setState({ candidates: data.candidates })
},
});
}
render() {
return (
<DragDropContextProvider backend={HTML5Backend}>
<div className="recruitment">
{
["À Rencontrer", "Entretien"].map((status, index) => {
return (
<CardBoard candidates={(this.filterByStatus({status}))} status={status} key={index} />
);
})
}
</div>
</DragDropContextProvider>
);
}
}
CardBoard.jsx =
import React, { PropTypes } from 'react';
import Card from './Card.jsx';
import { DropTarget } from 'react-dnd';
import ItemTypes from './ItemTypes';
const cardTarget = {
drop(props: Props) {
var status = ''
if(props.status == "À Rencontrer") {
status = 'to_book'
} else {
status = 'interview'
}
return { status: status };
},
};
#DropTarget(ItemTypes.CARD, cardTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}))
export default class CardBoard extends React.Component<Props> {
constructor(props, _railsContext) {
super(props);
}
render() {
const { canDrop, isOver, connectDropTarget } = this.props;
const isActive = canDrop && isOver;
return connectDropTarget(
<div className={`${this.props.status} ui cards`}>
<h2>{`${this.props.status} (${this.props.candidates.length})`}</h2>
{
(this.props.candidates).map((candidate, index) => {
return <Card candidate={candidate} key={index} />
})
}
{ isActive?
'Release to drop' : 'drag a card here'
}
</div>
);
}
}
Card.jsx=
import React, { PropTypes } from 'react';
import { DragSource } from 'react-dnd';
import ItemTypes from './ItemTypes';
const cardSource = {
beginDrag(props) {
return {
candidate_id: props.candidate.id,
};
},
endDrag(props, monitor) {
const item = monitor.getItem();
const dropResult = monitor.getDropResult();
if (dropResult) {
console.log(`You dropped ${item.candidate_id} vers ${dropResult.status} !`);
$.post(`/update_status/${item.candidate_id}/${dropResult.status}`);
}
},
};
#DragSource(ItemTypes.CARD, cardSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging(),
}))
export default class Card extends React.Component {
constructor(props, _railsContext) {
super(props);
}
render() {
const { isDragging, connectDragSource } = this.props;
const { name } = this.props;
const opacity = isDragging ? 0 : 1;
var candidate = this.props.candidate;
return (
connectDragSource(
<div className="card" key={candidate.id} style={{opacity}}>
<div className="content">
<div className="header">{`${candidate.first_name} ${candidate.last_name}`}</div>
<div className="description">
{candidate.job_title}
</div>
<span className="right floated">
<i className="heart outline like icon"></i>
{candidate.average_rate}
</span>
</div>
</div>
)
);
}
}
for better comprehension, here a gif of my feature and his bug :
feature with bug
here my github repo with all code
Just try to make the context of HTML5Backend a singleton. You can check the codes at:
https://github.com/react-dnd/react-dnd/issues/186
https://github.com/react-dnd/react-dnd/issues/708
I faced with similar issue and found workaround by moving
<DragDropContextProvider></DragDropContextProvider>
to the topmost react component.
Structure before:
App
Component1
Component2
...
ComponentX with render(<DragDropContextProvider>...<DragDropContextProvider>)
Structure after:
App with render(<DragDropContextProvider>...<DragDropContextProvider>)
Component1
Component2
...
ComponentX
Supposing App is loaded only once and HTML5 backend is not replicated.
You can either create a new file and import DragDropContext where you need it:
import HTML5 from 'react-dnd-html5-backend';
import {DragDropContext} from 'react-dnd';
export default DragDropContext(HTML5);
Source: https://github.com/react-dnd/react-dnd/issues/708#issuecomment-309259695
Or using Hooks:
import { DndProvider, createDndContext } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend";
import React, { useRef } from "react";
const RNDContext = createDndContext(HTML5Backend);
function useDNDProviderElement(props) {
const manager = useRef(RNDContext);
if (!props.children) return null;
return <DndProvider manager={manager.current.dragDropManager}>{props.children}</DndProvider>;
}
export default function DragAndDrop(props) {
const DNDElement = useDNDProviderElement(props);
return <React.Fragment>{DNDElement}</React.Fragment>;
}
Use:
import DragAndDrop from "../some/path/DragAndDrop";
export default function MyComp(props){
return <DragAndDrop>....<DragAndDrop/>
}
Source: https://github.com/react-dnd/react-dnd/issues/186#issuecomment-561631584
If you use BrowserRouter then move the DragDropContextProvider out of BrowserRouter:
Source: https://github.com/react-dnd/react-dnd/issues/186#issuecomment-453990887
DndProvider has a options prop in where you can set rootElement which bounds DnD to that specified context, and unfortunately it isn't documented well. This approach solved all my issues, as I had other component which was using DnD and they were out of my boundary and I wasn't able to make HTML5Backend Singleton.
I tried this approach with "react-dnd": "^14.0.2"
const myFirstId = 'first-DnD-Containier';
const mySecondId = 'second-DnD-Containier';
export const DndWrapper = React.memo((props) => {
const [context, setContext] = useState(null);
useEffect(() => {
setContext(document.getElementById(props.id))
},[props.id])
return context ? (
<DndProvider backend={HTML5Backend} options={{ rootElement: context}}>
{props.children}
</DndProvider>
) : null;
});
export default function App() {
return (
<div>
<div id={myFirstId}>
<DndWrapper id={myFirstId}>
<MyOtherComponents />
</DndWrapper>
</div>
<div id={mySecondId}>
<DndWrapper id={mySecondId}>
<MyOtherComponents />
</DndWrapper>
</div>
</div>
);
}
P.S. Make sure by the time you call document.getElementById the DOM exist with ids.
Move the DragDropContextProvider out of BrowserRouter.
import { BrowserRouter as Router } from 'react-router-dom';
import { DndProvider } from 'react-dnd';
<DndProvider>
<Router>
<App />
</Router>
</DndProvider>