How to generate the customized key in slatejs? - reactjs

I'm trying to make WYSIWYG editor that it is possible to annotate about selected text.
Firstly, I used Draft.js. However, it was not suitable for pointing the annotated text using key because entity key of Draft.js was initiated when selections were duplicated.
So, I found the slatejs while I searched other libraries related this stuff.
The slatejs had 'setKeyGenerator' utils. However, I couldn't find any information about 'setKeyGenerator' of slatejs. This util is just setting function like below.
function setKeyGenerator(func) {
generate = func;
}
And I didn't know how to generate key using this function.
Then, Anyone know how to use this function or have any idea for annotation selected text?

If you're trying to generate a key to reference an element (block) by, here's what you can do:
// A key to reference to block by (you should make it more unique than `Math.random()`)
var uniqueKey = Math.random();
// Insert a block with a unique key
var newState = this.state
.transform()
.insertBlock({
type: 'some-block-type',
data: {
uniqueKey: uniqueKey
},
})
.apply();
// Get the block's unique Slate key (used internally)
var blockKey;
var { document } = self.state;
document.nodes.some(function(node) {
if (node.data.get('uniqueKey') == uniqueKey) {
blockKey = node.key;
}
});
// Update data on the block, using it's key to find it.
newState = newState
.transform()
.setNodeByKey(blockKey, {
data: {
// Define any data parameters you want attached to the block.
someNewKey: 'some new value!'
},
})
.apply();
That will allow you to set a unique key on an insert block, and then get the block's actual SlateJs key and update the block with it.

Slate provides a KeyUtils.setGenerator(myKeygenFunction) to pass our own key generator. This gives us the opportunity to create truly unique keys across Editor instances.
In the parent that imports this component, pass a different idFromParentIteration prop for each instance of PlainText component and you should be good.
Like so:
['first-editor', 'second-editor'].map((name, idx) => <PlainText idFromParentIteration={name + idx} />)
And here's a complete example with a custom key generator.
import React from "react";
import Plain from "slate-plain-serializer";
import { KeyUtils } from 'slate';
import { Editor } from "slate-react";
const initialValue = Plain.deserialize(
"This is editable plain text, just like a <textarea>!"
);
class PlainText extends React.Component {
constructor(props) {
super(props);
let key = 0;
const keygen = () => {
key += 1;
return props.idFromParentIteration + key; // custom keys
};
KeyUtils.setGenerator(keygen);
}
render() {
return (
<Editor
placeholder="Enter some plain text..."
defaultValue={initialValue}
/>
);
}
}
export default PlainText;

Related

Firestore navigation for onUpdate and shuffle an array

I have this Firebase structure:
Firebase Structure.
Then I have a function in my Code, which adds a map called "set".
My Structure is looking like this after: New structure.
Now i want an onUpdate Firebase function, which is called after the map "set" is added in any document.
This function should shuffle my "question" array.
I tried something like this:
exports.shuffleSet = functions.firestore
.document('duell/{duell_id}/set/questions')
.onUpdate((change, context) => {
const data = change.after.data();
const previousData = change.before.data();
if (data.name == previousData.name) {
return null;
}
//shuffle code here
});
But Im not sure if .document('duell/{duell_id}/set/questions') is the correct way to navigate to the question array. And at the beginning the "set" is not even existing as explained at the top.
How do I navigate to the question array correctly, that I can pull it & update it shuffled?
You should pass a document path to functions.firestore.document(). You cannot pass a field name, since Firestore Cloud Functions are triggered by documents events.
So you should do as follows:
exports.shuffleSet = functions.firestore
.document('duell/{duell_id}')
.onUpdate((change, context) => {
if (!change.after.data().shuffledSet) {
const data = change.after.data();
const question = data.set.question; // get the value of the question field
const shuffledSet = shuffle(question); // generate the new, suffled set. It’s up to you to write this function
return change.after.ref.update({shuffledSet});
} else {
return null; // Nothing to do, the shuffled field is already calculated
}
});

Wordpress Gutenberg PluginDocumentSettingPanel not working with controls?

