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

Prerendering/Server Side Rendering Support #20

Open
shshaw opened this issue Aug 10, 2018 · 11 comments
Open

Prerendering/Server Side Rendering Support #20

shshaw opened this issue Aug 10, 2018 · 11 comments
Labels
enhancement New feature or request

Comments

@shshaw
Copy link
Owner

shshaw commented Aug 10, 2018

With Splitting.html, it would be fantastic to offer server side rendering support so pre-compiled pages could have elements pre-split so that Splitting wouldn't even need to be delivered to or run on the client.

Currently with VuePress, I'm having to do something like this in the mounted() function to ensure document is available:

    mounted() {
      import ('splitting').then(module => {
        const Splitting = module.default;
        this.splitText = Splitting.html({
          content: this.text,
          by: 'chars'
        });
      });
    },

Is it possible to utilize JSDOM when Splitting is called server side without delivering JSDOM to the client in the case of Vue components that would utilize Splitting?

Some suggestions from @visiblecode, using a JSON based approach, but this would likely require major restructuring of the way we handle splits:

So <p class="foo">Go <a href="blah.html" title="blah">here</a> for stuff.</p> might be encoded ["p", {"class": "foo"}, ["Go ", ["a", {"href": "blah.html", "title": "blah"}, ["here"]], " for stuff."]]

function generateDOM(expr) {
    if ((typeof expr === "string") || (expr instanceof String)) {
        return document.createTextNode(expr);
    } else {
        const element = document.createElement(expr[0]);
        Object.entries(expr[1]).forEach(([attr, value]) => element.setAttribute(attr, value));
        (expr[2] || []).forEach((child) => element.appendChild(generateDOM(child)));
        return element;
    }
}

String approach:

function toHTMLFragments(expr, fragments) {
    if ((typeof expr === "string") || (expr instanceof String)) {
        fragments.push(escapeHTML(expr));
    } else {
        fragments.push("<" + expr[0]);
        Object.entries(expr[1]).forEach([attr, value]) => fragments.push(" " + attr + "=\"" + escapeAttr(value) + "\""));
        fragments.push(">");
        if (expr.length > 2) {
            expr[2].forEach((child) => toHTMLFragments(child, fragments));
            fragments.push("</" + expr[0] + ">");
        }
    }
}
function toHTML(expr) {
    const fragments = [];
    toHTMLFragments(expr, fragments);
    return fragments;
}
@shshaw shshaw added the enhancement New feature or request label Aug 10, 2018
@shshaw
Copy link
Owner Author

shshaw commented Aug 10, 2018

These are the main areas where DOM manipulation happens that we'd need to figure out SSR methods for.

createElement: https://github.com/shshaw/Splitting/blob/master/src/utils/dom.js#L24
splitText: https://github.com/shshaw/Splitting/blob/master/src/utils/split-text.js#L13
Splitting.html: https://github.com/shshaw/Splitting/blob/master/src/core/splitting.js#L48

@shshaw
Copy link
Owner Author

shshaw commented Aug 10, 2018

Undom may be a good "recommend to the user" solution.
https://github.com/developit/undom

@shshaw shshaw changed the title Server Side Rendering Support Prerendering/Server Side Rendering Support Aug 28, 2018
@shshaw
Copy link
Owner Author

shshaw commented Aug 29, 2018

Re: our recent conversation with @towc
Utilizing an internal createElement function adhering to basic JSX principles may allow us to add a Splitting.jsx for use with React & Vue's render functions.

shshaw [9:23 AM]
What’s the cross-framework alternative to the ⚠️DANGEROUS⚠️ way? 🙂

notoriousb1t [9:24 AM]
maybe there is a way to pass in the element factory? This would also work for vue jsx

towc [9:24 AM]
@shshaw don't think there is one 😛 (edited)
you write "bindings", I guess
so the core library just glues together different bindings
that's kind of how mobx/redux work
for hooking into react component lifecycle stuff (edited)

notoriousb1t [9:25 AM]
so

Splitting.jsx({  
      root: <Something></Something>,
      factory: React.createElement,
      by: 'chars'
})

or something like that

shshaw [9:26 AM]
Hm, I could see that working.

notoriousb1t [9:26 AM]
I'm just not sure if you can use setAttribute etc on jsx elements in render()?

shshaw [9:27 AM]
True… Might have to have our own version of createElement that’s used internally?
For the regular splits & element creations

notoriousb1t [9:28 AM]
if jsx elements have enough of the node interface, we can just make createElement passed in as a default option and provide an override
I have just never tried to mutate a react or vue jsx element in the actual render method

