react-sortable-hoc: Handling of click event on list item - reactjs

This question is about https://github.com/clauderic/react-sortable-hoc.
I am pretty new to React, so I find the following a bit irritating. Go to
https://jsfiddle.net/7tb7jkz5/. Notice line 4
const SortableItem = SortableElement(({value}) => <li className="SortableItem" onClick={console.debug(value)}>{value}</li>);
When you run the code, the console will log "Item 0" to "Item 99". A click on an item will log the same, but three times. Why does this happen? And is this really necessary or more like a bug?
I expected a behavior similar to an ordinary DOM, so the click event would bubble up from the clicked item and could be caught along the way through its ancestors. The observed behavior looks to me like the click event would be sent down by the list to each list item three times.
Update:
Well, this is actually as clear as crystal, thanks for pointing this out, Shubham. I did not just specify a reference, but an actual call to console.debug, which was executed every time the expression was evaluated. Common mistake.
Still, this means, each list item is rendered (I guess) three times, when I click one of them. I suspect missing optimization or even useless redrawing.

Try to use distance={1} on SortableContainer component.
Check this link https://github.com/clauderic/react-sortable-hoc/issues/461
const ListItemContainer = SortableContainer((props) => {
return <listItem />
})
<ListItemContainer
onSortEnd={this._orderingFolder}
lockAxis='y'
lockToContainerEdges={true}
lockOffset='0%'
distance={1}
/>

Another way you can define and handle click event using react-sortable-hoc:
please check below code
import { SortableContainer, SortableElement, arrayMove } from 'react-sortable-hoc';
const SortableItem = SortableElement(({ value }) => {
return (
<div >
<div id="button_value">{value}</div>
</div >
);
});
const SortableList = SortableContainer(({ items }) => {
return (
<div >
{items.map((value, index) => (
<SortableItem
key={`item-${index}`}
index={index}
value={value}
/>
))}
</div>
);
});
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6'],
};
}
onSortEnd = ({ oldIndex, newIndex }) => {
this.setState(({ items }) => ({
items: arrayMove(items, oldIndex, newIndex),
}));
};
shouldCancelStart = (e) => {
var targetEle = e;
if (!targetEle.id) {
targetEle = e.target;
}
if (targetEle.id === 'button_value') {
console.log('Div button click here ');
// perform you click opration here
}
// you can define multiple click event here using seprate uniq id of html element even you can manage delay on click here also using set timeout and sortmove props and sortend props you need to manage just one boolean flag.
}
render() {
return (
<div>
<SortableList
items={this.state.items}
onSortEnd={this.onSortEnd}
onSortStart={(_, event) => event.preventDefault()}
shouldCancelStart={this.shouldCancelStart} />
</div>
);
}
}
export default App;
First define id button_value as in html element and then using this id you can get click event using this props shouldCancelStart

You need to mention the onClick action as a function. Use () => inside the handler call. Try the below method, it works although has a very slow response
const SortableItem = SortableElement(({value}) => <li className="SortableItem" onClick={() => console.debug(value)}>{value}</li>);

Related

SolidJS: input field loses focus when typing

