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?
Related
i'm working on a simple note-taking app for my portfolio using JS and Firebase. Before i tell you what's happening i feel like i need to show you how my code works, if you have any tips and concerns please tell me as it would be GREATLY appreciated. That being said, let's have a look "together". I'm using this class to create the notes:
const htmlElements = [document.querySelector('.notes'), document.querySelector('.note')];
const [notesDiv, noteDiv] = htmlElements;
class CreateNote {
constructor(title, body) {
this.title = title;
this.body = body;
this.render = () => {
const div1 = document.createElement('div');
div1.className = 'notes-prev-container';
div1.addEventListener('click', () => { this.clickHandler(this) });
const div2 = document.createElement('div');
div2.className = 'notes-prev';
const hr = document.createElement('hr');
hr.className = 'notes__line';
// Nest 'div2' inside 'div1'
div1.appendChild(div2);
div1.appendChild(hr);
/*
Create Paragraph 1 & 2 and give them the same
class name and some text
*/
const p1 = document.createElement('p');
p1.className = 'notes-prev__title';
p1.innerText = this.title;
const p2 = document.createElement('p');
p2.className = 'notes-prev__body';
p2.innerText = this.body;
// Nest p 1 & 2 inside 'div2'
div2.appendChild(p1);
div2.appendChild(p2);
// Finally, render the div to its root tag
notesDiv.appendChild(div1);
}
}
/*
Every time this method is called, it creates 2 textareas,
one for the note title and the other for its body then it
appends it to the DOM.
*/
renderNoteContent () {
const title = document.createElement('textarea');
title.placeholder = 'Title';
title.value = this.title;
title.className = 'note__title';
const body = document.createElement('textarea');
body.placeholder = 'Body';
body.value = this.body;
body.className = 'note__body';
noteDiv.appendChild(title);
noteDiv.appendChild(body);
}
/*
When this method is called, it checks to see if there's a
note rendered already (childElementCount === 1 because there's a
button, so if there's only this button it means there's no
textareas rendered).
If yes, then merely call the renderNoteContent method. Else
get the tags with the classes 'note__title' and 'note__body'
and remove them from the DOM, then call renderNoteContent to
create the textareas with the clicked notes values.
This function gets mentioned at line 19.
*/
clickHandler(thisClass) {
if (noteDiv.childElementCount === 1) {
thisClass.renderNoteContent();
} else {
document.querySelector('.note__title').remove();
document.querySelector('.note__body').remove();
thisClass.renderNoteContent();
}
}
}
Now i need 2 buttons, createNotesButton and saveNotesButton respectively. These 2 buttons must be inside a function that will be called inside .onAuthStateChanged (why? because they will be needing access to the currentUser on firebase auth).
I want the createNotesButton to create a note prototype, render it to the DOM and create a new document on firestore, where this note contents will be stored. Here's how i did it:
PS: I feel like i'm not using this class correctly, so again if you have any tips i appreciate it.
import {db} from '../../firebase_variables/firebase-variables.js';
import {CreateNote} from '../create_notes_class/create_notes_class.js';
const htmlElements = [
document.querySelector('.createNotes-button'),
document.querySelector('.saveNotes-button')
];
const [createNotesButton, saveNotesButton] = htmlElements;
function clickHandler(user) {
/*
1. Creates a class.
2. Creates a new document on firebase with the class's empty value.
3. Renders the empty class to the DOM.
*/
createNotesButton.addEventListener('click', () => {
const note = new CreateNote('', '');
note.render();
// Each user has it's own note collection, said collection has their `uid` as name.
db.collection(`${user.uid}`).doc().set({
title: `${note.title}`,
body: `${note.body}`
})
})
}
Now i need a saveNotesButton, he's the one i'm having issues with. He needs to save the displayed note's content on firestore. Here's what i tried doing:
import {db} from '../../firebase_variables/firebase-variables.js';
import {CreateNote} from '../create_notes_class/create_notes_class.js';
const htmlElements = [
document.querySelector('.createNotes-button'),
document.querySelector('.saveNotes-button')
];
const [createNotesButton, saveNotesButton] = htmlElements;
function clickHandler(user) {
createNotesButton.addEventListener('click', () => {...})
/*
1. Creates 2 variables, `title` and `body, if there's not a note being displayed
their values will be null, which is why the rest of the code is inside an if
statement
2. If statement to check if there's a note being displayed, if yes then:
1. Call the user's note collection. Any document who has the title field equal to the
displayed note's value gets returned as a promise.
2. Then call an specific user document and update the fields `title` and `body` with
the displayed note's values.
3. If no then do nothing.
*/
saveNotesButton.addEventListener('click', () => {
const title = document.querySelector('.note__title');
const body = document.querySelector('.note__body');
db.collection(`${user.uid}`).where('title', '==', `${title.value}`)
.get()
.then(userCollection => {
db.collection(`${user.uid}`).doc(`${userCollection.docs[0].id}`).update({
title: `${title.value}`,
body: `${body.value}`
})
})
.catch(error => {
console.log('Error getting documents: ', error);
});
});
}
This didn't work because i'm using title.value as a query, so if i change it's value it will also change the queries direction to a path that doesn't exist.
So here's the question: how can i make it so the saveNotesButton does its job? I was thinking of adding another field to each note, something that won't change so i can easily identify and edit each note. Again, if there's something in my code that you think can or should be formatted please let me know, i'm using this project as a way to solidify my native JS knowledge so please be patient. I feel like if i had used React i would've finished this sometime ago but definitely wouldn't have learned as much, anyway thanks for your help in advance.
I was thinking of adding another field to each note, something that won't change so i can easily identify and edit each note.
Yes, you absolutely need an immutable identifier for each note document in the firestore so you can unambiguously reference it. You almost always want this whenever you're storing a data object, in any application with any database.
But, the firestore already does this for you: after calling db.collection(user.uid).doc() you should get a doc with an ID. That's the ID you want to use when updating the note.
The part of your code that interacts with the DOM will need to keep track of this. I suggest moving the code the creates the firestore document into the constructor of CreateNote and storing it on this. You'll need the user id there as well.
constructor(title, body, userId) {
this.title = title;
this.body = body;
const docRef = db.collection(userId).doc();
this.docId = docRef.id;
/* etc. */
Then any time you have an instance of CreateNote, you'll know the right user and document to reference.
Other suggestions (since you asked)
Use JsPrettier. It's worth the setup, you'll never go back.
Use HTML semantics correctly. Divs shouldn't be appended as children of hrs, because they're for "a thematic break between paragraph-level elements: for example, a change of scene in a story, or a shift of topic within a section." MDN
For your next project, use a framework. Essentially no one hand-codes event listeners and appends children to get things done. I see the value for basic understanding, but there's a rich and beautiful world of frameworks out there; don't limit yourself by avoiding them :-)
//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]
})
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 :)
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 would like to ask what is the correct way to fast render > 10000 items in React.
Suppose I want to make a checkboxList which contain over dynamic 10000 checkbox items.
I make a store which contain all the items and it will be used as state of checkbox list.
When I click on any checkbox item, it will update the corresponding item by action and so the store is changed.
Since store is changed so it trigger the checkbox list update.
The checkbox list update its state and render again.
The problem here is if I click on any checkbox item, I have to wait > 3 seconds to see the checkbox is ticked. I don't expect this as only 1 checkbox item need to be re-rendered.
I try to find the root cause. The most time-consuming part is inside the checkbox list render method, related to .map which create the Checkbox component to form componentList.. But actually only 1 checkbox have to re-render.
The following is my codes.
I use ReFlux for the flux architecture.
CheckboxListStore
The Store store all the checkbox item as map. (name as key, state (true/false) as value)
const Reflux = require('reflux');
const Immutable = require('immutable');
const checkboxListAction = require('./CheckboxListAction');
let storage = Immutable.OrderedMap();
const CheckboxListStore = Reflux.createStore({
listenables: checkboxListAction,
onCreate: function (name) {
if (!storage.has(name)) {
storage = storage.set(name, false);
this.trigger(storage);
}
},
onCheck: function (name) {
if (storage.has(name)) {
storage = storage.set(name, true);
this.trigger(storage);
}
},
onUncheck: function (name) {
if (storage.has(name)) {
storage = storage.set(name, false);
this.trigger(storage);
}
},
getStorage: function () {
return storage;
}
});
module.exports = CheckboxListStore;
CheckboxListAction
The action, create, check and uncheck any checkbox item with name provided.
const Reflux = require('reflux');
const CheckboxListAction = Reflux.createActions([
'create',
'check',
'uncheck'
]);
module.exports = CheckboxListAction;
CheckboxList
const React = require('react');
const Reflux = require('reflux');
const $ = require('jquery');
const CheckboxItem = require('./CheckboxItem');
const checkboxListAction = require('./CheckboxListAction');
const checkboxListStore = require('./CheckboxListStore');
const CheckboxList = React.createClass({
mixins: [Reflux.listenTo(checkboxListStore, 'onStoreChange')],
getInitialState: function () {
return {
storage: checkboxListStore.getStorage()
};
},
render: function () {
const {storage} = this.state;
const LiComponents = storage.map((state, name) => {
return (
<li key = {name}>
<CheckboxItem name = {name} />
</li>
);
}).toArray();
return (
<div className = 'checkbox-list'>
<div>
CheckBox List
</div>
<ul>
{LiComponents}
</ul>
</div>
);
},
onStoreChange: function (storage) {
this.setState({storage: storage});
}
});
module.exports = CheckboxList;
CheckboxItem
Inside onChange callback, I call the action to update the item.
const React = require('react');
const Reflux = require('reflux');
const $ = require('jquery');
const checkboxListAction = require('./CheckboxListAction');
const checkboxListStore = require('./CheckboxListStore');
const CheckboxItem = React.createClass({
mixins: [Reflux.listenTo(checkboxListStore, 'onStoreChange')],
propTypes: {
name: React.PropTypes.string.isRequired
},
getInitialState: function () {
const {name} = this.props;
return {
checked: checkboxListStore.getStorage().get(name)
};
},
onStoreChange: function (storage) {
const {name} = this.props;
this.setState({
checked: storage.get(name)
});
},
render: function () {
const {name} = this.props;
const {checked} = this.state;
return (
<div className = 'checkbox' style = {{background: checked ? 'green' : 'white'}} >
<span>{name}</span>
<input ref = 'checkboxElement' type = 'checkbox'
onChange = {this.handleChange}
checked = {checked}/>
</div>
);
},
handleChange: function () {
const {name} = this.props;
const checked = $(this.refs.checkboxElement).is(':checked');
if (checked) {
checkboxListAction.check(name);
} else {
checkboxListAction.uncheck(name);
}
}
});
module.exports = CheckboxItem;
There are a few approaches you can take:
Don't render all 10,000 - just render the visible check boxes (+ a few more) based on panel size and scroll position, and handle scroll events to update the visible subset (use component state for this, rather than flux). You'll need to handle the scroll bar in some way, either by rendering one manually, or easier by using the normal browser scroll bar by adding huge empty divs at the top and bottom to replace the checkboxes you aren't rendering, so that the scroll bar sits at the correct position. This approach allows you to handle 100,000 checkboxes or even a million, and the first render is fast as well as updates. Probably the preferred solution. There are lots of examples of this kind of approach here: http://react.rocks/tag/InfiniteScroll
Micro-optimize - you could do storage.toArray().map(...) (so that you aren't creating an intermediate map), or even better, make and empty array and then do storage.forEach(...) adding the elements with push - much faster. But the React diffing algorithm is still going to have to diff 10000 elements, which is never going to be fast, however fast you make the code that generates the elements.
Split your huge Map into chunks in some way, so that only 1 chunk changes when you check a chechbox. Also split up the React components in the same way (into CheckboxListChunks) or similar. This way, you'll only need to re-render the changed chunk, as long as you have a PureComponent type componentShouldUpdate function for each chunk (possibly Reflux does this for you?).
Move away from ImmutableJS-based flux, so you have better control over what changes when (e.g. you don't have to update the parent checkbox map just because one of the children has changed).
Add a custom shouldComponentUpdate to CheckboxList:
shouldComponentUpdate:function(nextProps, nextState) {
var storage = this.state.storage;
var nextStorage = nextState.storage;
if (storage.size !== nextStorage.size) return true;
// check item names match for each index:
return !storage.keySeq().equals(nextStorage.keySeq());
}
Beyond the initial render, you can significantly increase rendering speed of large collections by using Mobservable. It avoids re-rendering the parent component that maps over the 10.000 items unnecessarily when a child changes by automatically applying side-ways loading. See this blog for an in-depth explanation.
Btw, I give up flux...
I finally decided to use mobservable to solve my problem.
I have made an example https://github.com/raymondsze/react-example
see the https://github.com/raymondsze/react-example/tree/master/src/mobservable for the coding.
Your render function looks somewhat more complicated then it needs to be:
it first generates an array of JSX components
then converts applies a (jQuery?) .toArray()
then returns this newly generated array.
Maybe simplifying your render function to something like this would help?
render: function () {
return (
<div className = 'checkbox-list'>
<div>
CheckBox List
</div>
<ul>
{this.state.storage.map((state, name) => {
return (
<li key = {name}>
<CheckboxItem name = {name} />
</li>
);
})}
</ul>
</div>
);
},
Do you really need to save the check status in you store every time check/uncheck?
I recently meet similar problem like you. Just save a checkedList array [name1,name2 ...] in the CheckboxList component's state, and change this checkedList every time you check/uncheck an item. When you want to save check status to data storage, call an Action.save() and pass the checkedList to store.
But if you really need to save to data storage every time check/uncheck, this solution won't help -_-.