towc [9:30 AM]
they both try to keep the view away from the model. You're trying to directly access the view, which... well...

notoriousb1t [9:31 AM]
I think the best way to think of it is a post-processor for the render() function, so a transform for the view (not a model strictly speaking)

towc [9:33 AM]
maybe you can hook into the react-dom package

shshaw [9:33 AM]
If we did a Splitting.jsx style approach, that might solve prerendering/SSR as well. With a little shifting of the internals, we could use our own createElement(tagName, attributes) for most of it
the direct Splitting.jsx method would just bypass where we add classes to the targetted element, and would instead create the element directly like Splitting.html

notoriousb1t [9:49 AM]
It just depends on what we have available in the render function. If we can't mutate the elements, it might make sense to do internal json and then have a rendering phase

but that might make reusing existing elements interesting

shshaw [9:50 AM]
Hm. It may be about the same. We’re only injecting our create elements into existing elements, not modifying them directly (except a class addition)
So at each “text node only” level, we utilize the JSX/JSON style creation.

notoriousb1t [9:58 AM]
I don't know... maybe creating splitting-react component would be the simplest way to provide first level support
it doesn't help in SSR, but it would not require any rework of the library

@shshaw
Copy link
Owner Author

shshaw commented Oct 15, 2018

davidkpiano [1:24 PM]
wish list btw:
Splitting.objects(someElement)
just give me an array of [{ word: 'foo', letters: ['f','o','o'] }, .. ]
whatever internal structure you have
or Splitting.objects(someSentence) even
you want full framework/front-end/back-end support? that's how ya do it

shshaw [2:01 PM]
How would you utilize Splitting.objects in a React setup in a meaningful way?

davidkpiano [2:02 PM]
well that could be an internally used API for something like...
<Split message={message.text} />

davidkpiano [2:03 PM]
see above ^ that'd be a good API for React

davidkpiano [2:04 PM]
you don't need innerHTML
internally, it would be...

  <div style={{'--num-words': numWords}}>
  {words.map(word => <span>{word}</span>)}
)}```

@shshaw
Copy link
Owner Author

shshaw commented Oct 15, 2018

Since a lot of this is about React, here's a potential solution that could be offered as a separate include in the main Splitting repo:

https://codepen.io/shshaw/pen/b9ff364ed9c1ca5d6efffc68317b8de5/

class Split extends React.Component {
  target = React.createRef();

  split = () => {
    if ( this.target.current ) {
      Splitting({ target: this.target.current, ...this.props });
    }
  }

  componentDidMount = this.split;
  componentDidUpdate = this.split;

  render(){
    return (
      <div ref={this.target} {...this.props}>
        {this.props.children}
      </div>
    )
  }
}

@shshaw
Copy link
Owner Author

shshaw commented May 23, 2019

This may be a little "un-React-y", but here's a functional component way that could prevent needing to import React or anything within the Splitting Repo while offering a splitting.jsx.js for use in React projects.

https://codepen.io/shshaw/pen/c2f69f5ac0f1e3e51ac4560669eba50b?editors=0010

function SplittingWrap({ splitting, ...props}) {

  let target;
  
  setTimeout(() => {
    if ( window && document && target ) {
      Splitting({ ...splitting, target: target,  });
    }
  });

  return (
    <span ref={(el) => { target = el; }} {...props}>
      {props.children}
    </span>
  )
}

@flayks
Copy link

flayks commented Sep 21, 2020

@shshaw Any updates on this? I'm doing a project based on NextJS and can't easily use Splitting because it relies on document or window 😿

@aqumus
Copy link

aqumus commented Sep 26, 2020

@shshaw Will the proposed solution work with server side rendering, right I am trying to develop my portfolio website using gatsby and for fancy stuff using splitting.js but I am not able to build since importing splitting module itself uses document

@der-lukas
Copy link

@shshaw Sadly can't get it to work even with you proposed "un-React-y" example! Any ETA on this?
PS: Sorry to bother you with this during the holidays! Please enjoy the free time and don't feel pressured to help right away! :)

@trompx
Copy link

trompx commented May 11, 2021

Same here, not working with next.js, I get ReferenceError: document is not defined triggered by module.exports = require("splitting");

@craigrileyuk
Copy link

If you're running Vue 3, we've just created a lite adaptation of Splitting designed for Vue 3 which is fully SSR compatible (that's why we made it)

https://www.npmjs.com/package/vue3-splitting

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

No branches or pull requests

6 participants