I have a newbie question on SolidJS. I have an array with objects, like a to-do list. I render this as a list with input fields to edit one of the properties in these objects. When typing in one of the input fields, the input directly loses focus though.
How can I prevent the inputs to lose focus when typing?
Here is a CodeSandbox example demonstrating the issue: https://codesandbox.io/s/6s8y2x?file=/src/main.tsx
Here is the source code demonstrating the issue:
import { render } from "solid-js/web";
import { createSignal, For } from 'solid-js'
function App() {
const [todos, setTodos] = createSignal([
{ id: 1, text: 'cleanup' },
{ id: 2, text: 'groceries' },
])
return (
<div>
<div>
<h2>Todos</h2>
<p>
Problem: whilst typing in one of the input fields, they lose focus
</p>
<For each={todos()}>
{(todo, index) => {
console.log('render', index(), todo)
return <div>
<input
value={todo.text}
onInput={event => {
setTodos(todos => {
return replace(todos, index(), {
...todo,
text: event.target.value
})
})
}}
/>
</div>
}}
</For>
Data: {JSON.stringify(todos())}
</div>
</div>
);
}
/*
* Returns a cloned array where the item at the provided index is replaced
*/
function replace<T>(array: Array<T>, index: number, newItem: T) : Array<T> {
const clone = array.slice(0)
clone[index] = newItem
return clone
}
render(() => <App />, document.getElementById("app")!);
UPDATE: I've worked out a CodeSandbox example with the problem and the three proposed solutions (based on two answers): https://codesandbox.io/s/solidjs-input-field-loses-focus-when-typing-itttzy?file=/src/App.tsx
<For> components keys items of the input array by the reference.
When you are updating a todo item inside todos with replace, you are creating a brand new object. Solid then treats the new object as a completely unrelated item, and creates a fresh HTML element for it.
You can use createStore instead, and update only the single property of your todo object, without changing the reference to it.
const [todos, setTodos] = createStore([
{ id: 1, text: 'cleanup' },
{ id: 2, text: 'groceries' },
])
const updateTodo = (id, text) => {
setTodos(o => o.id === id, "text", text)
}
Or use an alternative Control Flow component for mapping the input array, that takes an explicit key property:
https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#Key
<Key each={todos()} by="id">
...
</Key>
While #thetarnav solutions work, I want to propose my own.
I would solve it by using <Index>
import { render } from "solid-js/web";
import { createSignal, Index } from "solid-js";
/*
* Returns a cloned array where the item at the provided index is replaced
*/
function replace<T>(array: Array<T>, index: number, newItem: T): Array<T> {
const clone = array.slice(0);
clone[index] = newItem;
return clone;
}
function App() {
const [todos, setTodos] = createSignal([
{ id: 1, text: "cleanup" },
{ id: 2, text: "groceries" }
]);
return (
<div>
<div>
<h2>Todos</h2>
<p>
Problem: whilst typing in one of the input fields, they lose focus
</p>
<Index each={todos()}>
{(todo, index) => {
console.log("render", index, todo());
return (
<div>
<input
value={todo().text}
onInput={(event) => {
setTodos((todos) => {
return replace(todos, index, {
...todo(),
text: event.target.value
});
});
}}
/>
</div>
);
}}
</Index>
Dat: {JSON.stringify(todos())}
</div>
</div>
);
}
render(() => <App />, document.getElementById("app")!);
As you can see, instead of the index being a function/signal, now the object is. This allows the framework to replace the value of the textbox inline.
To remember how it works: For remembers your objects by reference. If your objects switch places then the same object can be reused. Index remembers your values by index. If the value at a certain index is changed then that is reflected in the signal.
This solution is not more or less correct than the other one proposed, but I feel this is more in line and closer to the core of Solid.
With For, whole element will be re-created when the item updates. You lose focus when you update the item because the element (input) with the focus gets destroyed, along with its parent (li), and a new element is created.
You have two options. You can either manually take focus when the new element is created or have a finer reactivity where element is kept while the property is updated. The indexArray provides the latter out of the box.
The indexArray keeps the element references while updating the item. The Index component uses indexArray under the hood.
function App() {
const [todos, setTodos] = createSignal([
{ id: 1, text: "cleanup" },
{ id: 2, text: "groceries" }
]);
return (
<ul>
{indexArray(todos, (todo, index) => (
<li>
<input
value={todo().text}
onInput={(event) => {
const text = event.target.value;
setTodos(todos().map((v, i) => i === index ? { ...v, text } : v))
}}
/>
</li>
))}
</ul>
);
}
Note: For component caches the items internally to avoid unnecessary re-renders. Unchanged items will be re-used but updated ones will be re-created.

autosuggest not showing item immediately

I am looking into fixing a bug in the code. There is a form with many form fields. Project Name is one of them. There is a button next to it.So when a user clicks on the button (plus icon), a popup window shows up, user enters Project Name and Description and hits submit button to save the project.
The form has Submit, Reset and Cancel button (not shown in the code for breviety purpose).
The project name field of the form has auto suggest feature. The code snippet below shows the part of the form for Project Name field.So when a user starts typing, it shows the list of projects
and user can select from the list.
<div id="formDiv">
<Growl ref={growl}/>
<Form className="form-column-3">
<div className="form-field project-name-field">
<label className="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-animated custom-label">Project Name</label>
<AutoProjects
fieldName='projectId'
value={values.projectId}
onChange={setFieldValue}
error={errors.projects}
touched={touched.projects}
/>{touched.projects && errors.v && <Message severity="error" text={errors.projects}/>}
<Button className="add-project-btn" title="Add Project" variant="contained" color="primary"
type="button" onClick={props.addProject}><i className="pi pi-plus" /></Button>
</div>
The problem I am facing is when some one creates a new project. Basically, the autosuggest list is not showing the newly added project immediately after adding/creating a new project. In order to see the newly added project
in the auto suggest list, after creating a new project,user would have to hit cancel button of the form and then open the same form again. In this way, they can see the list when they type ahead to search for the project they recently
created.
How should I make sure that the list gets immediately updated as soon as they have added the project?
Below is how my AutoProjects component looks like that has been used above:
import React, { Component } from 'react';
import Autosuggest from 'react-autosuggest';
import axios from "axios";
import { css } from "#emotion/core";
import ClockLoader from 'react-spinners/ClockLoader'
function escapeRegexCharacters(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Use your imagination to render suggestions.
const renderSuggestion = suggestion => (
<div>
{suggestion.name}, {suggestion.firstName}
</div>
);
const override = css`
display: block;
margin: 0 auto;
border-color: red;
`;
export class AutoProjects extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
projects: [],
suggestions: [],
loading: false
}
this.getSuggestionValue = this.getSuggestionValue.bind(this)
this.setAutoSuggestValue = this.setAutoSuggestValue.bind(this)
}
// Teach Autosuggest how to calculate suggestions for any given input value.
getSuggestions = value => {
const escapedValue = escapeRegexCharacters(value.trim());
if (escapedValue === '') {
return [];
}
const regex = new RegExp(escapedValue, 'i');
const projectData = this.state.projects;
if (projectData) {
return projectData.filter(per => regex.test(per.name));
}
else {
return [];
}
};
// When suggestion is clicked, Autosuggest needs to populate the input
// based on the clicked suggestion. Teach Autosuggest how to calculate the
// input value for every given suggestion.
getSuggestionValue = suggestion => {
this.props.onChange(this.props.fieldName, suggestion.id)//Update the parent with the new institutionId
return suggestion.name;
}
fetchRecords() {
const loggedInUser = JSON.parse(sessionStorage.getItem("loggedInUser"));
return axios
.get("api/projects/search/getProjectSetByUserId?value="+loggedInUser.userId)//Get all personnel
.then(response => {
return response.data._embedded.projects
}).catch(err => console.log(err));
}
setAutoSuggestValue(response) {
let projects = response.filter(per => this.props.value === per.id)[0]
let projectName = '';
if (projects) {
projectName = projects.name
}
this.setState({ value: projectName})
}
componentDidMount() {
this.setState({ loading: true}, () => {
this.fetchRecords().then((response) => {
this.setState({ projects: response, loading: false }, () => this.setAutoSuggestValue(response))
}).catch(error => error)
})
}
onChange = (event, { newValue }) => {
this.setState({
value: newValue
});
};
// Autosuggest will call this function every time you need to update suggestions.
// You already implemented this logic above, so just use it.
onSuggestionsFetchRequested = ({ value }) => {
this.setState({
suggestions: this.getSuggestions(value)
});
};
// Autosuggest will call this function every time you need to clear suggestions.
onSuggestionsClearRequested = () => {
this.setState({
suggestions: []
});
};
render() {
const { value, suggestions } = this.state;
// Autosuggest will pass through all these props to the input.
const inputProps = {
placeholder: value,
value,
onChange: this.onChange
};
// Finally, render it!
return (
<div>
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={renderSuggestion}
inputProps={inputProps}
/>
<div className="sweet-loading">
<ClockLoader
css={override}
size={50}
color={"#123abc"}
loading={this.state.loading}
/>
</div>
</div>
);
}
}
The problem is you only call the fetchRecord when component AutoProjects did mount. That's why whenever you added a new project, the list didn't update. It's only updated when you close the form and open it again ( AutoProjects component mount again)
For this case I think you should lift the logic of fetchProjects to parent component and past the value to AutoProjects. Whenever you add new project you need to call the api again to get a new list.

(React-redux) BlueprintJs Suggest cannot backspace in input after making a selection

class MySuggest extends React.Component<Props, State> {
....
....
private handleClick = (item: string, event: SyntheticEvent<HTMLElement, Event>) => {
event.stopPropagation();
event.preventDefault();
this.props.onChange(item);
}
public render() {
const { loading, value, error} = this.props;
const { selectValue } = this.state;
const loadingIcon = loading ? <Icon icon='repeat'></Icon> : undefined;
let errorClass = error? 'error' : '';
const inputProps: Partial<IInputGroupProps> = {
type: 'search',
leftIcon: 'search',
placeholder: 'Enter at least 2 characters to search...',
rightElement: loadingIcon,
value: selectValue,
};
return (
<FormGroup>
<Suggest
disabled={false}
onItemSelect={this.handleClick}
inputProps={inputProps}
items={value}
fill={true}
inputValueRenderer={(item) => item.toString()}
openOnKeyDown={true}
noResults={'no results'}
onQueryChange={(query, event) => {
if (!event) {
this.props.fetchByUserInput(query.toUpperCase());
}
}}
scrollToActiveItem
itemRenderer={(item, { modifiers, handleClick }) => (
<MenuItem
active={modifiers.active}
onClick={() => this.handleClick(item) }
text={item}
key={item}
/>
)}
/>
</FormGroup>
);
}
}
Everything works fine, I am able to make a selection from drop-down list, however I cannot use backspace in input if I made a selection. I checked the official documentation(https://blueprintjs.com/docs/#select/suggest), it has the same issue in its example. Does anyone has the similar problems and solutions?
The reason for this is once you type something in the field, it becomes an element of the page, so once you make a selection, it assumes you highlighted an element, so will assume you are trying to send the page a command for that selection (backspace is the default page-back command for most browsers).
Solution:
Create a new dialog input every time the user makes a selection, so the user can continue to make selections and edits.
It took forever.. post my solution here:
be careful about two things:
1. query = {.....} needed to control the state of the input box
2. openOnKeyDown flag, it makes the delete not working

Hide all but one tool tip

I have the following tooltip component:
export interface ITooltipProps {
Title: string;
Visibility: boolean;
Items: any[];
}
export const Tooltip: React.StatelessComponent<ITooltipProps> = (props) => {
if (!props.Visibility) {
return null;
}
return (
<div className={css.toolTip} role="tooltip" style={{margin: props.Margin}} aria-hidden={props.Visibility}>
<h1 className={css.toolTipHeader}>{props.Title}</h1>
<ul className={css.itemList}>
{props.Items.map((o) => {
return (
<li key={o.ID}>{o.Data}</li>
);
})}
</ul>
</div>
);
};
That gets called from another component like:
<div onMouseOver={this.showtooltip} onMouseLeave={this.hidetooltip}>
<Tooltip Title={strings.SecurityGroup_Label_ManagementOffices} ManagementOffices={offices} Visibility={this.state.IsToolTipVisible} />
</div>
private showtooltip = () => {
this.setState({ IsToolTipVisible: true });
}
private hidetooltip = () => {
this.setState({ IsToolTipVisible: false });
}
The problem I'm facing is, since there's one IsToolTipVisible, if I have multiple tooltips in the component, it displays/hides it all the tool tips at once. How do I code this so that it only displays the item being hovered over?
This can go into comments but adding it as an answer since it's big.
I can think of two ways now using your unique id (expecting it to be a id in the object we set to tooltip and not html id attributes)
you can use uniqueid and instead of saving a Boolean of istooltipvisible, save which one is currently visible.
And in the place where you check for istooltipvisible with Boolean, use something like eachuniqueid === currentvisibleid
Using this, you will have only one tooltip at a time which could be a concern.
You can save a property istooltipvisible for each tooltip and whenever you have it visible, set it to true and when you close it, set it to false.
This way you can manage multiple tooltips aswell.

React function not working from child component

I am trying to get a function working which removes an image uploaded using React Dropzone and react-sortable.
I have the dropzone working, and the sort working, but for some reason the function I have on the sortable item which removes that particular item from the array does not work.
The onClick event does not seem to call the function.
My code is below.
const SortableItem = SortableElement(({value, sortIndex, onRemove}) =>
<li>{value.name} <a onClick={() => onRemove(sortIndex)}>Remove {value.name}</a></li>
);
const SortableList = SortableContainer(({items, onRemove}) => {
return (
<ul>
{items.map((image, index) => (
<SortableItem key={`item-${index}`} index={index} value={image} sortIndex={index} onRemove={onRemove} />
))}
</ul>
);
});
class renderDropzoneInput extends React.Component {
constructor (props) {
super(props)
this.state = { files: [] }
this.handleDrop = this.handleDrop.bind(this)
}
handleDrop (files) {
this.setState({
files
});
this.props.input.onChange(files)
}
remove (index){
var array = this.state.files
array.splice(index, 1)
this.setState({files: array })
this.props.input.onChange(array)
}
onSortEnd = ({oldIndex, newIndex}) => {
this.setState({
files: arrayMove(this.state.files, oldIndex, newIndex),
});
};
render () {
const {
input, placeholder,
meta: {touched, error}
} = this.props
return (
<div>
<Dropzone
{...input}
name={input.name}
onDrop={this.handleDrop}
>
<div>Drop your images here or click to open file picker</div>
</Dropzone>
{touched && error && <span>{error}</span>}
<SortableList items={this.state.files} onSortEnd={this.onSortEnd} onRemove={(index) => this.remove(index)} />
</div>
);
}
}
export default renderDropzoneInput
Update: This was caused by react-sortable-hoc swallowing click events. Setting a pressDelay prop on the element allowed the click function to fire.
This is old question, but some people, like me, who still see this issue, might want to read this: https://github.com/clauderic/react-sortable-hoc/issues/111#issuecomment-272746004
Issue is that sortable-hoc swallows onClick events as Matt found out. But we can have workarounds by setting pressDelay or distance.
For me the best option was to set minimum distance for sortable list and it worked nicely
You can also use the distance prop to set a minimum distance to be dragged before sorting is triggered (for instance, you could set a distance of 1px like so: distance={1})

Resources