I want to add a custom meta field to the gutenberg document panel and used this doc. For the custom meta field I used this tutorial.
The problem I have occurs when trying to put them together.
Here is my code so far:
const { __ } = wp.i18n;
const { registerBlockType } = wp.blocks;
const { InspectorControls } = wp.editor;
const { registerPlugin } = wp.plugins
const { PluginDocumentSettingPanel } = wp.editPost
const { PanelBody, PanelRow, TextControl } = wp.components
const PluginDocumentSettingPanelDemo = () => (
<PluginDocumentSettingPanel
name="custom-panel"
title="Custom Panel"
className="custom-panel"
>
<TextControl
value={wp.data.select('core/editor').getEditedPostAttribute('meta')['_myprefix_text_metafield']}
label={ "Text Meta" }
onChange={(value) => wp.data.dispatch('core/editor').editPost({meta: {_myprefix_text_metafield: value}})}
/>
</PluginDocumentSettingPanel>
)
registerPlugin('plugin-document-setting-panel-demo', {
render: PluginDocumentSettingPanelDemo
})
Edit: Thanks to Ivan I solved this side issue :)
My Sidebar looks okay at first:
But when I try to change the inputs value it isn't updated (but the storage in wp.data is). I can't delete it, too. It stays at it's initial value. When I remove the part where I set the initial value it works like it should be but since I need the initial value this isn't an option for me ;)
Here an example log from the console when I add an "x" to the end of the input (as mentioned above the text in the input itself doesn't change)
Does anyone know how to make the input field working properly?
First of all, make sure you have https://wordpress.org/plugins/gutenberg/ plugin installed, because PluginDocumentSettingPanel is not fully implemented in core WP yet. It should be for 5.3 version, as per these tweets.
Second, you don't need the interval function for the wp.plugins. The reason it is undefined at first is that WordPress doesn't know that you need the wp-plugins loaded first. From the WordPress documentation
If you wanted to use the PlainText component from the editor module, first you would specify wp-editor as a dependency when you enqueue your script
The same applies for all other modules (read scripts, like 'wp-plugins').
You have to add the 'wp-plugins' script as a dependency, when registering your js plugin script:
<?php
/*
Plugin Name: Sidebar plugin
*/
function sidebar_plugin_register() {
wp_register_script(
'plugin-sidebar-js',
plugins_url( 'plugin-sidebar.js', __FILE__ ),
array( 'wp-plugins', 'wp-edit-post', 'wp-element' ) // <== the dependencies array is important!
);
}
add_action( 'init', 'sidebar_plugin_register' );
function sidebar_plugin_script_enqueue() {
wp_enqueue_script( 'plugin-sidebar-js' );
}
add_action( 'enqueue_block_editor_assets', 'sidebar_plugin_script_enqueue' );
The PHP above is taken from the official WP documentation.
I would also suggest reading thoroughly this awesome tutorial from Css Tricks. It goes in depth about setting up an ESNext environment with only the #wordpress/scripts package. It goes over the dependencies, adding meta fields and much more :) I hope this helps!
--------------- Initial answer ends here ---------------
Edit: After testing the code from the author, I found out a couple of issues. First of all, there was a missing closing tag for the TextControl. Second, I added Higher order components from the wp-data package, which I then used to "enhance" the TextControl, so that it doesn't manipulate or read data directly, but abstract that logic into it's higher order components. The code looks like so:
const { __ } = wp.i18n;
const { registerPlugin } = wp.plugins;
const { PluginDocumentSettingPanel } = wp.editPost;
const { TextControl } = wp.components;
const { withSelect, withDispatch, dispatch, select } = wp.data;
// All the necessary code is pulled from the wp global variable,
// so you don't have to install anything
// import { withSelect, withDispatch, dispatch, select } from "#wordpress/data";
// !!! You should install all the packages locally,
// so your editor could access the files so you could
// look up the functions and classes directly.
// It will not add to the final bundle if you
// run it through wp-scripts. If not, you can
// still use the wp global variable, like you have done so far.
let TextController = props => (
<TextControl
value={props.text_metafield}
label={__("Text Meta", "textdomain")}
onChange={(value) => props.onMetaFieldChange(value)}
/>
);
TextController = withSelect(
(select) => {
return {
text_metafield: select('core/editor').getEditedPostAttribute('meta')['_myprefix_text_metafield']
}
}
)(TextController);
TextController = withDispatch(
(dispatch) => {
return {
onMetaFieldChange: (value) => {
dispatch('core/editor').editPost({ meta: { _myprefix_text_metafield: value } })
}
}
}
)(TextController);
const PluginDocumentSettingPanelDemo = () => {
// Check if a value has been set
// This is for editing a post, because you don't want to override it everytime
if (!select('core/editor').getEditedPostAttribute('meta')['_myprefix_text_metafield']) {
// Set initial value
dispatch('core/editor').editPost({ meta: { _myprefix_text_metafield: 'Your custom value' } });
}
return (
<PluginDocumentSettingPanel
name="custom-panel"
title="Custom Panel"
className="custom-panel"
>
<TextController />
</PluginDocumentSettingPanel>
)
};
registerPlugin('plugin-document-setting-panel-demo', {
render: PluginDocumentSettingPanelDemo
})
Since the meta field is registered with an underscore as a first symbol in the name, WordPress will not allow you to save it, because it treats it as a private value, so you need to add extra code, when registering the field:
function myprefix_register_meta()
{
register_post_meta('post', '_myprefix_text_metafield', array(
'show_in_rest' => true,
'type' => 'string',
'single' => true,
'sanitize_callback' => 'sanitize_text_field',
'auth_callback' => function () {
return current_user_can('edit_posts');
}
));
}
add_action('init', 'myprefix_register_meta');
Again, all of this is explained in the Css tricks tutorial.
I had the same problem - values were not being updated in the field and in the database - and, after some research, I have found that the reason for this is that you should add 'custom-fields' to the 'supports' array in your call to register_post_type(), like this:
register_post_type(
'my_post_type_slug',
array(
...
'supports' => array( 'title', 'editor', 'custom-fields' ),
...
)
);
You can test that this works by calling wp.data.select( 'core/editor' ).getCurrentPost().meta in your JavaScript console, when the block editor is loaded. If your post type does not add support for 'custom-fields', this call will return undefined; if it does, it will return an empty array (or rather, an array with the already existing meta from the database). This behavior is mentioned in the Gutenberg docs, in a note on registering your post meta:
To make sure the field has been loaded, query the block editor internal data structures, also known as stores. Open your browser’s console, and execute this piece of code:
wp.data.select( 'core/editor' ).getCurrentPost().meta;
Before adding the register_post_meta function to the plugin, this code returns a void array, because WordPress hasn’t been told to load any meta field yet. After registering the field, the same code will return an object containing the registered meta field you registered.
I worked on a similar implementation recently, and worked through a bunch of examples as well. Between the above-mentioned articles, and this great series by one of the Automattic devs, I got a working version of the above example using the newer useSelect and useDispatch custom hooks. It's really quite similar, but utilizes custom hooks from React 16.8 for a slightly more concise dev experience:
(Also, using #wordpress/scripts, so the imports are from the npm packages instead of the wp object directly, but either would work.)
import { __ } from '#wordpress/i18n';
import { useSelect, useDispatch } from '#wordpress/data';
import { PluginDocumentSettingPanel } from '#wordpress/edit-post';
import { TextControl } from '#wordpress/components';
const TextController = (props) => {
const meta = useSelect(
(select) =>
select('core/editor').getEditedPostAttribute('meta')['_myprefix_text_metafield']
);
const { editPost } = useDispatch('core/editor');
return (
<TextControl
label={__("Text Meta", "textdomain")}
value={meta}
onChange={(value) => editPost({ meta: { _myprefix_text_metafield: value } })}
/>
);
};
const PluginDocumentSettingPanelDemo = () => {
return (
<PluginDocumentSettingPanel
name="custom-panel"
title="Custom Panel"
className="custom-panel"
>
<TextController />
</PluginDocumentSettingPanel>
);
};
export default PluginDocumentSettingPanelDemo;
Hopefully that helps someone else searching.

List of child components not updating correctly when deleting object from state

I'm working on a dynamic form where we can have a variable amount of facilities. I want to be able to remove specific facilities as needed, but I'm having trouble with making the delete work properly. If I have three facilities and delete the second one, the page always appears like I deleted the last one, even though the state correctly reflects the deletion.
I've tried looking into how rendering and updating works, but I'm rather at a loss at how React does this entire process. It correctly renders that a deletion occurs, but it doesn't update the props of the child components correctly (I believe).
class Request extends React.Component {
constructor(props) {
super(props);
this.state = {
...
facilities: [],
...
}
this.removeFacility = this.removeFacility.bind(this);
}
removeFacility = (number) => (event) => {
var newArray = [...this.state.facilities];
var index = -1;
for (var i = 0; i < newArray.length; i++) {
if (newArray[i].number === number) {
index = i;
}
}
if (index !== -1) {
newArray.splice(index, 1);
} else {
return;
}
for (var i = 0; i < newArray.length; i++) {
newArray[i].number = i + 1;
}
this.setState({facilities: newArray});
}
render() {
const { classes } = this.props;
return (
...
<div>
{this.state.facilities.map(function(f, idx) {
return (
<FacilityRequest
key={idx}
facility={f}
removeFacility={this.removeFacility.bind(this)}/>
)
}, this)}
</div>
...
);
}
}
class FacilityRequest extends React.Component {
...
render() {
const { classes, key, facility, removeFacility} = this.props;
return (
<Paper>
...
{facility.number}
<Button onClick={removeFacility(facility.number)}>Delete</Button>
...
Various form fields
...
</Paper>
);
}
}
When I click delete for a specific facility, the state correctly deletes the facility I want gone, but the last facility is the one that is removed (the other fields in the form remain the same as the one that was supposed to be removed, not matching the state.)
This is probably because you're using the array index as a key to the components you're rendering.
The short explanation is that React uses keys to determine when to re-render mapped components. By using index as a key, and you remove the item at index 4, React will not see that index 4 was deleted, but that it was modified, since something else took its place. This can have some strange side-effects, such as what you're seeing.
You're better off providing a unique identifier to key, such that it identifies the actual element and its content. In this case, facility.number could be a good candidate:
{this.state.facilities.map(function(f, idx) {
return (
<FacilityRequest
key={f.number}
facility={f}
removeFacility={this.removeFacility.bind(this)}/>
)
}, this)}
The best key to use will depend on your implementation, of course.
Using an index as a component key should generally be a last resort.

Displaying data from Firebase in React without arrays

I am new to both React and Firebase. I struggled a bit to get data from the database, even though the instructions on the Firebase website were pretty straightforward.
I managed to print data in the view by using this code:
Get data from DB and save it in state:
INSTRUMENTS_DB.once('value').then(function(snapshot) {
this.state.instruments.push(snapshot.val());
this.setState({
instruments: this.state.instruments
});
From Firebase, I receive and Object containing several objects, which correspond to the differen instruments, like shown in the following snippet:
Object {
Object {
name: "Electric guitar",
image: "img/guitar.svg"
}
Object {
name: "Bass guitar",
image: "img/bass.svg"
}
// and so on..
}
Currently, I print data by populating an array like this:
var rows = [];
for (var obj in this.state.instruments[0]) {
rows.push(<Instrument name={this.state.instruments[0][obj].name}
image={this.state.instruments[0][obj].image}/>);
}
I feel like there's a better way to do it, can somedody give a hint? Thanks
I user firebase a lot and mu solution is little ES6 helper function
const toArray = function (firebaseObj) {
return Object.keys(firebaseObj).map((key)=> {
return Object.assign(firebaseObj[key], {key});
})
};
I also assign the firebase key to object key property, so later I can work with the keys.
The native map function only works for arrays, so using directly it on this object won't work.
What you can do instead is:
Call the map function on the keys of your object using Object.keys():
getInstrumentRows() {
const instruments = this.state.instruments;
Object.keys(instruments).map((key, index) => {
let instrument = instruments[key];
// You can now use instrument.name and instrument.image
return <Instrument name={instrument.name} image={instrument.image}/>
});
}
Alternatively, you can also import the lodash library and use its map method which would allow you to refactor the above code into:
getInstrumentRowsUsingLodash() {
const instruments = this.state.instruments;
_.map(instruments, (key, index) => {
let instrument = instruments[key];
// You can now use instrument.name and instrument.image
return <Instrument name={instrument.name} image={instrument.image}/>
});
}
Side note:
When you retrieve you data from Firebase you attempt to update the state directly with a call on this.state.instruments. The state in React should be treated as Immutable and should not be mutated with direct calls to it like push.
I would use map function:
_getInstrumentRows() {
const instruments = this.state.instruments[0];
if (instruments) {
return instruments.map((instrument) =>
<Instrument name={instrument.name}
image={instrument.image}/>);
}
}
In your render() method you just use {_getInstrumentRows()} wherever you need it.

ReactJS parent/child list items not rendering properly after an item is removed

Example: https://jsfiddle.net/wbellman/ghuw2ers/6/
In an application I am working on, I have a parent container (List, in my example) that contains a list of children (Hero, in my example). The list is governed by an outside object. For simplicity I declared the object directly in the JS. (In my real application the data store is properly namespaced and so forth.)
The problem I have is in the list I have three elements, if I remove an item from the middle, the rendered list appears to remove the last element. However the outside object reflects the proper list.
For example:
My list has the elements: cap, thor, hulk
If you remove "thor", "cap" and "thor" are rendered
The heroList reflects "cap" and "hulk" as it should
I am relatively new to ReactJs, so there is a good chance my premise is fundamentally flawed.
Note: The example reflects a much more complex application. It's structured similarly for purposes of demonstration. I am aware you could make a single component, but it would not be practical in the actual app.
Any help would be appreciated.
Here is the code from JSFiddle:
var heroList = [
{ name: "cap" },
{ name: "thor"},
{ name: "hulk"}
];
var List = React.createClass({
getInitialState() {
console.log("heros", heroList);
return {
heros: heroList
};
},
onChange(e){
this.setState({heros: heroList});
},
removeHero(i,heros){
var hero = heros[i];
console.log("removing hero...", hero);
heroList = _.filter(heroList, function(h){ return h.name !== hero.name;});
this.setState({heros:heroList});
},
render() {
var heros = this.state.heros;
var createHero = (hero,index) => {
return <Hero hero={hero} key={index} onRemove={this.removeHero.bind(this,index,heros)}/>;
};
console.log("list", heros);
return (
<ul>
{heros.map(createHero)}
</ul>
)
}
});
var Hero = React.createClass({
getInitialState() {
return {
hero: this.props.hero
}
},
render() {
var hero = this.state.hero;
return (
<li>Hello {hero.name} | <button type="button" onClick={this.props.onRemove}>-</button></li>
);
}
});
ReactDOM.render(
<List />,
document.getElementById('container')
);
Additional: I was having problems copying the code from JSFiddle, anything I broke by accident should work in the JSFiddle listed at the top of this question.
Edit:
Based on the commentary from madox2, nicole, nuway and Damien Leroux, here's what I ended up doing:
https://jsfiddle.net/wbellman/ghuw2ers/10/
I wish there was a way to give everyone credit, you were all a big help.
Changing your Hero class to this fixed the issue of displaying the wrong hero name for me:
var Hero = React.createClass({
render() {
return (
<li>Hello {this.props.hero.name} | <button type="button" onClick={this.props.onRemove}>-</button></li>
);
}
});
i.e. I removed the local state from the class and used the prop directly.
Generally speaking, try to use the local store only when you really need it. Try to think of your components as stateless, i.e. they get something through the props and display it, that's it.
Along these lines, you should consider passing the hero list through the props to your List component as well.
if you really have problems with managing your data you should use Flux or Redux.
in this code:
heroList = _.filter(heroList, function(h){ return h.name !== hero.name;});
i just dont get why you filer the heroList instead of this.state.heros? every time you add or remove a hero, the heroList in your current scope shouldnt be kept in state? the global heroList is just the initial state.
The problem is with the keys used. Since the key is taken from the index, that key has already been used and thus the hero with that key is shown.
change it to key={Math.random() * 100} and it will work

Resources