Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support custom element and props in render #6

Closed
elyobo opened this issue Nov 22, 2017 · 20 comments
Closed

Support custom element and props in render #6

elyobo opened this issue Nov 22, 2017 · 20 comments

Comments

@elyobo
Copy link
Contributor

elyobo commented Nov 22, 2017

The .render for rich text currently forces an additional div around the rendered elements, with no ability to customise the wrapper div nor any ability to pass props to that wrapper (e.g. a className).

As of React 16, components can render fragments so for React 16 the additional wrapper is not required. If you're keeping React 15 compatibility, and want to keep the same interface for consistency, it would be useful to allow options to be passed to configure the element type to wrap with and any additional props to pass at least, e.g.

  render(richText, linkResolver, htmlSerializer, { tagName = 'div', props = {}) = {}) {
    const serializedChildren = PrismicRichText.serialize(richText, serialize.bind(null, linkResolver), htmlSerializer);
    const _props = Object.assign(propsWithUniqueKey(), props);
    return React.createElement(tagName, _props, serializedChildren);
  }
@levimykel
Copy link
Contributor

Hi @elyobo, thanks for bringing this up, you're absolutely right. We'll add this functionality.

@elyobo
Copy link
Contributor Author

elyobo commented Nov 22, 2017

I realised after this that, as this isn't actually a component, it can happily return an array of elements even with React 15, which is possibly a cleaner approach - the user can then wrap (or not wrap) the result however they want.

You could return a single element if that's all that there was (e.g. a title field or a single item in a rich text field), or an array if there were multiple items; both can be rendered directly by React.

A related issue is the key for elements, that you're currently randomly generating - this is not a good idea, but you do have a problem with the key that you use for each top level element in the array that you return and I'm not sure I see a great way to do that. I really can't see any reason to be assigning key attributes to everything underneath that though.

@arnaudlewis
Copy link
Member

arnaudlewis commented Nov 22, 2017 via email

@elyobo
Copy link
Contributor Author

elyobo commented Nov 22, 2017

Thanks @arnaudlewis,

