facebookarchive / draft-js

A React framework for building text editors.
https://draftjs.org/
MIT License
22.57k stars 2.63k forks source link

Questions about the 'atomic' block type #543

Open mjomble opened 8 years ago

mjomble commented 8 years ago

I'm a bit confused about the 'atomic' block type.

It seems the documentation and examples suggest that:

So to generalize, I assume whenever you want a custom block type, you should use the name 'atomic'.

But what should I do if I want more than one custom block type? Use 'atomic' for both and add conditions to the render function for 'atomic'?

The most obvious solution to me would be to use new block type names for new block types. For example, the block type for a TeX editor could be 'tex' instead of 'atomic' and the one for media could be 'media' instead of 'atomic'.

But @hellendag wrote here https://github.com/facebook/draft-js/issues/248#issuecomment-207163406

I generally wouldn't advise using block types that are outside the EditorBlockType enum.

Could you elaborate more on this?

I also found that older versions of the documentation did suggest 'media' instead of 'atomic', until https://github.com/facebook/draft-js/pull/258

It does make sense that the Draft.js core should not contain a class named MediaUtils with code specific to the 'media' block type. But why should users of this library avoid defining their own 'media' block type?

mjomble commented 8 years ago

I may have just figured out a part of the answer on my own :)

I tried to implement a custom block and wanted to be able to remove the entire block by pressing backspace from the start of the next block. Instead of the custom block disappearing, the next one did and the selection got all messed up. After debugging for a while, I realised that the text of the next block was merged into the custom block's text and it seemed to disappear because the custom block did not display this text. However, in the media example, the custom block gets properly deleted in such a scenario. So eventually I discovered that there is a special backspace handler for the 'atomic' block type that does the magic in the media example.

Now knowing this, the 'atomic' type makes more sense (I even finally figured out why exactly it's called that) and it seems reasonable to have one custom renderer for 'atomic' which then renders different components based on some kind of additional data.

Though perhaps a different approach would be to support custom block type strings like 'media' and 'tex' and providing a different mechanism for marking them as atomic? e.g. type: 'media', atomic: true instead of type: 'atomic'? Could be something to consider.

Also, looking deeper into AtomicBlockUtils.insertAtomicBlock, the character parameter seems like a strange hack for non-textual custom blocks. The media example uses ' ' for it as a workaround because it can't be omitted. It seems like block level metadata would be a better option for atomic blocks than entities, so perhaps some tools for that could be added to AtomicBlockUtils. Or at least the character parameter could be made optional.

Losses commented 6 years ago

For 2018, if you want to add multi type of atomic block, we can:

  1. Define a blockRendererFn and let it render a custom component, like this:
const blockRendererFn = contentBlock => {
  if (contentBlock.getType() !== 'atomic') return null;

  const entityId = contentBlock.getEntityAt(0);

  return {
    component: AtomicBlock,
    editable: false,
  };
}
  1. Define an insertBlock method in your Editor component:
  insertBlock = (type, data) => {
    const {editorState} = this.state;
    const contentState = editorState.getCurrentContent(); 
    const contentStateWithEntity = contentState.createEntity(type, 'IMMUTABLE', data);

    const entityKey = contentStateWithEntity.getLastCreatedEntityKey();

    const newEditorState = EditorState.set(
      editorState,
      {currentContent: contentStateWithEntity},
    );

    this.setState({
      editorState: AtomicBlockUtils.insertAtomicBlock(newEditorState, entityKey, ' '),
    });
  }
  1. Define a new component, maybe it calls AtomicBlock or something else, and:
import React from 'react';

class AtomicBlock extends React.Component {
  render() {
    const {block, contentState} = this.props;
    const {foo} = this.props.blockProps;
    const entity = contentState.getEntity(block.getEntityAt(0));
    const data = entity.getData();
    const type = entity.getType();

    switch(type){
      /*You know what you should do here :D*/
    }
  }
}

export default AtomicBlock
  1. Finally, if you want to add some custom component into the editor, just call:
this.insertBlock('SomeTypeOfElement', {foo: 'bar', otaku: 'saiko'})

I dont know whether how i finish this task is reasonable, but FB didn't provided a better document.

leanazulyoro commented 5 years ago

@Losses basically what you're doing is creating a new atomic block, assigning an Entity to it, and let that entity hold some data. Then retrieve that entity, it's type and it's data to know how to render it.

Wouldn't it be better to just create a block with a custom "type", adding some metadata to it (though ContentBlock.getData()) and using that type and/or data (which may or may not contain a "type") to decide how to render it ?

I'm coming back to draft.js after some years of using it, and I'm sure some aspects of the API have changed. I have in the past used Atomic blocks and Entities to hold the type and data necessary to handle rendering.

I'm wondering if as of today (Jan 2019) using the own ContentBlock's metadata over an Entity is a better practice.

leanazulyoro commented 5 years ago

Did some further reading and found: https://github.com/facebook/draft-js/issues/129 and https://github.com/facebook/draft-js/pull/216. There we se when and why the "data" attribute for ContentBlock was added, and indeed seams to indicate that if you need to have a custom block that renders in it's entirety with a custom component, you should use the block's metadata instead of Entities. Entities are meant to be tied to ranges of text, thus, if your custom block does not hold any text, then it does not make sense to have an Entity, and just need to use the own's block metadata.

vicary commented 4 years ago

@leanazulyoro How do you add block level metadata? Modify.setBlockData(...) doesn't do anything, the fact that AtomicBlockUtil.insertAtomicBlock(...) have no data parameter almost convinced me that block level is the one being historical until I see your comment here...

andrew-the-drawer commented 4 years ago

Using atomic block is the most common way to display media component in Draft-js, and this practice is commonly reproduced in multiple examples across the Internet. However, when you look into Facebook Note (yelp, FB Note uses draft-js), you can see that it doesn't display a photo by a different atomic block, but by a combination of entity and composite decorator.

So I think the best practice, simply put, is the following:

  1. You register an entity into a current selection. Modifier.applyEntity(...); EditorState.set(...);
  2. Modify composite decorator strategy to find this entity in the content state (As far as I concern, use contentBlock.findEntityRange(...)).
  3. Register a component to composite decorator to display that entity.

So if your decorator's component is like this <div><img src="" /></div> then it can display the image in the middle of your block, without breaking/splitting your current block. If you want to display the image in a different block, simply put the cursor at the beginning of a new empty block and insert image.

Since FB Note use this practice, I think Composite Decorator is definitely a palpable way to deal with non-text component in Draft-js.