I'm currently working on a project which involves using multiple wysiwyg editors. I have previously used react-draft in the same project but has always been used with static elements eg, each editor is fixed.
In my case, my editors are created on the fly, (min 1, max 15) editors. I'm rendering these into my containers using map() with constructed object each time. Allowing the user to click + or - buttons to create / remove a editor.
for example to create a new editor into, i push to then map over the components array which looks something like the below:
components: [
{
id:1,
type: 'default',
contentValue: [
title: 'content-block',
value: null,
editorState: EditorState.CreateEmpty(),
]
}
]
I am able to render multiple editors just fine and createEmpty ediorstates. My issue is when i try to update the contents editor state.
Usually to update a single editor id use:
onEditorStateChange = editorState => {
this.setState({
editorstate,
})
}
However, given the fact my editors are dynamically rendered, i have the editor state isolated within the "Components" array. So i've tried the following which did not work:
In Render
this.state.components.map((obj) => {
return (
<Editor
editorState={obj.contentValue.editorState}
onEditorStateChange={(e) => this.onEditorStateChange(e, obj.id)}
/>
);
}
onEditorStateChange
onEditorStateChange(e, id){
const { components } = this.state;
const x = { components };
for (const i in x){
if(x[i].id ==== id){
x[i].contentValue.editorState = e;
}
}
this.setState({components: x})
}
Upon debugging, the "setState" does get called in the above snippet, and it does enter the if statement, but no values are set in my editorState.
I'm happy for alternative ways to be suggested, as long as this will work with dynamically rendered components.
I require this value to be set, as i will be converting to HTML / string and using the content to save to a database.
I hope i have explained this well, if not please let me know and ill be happy to provide further information / snippets.
Thank you in advance.
Okay, i figured out a solution.
inside my onEditorStateChange() i update the value with the parameter (e) which was initally passed in. Using draftToHtml i convert it to raw and pass it to set state as shown below:
onEditorStateChange(e, id) {
const { components } = this.state;
console.log(e);
const x = components;
for (const i in x) {
if (x[i].id === id) {
x[i].contentValue.editorState = e;
x[i].value = draftToHtml(convertToRaw(e.getCurrentContent()));
//console.log(x[i]);
}
}
this.setState({
components: x,
});
}
This gives me the a HTML value which i can now convert to string and save to the database.
Hope this helps someone else with the same issue :)
Related
I have some list of names that I take from the array using the Fetch method. Now I'm using the method of searchHandler at the click of a button, I enter the input data into the console:
https://codesandbox.io/s/jovial-lovelace-z659k
But I need to enter the input "First name", and click on the button, only a line with that name was displayed. But I don't know how to make the filter myself.
I found the solution on the internet, but unfortunately I can't integrate it into my code.Here it is:
getFilteredData() {
if (!this.state.search){
return this.state.data
}
return this.state.data.filter(item=>{
return item["firstName"].toLowerCase().includes(this.state.search.toLowerCase())
});
}
How to integrate it into my code? And what to write in the render method?
You are in the right direction there. The correct code (with comments explaining the changes) should be:
searchHandler = search => {
// This if checks if search is empty. In that case, it reset the data to print the initial list again
if (search) {
// This 'arr' variable is a copy of what you do in your Table.js
const arr = this.state.data.group && this.state.data.group.first ? this.state.data.group.first : this.state.data;
const filteredArr = arr.filter((item) => {
// Here you compare with 'search' instead of 'state.search', since you didn't updated the state to include the search term
return item["firstName"].toLowerCase().includes(search.toLowerCase())
})
// This will update your state, which also updates the table
this.setState({data: filteredArr})
} else {
// As explained, if search was empty, return everything
this.resetData();
}
};
// This is a copy of what you have in componentDidMount
async resetData() {
const response = await fetch("/data/mass.json");
const data = await response.json();
this.setState({
data
});
}
Note:
includes is not supported by all browsers, as you can see here. If you need a more reliable solution, you could use indexOf, as explained here.
Since your fetched data is an array of objects, and you basically want to filter out the objects which match the serach criteria, heres how you can write your search handler:
searchHandler = search => {
const { data } = this.state;
const filteredData = {
group: {
first: Object.values(data.group.first).filter(item => item.firstName.includes(search)),
}
}
this.setState({ data: filteredData });
};
Basically, what its doing is taking the array of objects out of dataand filter out only those objects which have the name you search for. and sets the filtered array of objects in the same structure as your original data object is and there you go!!
Also you don't have to make any changes to the render method now. Since render method is already working with the state which has data in it. and as soon as you make a search state, data will be updated and available in the render.
//It's working now - updated code
I'm working on my own autocomplete component because I have problem with passing firebase data to a ready one.
The whole mechanism is working good but I have problem with passing values after getting user input
I'm setting initial state with those values
const INITIAL_STATE = {
allChars: [],
suggestions: [],
value: ""
};
Then in autocomplete class i'm loading all users from database
loadData(){
let self = this;
let characters = firebase.firestore().collection("users");
characters.get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
let document = doc.data();
self.setState(({allChars})=>({
allChars: [
...allChars,
document
]
}))
});
});
}
Here is my getSuggestions function. It is firing on input change
getSuggestions = event => {
const {value, suggestions} = event.target;
this.setState({
value: value,
suggestions: []
})
let suggest = [];
this.state.allChars.map((allChars) => {
if(value.length > 1 && allChars.name.toLowerCase().includes(value.toLowerCase())){
suggest.push (
allChars.name
);
}
})
this.setState({
suggestions: suggest
})
}
In render I just put {sugestions}
But in {suggestions} I get rendered only one name.
one
But when I console.log it - I get two names
two
There should be two.
I tried to set state in this function like in loadData(), but I still get only one value.
Is there other way to get both values into DOM
Full code can be found here: https://github.com/Ilierette/react-planner/blob/master/src/component/elements/Autocomplete.js
I think the reason you are just seeing one element each time your components re-render is that in your map function on your allChars array, when you want to update the suggestions in your state, you are setting just the name each time as a new array while you should update the existing array in your state, so your code should be:
this.setState({
suggestions: [...this.state.suggestions, allChars.name]
})
Been at this one for a LONG time and can't quite figure it out.
I have two components, controlled by a parent component. There is a property called "selected". So, when a user clicks on a list, it will update the parent component's selected property which is passed to the TagInput, which uses a MentionPlugin from draft-js.
In order to handle this, I implement a componentWillReceiveProps that looks as follows.
componentWillReceiveProps(nextProps) {
const { initialTags: newTags } = nextProps;
const previousTags = this.getTags(this.state.editorState);
if (previousTags.length !== newTags.length) {
const added = newTags.filter(tag => !previousTags.includes(tag));
const removed = previousTags.filter(tag => !newTags.includes(tag));
this.addMentions(added);
this.removeMentions(removed);
}
}
While it's easy to add entities in addMentions by creating a new entity and inserting it, for the life of me, I cannot figure out how to get a Mention by text and then delete it from the editor.
removeMentions(tags) {
const { editorState } = this.state;
for (const tag of tags) {
// find tag in editor
// select it and remove it
}
}
How would this be done?
So I just started trying to learn rxjs and decided that I would implement it on a UI that I'm currently working on with React (I have time to do so, so I went for it). However, I'm still having a hard time wrapping my head around how it actually works... Not only "basic" stuff like when to actually use a Subject and when to use an Observable, or when to just use React's local state instead, but also how to chain methods and so on. That's all too broad though, so here's the specific problem I have.
Say I have a UI where there's a list of filters (buttons) that are all clickeable. Any time I click on one of them I want to, first of all, make sure that the actions that follow will debounce (as to avoid making network requests too soon and too often), then I want to make sure that if it's clicked (active), it will get pushed into an array and if it gets clicked again, it will leave the array. Now, this array should ultimately include all of the buttons (filters) that are currently clicked or selected.
Then, when the debounce time is done, I want to be able to use that array and send it via Ajax to my server and do some stuff with it.
import React, { Component } from 'react';
import * as Rx from 'rx';
export default class CategoryFilter extends Component {
constructor(props) {
super(props);
this.state = {
arr: []
}
this.click = new Rx.Subject();
this.click
.debounce(1000)
// .do(x => this.setState({
// arr: this.state.arr.push(x)
// }))
.subscribe(
click => this.search(click),
e => console.log(`error ---> ${e}`),
() => console.log('completed')
);
}
search(id) {
console.log('search --> ', id);
// this.props.onSearch({ search });
}
clickHandler(e) {
this.click.onNext(e.target.dataset.id);
}
render() {
return (
<section>
<ul>
{this.props.categoriesChildren.map(category => {
return (
<li
key={category._id}
data-id={category._id}
onClick={this.clickHandler.bind(this)}
>
{category.nombre}
</li>
);
})}
</ul>
</section>
);
}
}
I could easily go about this without RxJS and just check the array myself and use a small debounce and what not, but I chose to go this way because I actually want to try to understand it and then be able to use it on bigger scenarios. However, I must admit I'm way lost about the best approach. There are so many methods and different things involved with this (both the pattern and the library) and I'm just kind of stuck here.
Anyways, any and all help (as well as general comments about how to improve this code) are welcome. Thanks in advance!
---------------------------------UPDATE---------------------------------
I have implemented a part of Mark's suggestion into my code, but this still presents two problems:
1- I'm still not sure as to how to filter the results so that the array will only hold IDs for the buttons that are clicked (and active). So, in other words, these would be the actions:
Click a button once -> have its ID go into array
Click same button again (it could be immediately after the first
click or at any other time) -> remove its ID from array.
This has to work in order to actually send the array with the correct filters via ajax. Now, I'm not even sure that this is a possible operation with RxJS, but one can dream... (Also, I'm willing to bet that it is).
2- Perhaps this is an even bigger issue: how can I actually maintain this array while I'm on this view. I'm guessing I could use React's local state for this, just don't know how to do it with RxJS. Because as it currently is, the buffer returns only the button/s that has/have been clicked before the debounce time is over, which means that it "creates" a new array each time. This is clearly not the right behavior. It should always point to an existing array and filter and work with it.
Here's the current code:
import React, { Component } from 'react';
import * as Rx from 'rx';
export default class CategoryFilter extends Component {
constructor(props) {
super(props);
this.state = {
arr: []
}
this.click = new Rx.Subject();
this.click
.buffer(this.click.debounce(2000))
.subscribe(
click => console.log('click', click),
e => console.log(`error ---> ${e}`),
() => console.log('completed')
);
}
search(id) {
console.log('search --> ', id);
// this.props.onSearch({ search });
}
clickHandler(e) {
this.click.onNext(e.target.dataset.id);
}
render() {
return (
<section>
<ul>
{this.props.categoriesChildren.map(category => {
return (
<li
key={category._id}
data-id={category._id}
onClick={this.clickHandler.bind(this)}
>
{category.nombre}
</li>
);
})}
</ul>
</section>
);
}
}
Thanks, all, again!
Make your filter items an Observable streams of click events using Rx.Observable.fromevent (see https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/events.md#converting-a-dom-event-to-a-rxjs-observable-sequence) - it understands a multi-element selector for the click handling.
You want to keep receiving click events until a debounce has been hit (user has enabled/disabled all filters she wants to use). You can use the Buffer operator for this with a closingSelector which needs to emit a value when to close the buffer and emit the buffered values.
But leaves the issue how to know the current actual state.
UPDATE
It seems to be far easier to use the .scan operator to create your filterState array and debounce these.
const sources = document.querySelectorAll('input[type=checkbox]');
const clicksStream = Rx.Observable.fromEvent(sources, 'click')
.map(evt => ({
name: evt.target.name,
enabled: evt.target.checked
}));
const filterStatesStream = clicksStream.scan((acc, curr) => {
acc[curr.name] = curr.enabled;
return acc
}, {})
.debounce(5 * 1000)
filterStatesStream.subscribe(currentFilterState => console.log('time to do something with the current filter state: ', currentFilterState);
(https://jsfiddle.net/crunchie84/n1x06016/6/)
Actually, your problem is about RxJS, not React itself. So it is easy. Suppose you have two function:
const removeTag = tagName =>
tags => {
const index = tags.indexOf(index)
if (index !== -1)
return tags
else
return tags.splice(index, 1, 0)
}
const addTag = tagName =>
tags => {
const index = tags.indexOf(index)
if (index !== -1)
return tags.push(tagName)
else
return tags
}
Then you can either using scan:
const modifyTags$ = new Subject()
modifyTags$.pipe(
scan((tags, action) => action(tags), [])
).subscribe(tags => sendRequest(tags))
modifyTags$.next(addTag('a'))
modifyTags$.next(addTag('b'))
modifyTags$.next(removeTag('a'))
Or having a separate object for tags:
const tags$ = new BehaviorSubject([])
const modifyTags$ = new Subject()
tags$.pipe(
switchMap(
tags => modifyTags$.pipe(
map(action => action(tags))
)
)
).subscribe(tags$)
tags$.subscribe(tags => sendRequest(tags))
I'm building an app that lets you organize emojis via drag and drop. I have a method that runs on drop that sorts a large array of emoji objects. When I run setState and commit the object back into a parent component's state, React removes the parent component from the DOM. It just disappears... There are no errors in the console and using react dev tools i can see that the components state has in fact been updated with the new object. Any help would be much appreciated.
This sorting mechanism sends the updated object to it's parent components via props:
endDrag(props, monitor, dragComponent) {
if (monitor.didDrop()) {
let sourceEmoji = dragComponent.props.emoji
let targetEmoji = monitor.getDropResult().props.emoji
const updatedCategorizedEmojis = props.categorizedEmojis.map((category)=> {
return category.emojis.map((emoji, idx)=> {
if (emoji.id == sourceEmoji.id) {
emoji['newPosition'] = (targetEmoji['newPosition'] - 0.5)
return emoji
} else {
return emoji
}
}).sort((a,b)=> {
if (a['newPosition'] > b['newPosition']) {
return 1
}
if (a['newPosition'] < b['newPosition']) {
return -1
}
}).map((emoji, idx)=> {
emoji['newPosition'] = idx
return emoji
})
})
props.updateSortedEmojis(updatedCategorizedEmojis)
}
}
The array of objects looks like this:
That whole thing gets added to the parent's state and when it does the component gets removed without errors.
Any info would be greatly appreciated, thanks!