The only array issue I was aware of with React 15 was that returning one directly from a render function was not possible (now supported in React 16), were there other issues as well? It does look like PrismicRichText.serialize will just return an array anyway... I understand wanting to keep the API simple to use without causing problems for users if there are other issues (and making the wrapper element configurable would keep it simple for most users while still allowing flexibility where it's needed). I would have thought that the no-array-return-from-render issue was well known enough that it alone would not be a justification for the wrapper though, but that decision is up to you guys :)

The key thing is a little tricky, but I think that for your particular case as one of the ones where just using the index as the key is probably fine; your content is static, it won't be reordered, and your components don't have any internal state. Small changes to prismic-richtext/serialize would allow this in a backwards compatible way by passing through the index through from fromRichText to serializeNode, then passing it in to step, e.g. something like this (disclaimer: I'm not familiar with TypeScript)

import Tree from './tree';
import { Node, SpanNode, NodeElement } from './nodes';
import { RichTextBlock } from './richtext';

type Serializer<T> = (type: string, element: NodeElement, text: string | null, children: T[]) => T;

function fromRichText<T>(richText: RichTextBlock[], serialize: Serializer<T>, htmlSerializer?: Serializer<T>): T[] {
  const tree = Tree.fromRichText(richText);
  return tree.children.map((node: Node, index: number) => {
    return serializeNode<T>(node, index, serialize, htmlSerializer);
  });
}

function serializeNode<T>(parentNode: Node, index: number, serializer: Serializer<T>, htmlSerializer?: Serializer<T>): T {

  function step(node: Node, idx: number): T {
    const text = node instanceof SpanNode ? node.text : null;
    const serializedChildren = node.children.reduce<T[]>((acc: T[], node: Node, i: number) => {
      return acc.concat([step(node, i)]);
    }, []);

    const maybeSerialized = htmlSerializer && htmlSerializer(node.type, node.element, text, serializedChildren);
    return maybeSerialized || serializer(node.type, node.element, text, serializedChildren, idx);
  }

  return step(parentNode, index);
}

export default fromRichText;

@arnaudlewis
Copy link
Member

arnaudlewis commented Nov 22, 2017 via email

@elyobo
Copy link
Contributor Author

elyobo commented Nov 22, 2017

I'm happy to provide some PRs, given that you're open to changes here :)

The key only matters in relation to the other items in the list, so I don't think tracking the depth in tree as part of the key is required (otherwise it would be simple to pass the tree depth through each step as you recurse as well).

I'll open a PR against prismic-richtext for my suggestion there, if that's approved then we can use that to improve the key generation here.

Thanks for your responsiveness to this issue; we're currently evaluating Prismic for a project of ours and it's very encouraging!

@arnaudlewis
Copy link
Member

arnaudlewis commented Nov 22, 2017 via email

@tremby
Copy link

tremby commented Nov 28, 2017

Would one way to allow custom props on the wrapper to provide an actual component? I was surprised not to find something like <PrismicRichText className="myClass" object={myRichTextObject} /> to be possible with this library when using JSX.

But since you guys are talking about possibly removing the wrapper entirely, that'd actually be preferable.

Eagerly awaiting a resolution to this.

@elyobo
Copy link
Contributor Author

elyobo commented Nov 28, 2017

We were doing something similar to that internally and it's possibly a nice thing to provide as well. Something like this (untested)?

import React from 'react';
import { RichText } from 'prismic-reactjs';

const PrismicRichText = ({ tag = 'div', children, ...props }) => React.createElement(tag, props, RichText.render(children));

Use like

<PrismicRichText className="myClass" other="props">{prismicHtmlHere}</PrismicRichText>

@tremby
Copy link

tremby commented Nov 28, 2017

By prismicHtmlHere you mean the rich text data structure, like response.results[0].data.my_richtext_field or whatever?

Sure, it'd be useful to include something like this. It should allow the linkResolver to be passed in too though. And passing in the data structure object as the component's children seems weird to me, since I'd expect to only put actual HTML or other components as something's children, but if that's a common pattern, so be it...

@elyobo
Copy link
Contributor Author

elyobo commented Nov 28, 2017

Yeah, the rich text field.

Yes, link resolver needs to be passed through; would that be a global thing or would you pass it as a prop?

Passing as children is a taste thing I guess; I think it makes sense when a component is not actually going to render other children, it means you can set prop types or flow types appropriately to indicate what you actually expect there, but it's easy to add a richText prop or something instead and just ignore any children that are given.

@tremby
Copy link

tremby commented Nov 28, 2017

Yes, link resolver needs to be passed through; would that be a global thing or would you pass it as a prop?

So far I'm just importing it from a module of my app wherever necessary and passing it into RichText.render. It's a little unwieldy. It'd be nice to register it (as a default but overridable linkResolver perhaps) with the Prismic tools so it's not necessary to pass each time.

@elyobo
Copy link
Contributor Author

elyobo commented Nov 28, 2017

If it was accepted as a prop then it would be easy enough for you to create your own RichText component to wrap one provided here and pass in the right link resolver, then use your own component instead e.g.

import linkResolver from './my-link-resolver';
import PrismicRichText from 'prismic-reactjs/component';

const RichText = ({ children, ...props }) => <PrismicRichText {...props} linkResolver={linkResolver}>{children}</PrismicRichText>;

export default RichText;

Same eventual outcome (you only need to deal with your link resolver once).

@tremby
Copy link

tremby commented Nov 28, 2017

Nice, I may try something like that. Thanks.

But I'd probably still rather have no wrapper!

@tremby
Copy link

tremby commented Nov 28, 2017

Realized for now I can just do {RichText.render(page.heading, linkResolver).props.children} to unwrap the contents.

elyobo added a commit to elyobo/prismic-reactjs that referenced this issue Nov 29, 2017
@elyobo
Copy link
Contributor Author

elyobo commented Nov 29, 2017

@tremby I suggest opening another issue if you'd like a render component or registration of a default link resolver.

@tremby
Copy link

tremby commented Nov 29, 2017

I think registration of a default link resolver is possibly best done with a per-project wrapper as you suggested.

As for a component, I'm not sure if it's super necessary or helpful. I was mostly just surprised there wasn't already one.

@tremby
Copy link

tremby commented Nov 29, 2017

Where did we land on ditching the wrapper element? The PR you opened doesn't address that.

@elyobo
Copy link
Contributor Author

elyobo commented Nov 29, 2017

No, I didn't want to mix up the concerns in that PR so have left it for the moment.

@mathieudelvaux
Copy link

Hi guys, any news on this?
The issue was closed but I can't find any PR addressing this.
I'm using @tremby's solution (props.children) for the moment.

Although, I have a similar issue. My custom type is setup this way:

"myText": {
    "type": "StructuredText",
    "config": {
      "single": "strong",
      "label": "My Text"
    }
  },

Paragraphs are supposed to not be allowed, but my structured text is still rendered inside a <p> tag, inside a <div>, obviously.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants