shshaw / Splitting

JavaScript microlibrary to split an element by words, characters, children and more, populated with CSS variables!
https://splitting.js.org
MIT License
1.67k stars 68 forks source link

Prerendering/Server Side Rendering Support #20

Open shshaw opened 5 years ago

shshaw commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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

shshaw commented 5 years ago

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 :warning:DANGEROUS:warning: way? :slightly_smiling_face:

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 :stuck_out_tongue: (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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 3 years ago

@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 commented 3 years ago

@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 commented 3 years ago

@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 commented 3 years ago

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

craigrileyuk commented 1 year ago

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