glyphic-co / textblock

Continuously responsive typesetting — Demo:
https://textblock.io
MIT License
548 stars 16 forks source link

Import Textblock for React #37

Open FredrikSigvartsen opened 5 years ago

FredrikSigvartsen commented 5 years ago

Hello! First of all, this framework seems pretty cool. Thank you for that!

But I have some problem using it in a React app.

Expected behaviour in App.js:

import React, { Component } from "react";
import "./App.scss";
import Textblock from "textblock";

class App extends Component {
  componentDidMount() {
    Textblock([
      {
        target: "h3",
        minWidth: 400,
        maxWidth: 800,
        minWidthFontSize: 18,
        maxWidthFontSize: 24,
        minWidthLineHeight: 1.33,
        maxWidthLineHeight: 1.2,
        units: "px"
      }
    ]);
  }

  render() {
    return
      <div className="App">
        <h3>This title scales perfectly based on Textblock</h3>
      </div>
    );
  }
}

export default App;

I guess this is the ES6 way to do it. This is mentioned here: https://github.com/glyphic-co/textblock/issues/7#issuecomment-364515428 Current behavior As it seems now, I have to do this in e.g. index.html the old fashioned way.

theorosendorf commented 5 years ago

Sorry, this is my fault. I published to npm but didn't actually test it in React. On a React site, we actually custom made a new component and wrapped it in the basic React stuff and then included the js notation below that (see attached).

@traviskochel / @tinymachine, is a proper component in order of is there an easy way to get it to work as is?

screen shot 2019-03-06 at 12 52 18 pm screen shot 2019-03-06 at 12 58 28 pm
tinymachine commented 5 years ago

I could be way off but I think the issue might be the import statement handling Textblock as if it were being exported as an ES6 module, when it's not (unlike in your special ReactTextblock component, @theorosendorf).

@FredrikSigvartsen, does the following change fix the issue?

- import Textblock from "textblock"; 
+ import "textblock";

I think this will add Textblock to the global namespace, as @traviskochel warns about here. I'm not sure how to allow the script to both be importable and be run as a vanilla script as it does now...

FredrikSigvartsen commented 5 years ago

@tinymachine , this solves the current problem. I just want to leave the solution for others to see:

Expected behaviour in App.js:

import React, { Component } from "react";
import "./App.scss";
import "textblock";

class App extends Component {
  componentDidMount() {
    window.Textblock([
      {
        target: "h3",
        minWidth: 400,
        maxWidth: 800,
        minWidthFontSize: 18,
        maxWidthFontSize: 24,
        minWidthLineHeight: 1.33,
        maxWidthLineHeight: 1.2,
        units: "px"
      }
    ]);
  }

  render() {
    return
      <div className="App">
        <h3>This title scales perfectly based on Textblock</h3>
      </div>
    );
  }
}

export default App;

But I guess the expected behaviour is still my last post, using ES6 Modules. But the solution above will definitely work for now.

theorosendorf commented 5 years ago

Anyone know of a good way to satisfy both scenarios in one codebase?

tinymachine commented 5 years ago

I don't know of a way to satisfy both scenarios, but I think the following code from the marked repo might be relevant?

The script opens with:

;(function(root) {
'use strict';

And closes with:

if (typeof module !== 'undefined' && typeof exports === 'object') {
  module.exports = marked;
} else if (typeof define === 'function' && define.amd) {
  define(function() { return marked; });
} else {
  root.marked = marked;
}
})(this || (typeof window !== 'undefined' ? window : global));

I haven't had a chance to make sense of this yet; just posting in case it's helpful...

tinymachine commented 5 years ago

I worked up a proof-of-concept for a single file that works as an imported module or as a script loaded in index.html. Here's a sample React project on CodeSandbox that imports the script as a module using the syntax import Textblock from "./textblock-umd-test";. And here's a vanilla HMTL project on CodePen that links to a copy of the same file using a <script> tag.

(Note: these examples use parameter names proposed in PR #36.)

Generating the script

This is a bit over my head, but I used grunt-umd to transform the source code into a UMD (universal module definition), which (I think) allows the script to be used as a CommonJS/AMD module (but not a more modern ES6 module) as well as a standard script. The grunt task works by wrapping UMD boilerplate code around the script (which increases the script size just a bit).

Minor issue with minification

The uglify task that mangles function names and properties (also introduced in PR #36) is currently breaking the script's ability to be imported as a module. This can be mitigated by turning off mangling, but the minified script size will of course increase.

A better way?

I'm not sure this is the most up-to-date approach, though. A better solution (but one that might be overkill) might be to leave the basic script alone, specified as pkg.main as it is now, and then also compile it as an ES6 module (ESM), maybe using Rollup.js, and point to that version of the script in pkg.module. (See the Rollup explanation of pkg.module — essentially, Webpack and other bundlers look at the module property first when resolving import statements.)

Any input welcome...

theorosendorf commented 5 years ago

@tinymachine – This sounds amazing but might be slightly over my head at the moment. Would there be any sense in running a poll to find what people want most or do we already know what that is? Is it plain JS and React, or the more specific ES6 designations?

tinymachine commented 5 years ago

Would there be any sense in running a poll to find what people want most or do we already know what that is? Is it plain JS and React, or the more specific ES6 designations?

@theorosendorf I'm out of my depth here, too — perhaps @FredrikSigvartsen or someone else could weigh in?

billymoon commented 5 years ago

I think a React importable component would be great, but I think some more changes should be made to suit the React paradigm. Perhaps the core functionality could be extracted into a shared file, and then a vanilla version and a react version can import the core. You could then add as many other flavours as you like when react is no longer the hottest thing in the world.

Steps to take to split into vanilla/react versions

  1. extract all dom selector functionality into vanilla.js - react will handle it differently
  2. extract onLoad functionality into vanilla.js - again, react will handle differently
  3. write react component that imports core with api that allows passing refs to point to dom elements, and using render cycle to determine when to reflow text
  4. write react hook to handle resize events, and trigger re-render
  5. put some simple build system in place to compile browser versions of both vanilla and react
tinymachine commented 5 years ago

@billymoon Thanks for your detailed suggestions; I think you're exactly right that this is how Textblock should evolve. (Well, except in your point 5 above, you mention "browser versions of both vanilla and react". You mean compile a vanilla script for the browser and a separate module for React, no?)

I'm not familiar enough with React to tackle this myself. I know it's a decent chunk of work, but any chance you'd be willing to submit a PR? (This would be tremendously educational for me!) Cheers!

theorosendorf commented 5 years ago

Hey guys, Textblock should now be available for import as an ES6 module. I’m sure it can be improved but works for now with a conditional statement testing if module.export can be used and adds it if so.

I was confident enough to release it but please let me know if you see any bugs. Thanks!

theorosendorf commented 5 years ago

Anyone here know about the React lifecycle and how we might adapt Textblock to work when loaded as a component and then again as a function when the DOM changes?

Of course @billymoon's comment about separating the concerns is the ultimate goal. I am testing a hacked version of Textblock in React. It works when loading a component but not subsequently. Is it “component / Will / Did / etc. / mount” that should be used?

tinymachine commented 5 years ago

@theorosendorf I'm planning on starting my first React project soon, so can't offer guidance now but hopefully in a few months! From what little I've read I think the relatively new useEffect hook for function components may be relevant here, too, in addition to componentDidMount in class components, but I barely know what I'm talking about!

kcargile commented 2 months ago

Howdy folks,

I realize this thread is a bit old, but I've been using Textblock in a React site for a few years and thought I'd share a few things for anyone else that's looking.

First, I just submitted PR #74 that addresses some things for use in React (specifically, returning cancel handles to prevent memory leaks).

We obviously don't want a peer dependency on React in the core lib, so I haven't added any React specific code here. @theorosendorf, I propose we set up a textblock-react repo and NPM package for this purpose.

Anyhow, if you're using React strictly CSR (or are using client components, e.g. with the "use client" pragma, useLayoutEffect works well. There's a tradeoff compared to useEffect in that useLayoutEffect blocks rendering while the screen is repainted. This delays first paint, but it removes some of the potential layout shift associated with useEffect. YMMV. If your components are server rendered, useLayoutEffect won't work. In that case, useEffect is your best (only) option.

Here's what it all looks like (in Typescript, vanilla JS works as well). Note that this code does depend on the changes in PR 74.

Create a React hook, like so:

// useTextblock.ts
import { Textblock, TextblockOptions, TextblockTarget } from "textblock";
import { useLayoutEffect } from "react";

export const useTextblock = (blocks: TextblockTarget[], options?: TextblockOptions) => {
    useLayoutEffect(() => {
        const cancelHandle = Textblock(blocks, { debug: true });
        return () => {
            if (cancelHandle && typeof cancelHandle === "function") {
                cancelHandle();
            }
        };
    }, [blocks, options]);
};

This can be used on its own in CSR components.

Optionally, also create a client component for better compatibility with SSR (e.g. Nextjs):

// Textblock.tsx

"use client";

import { useTextblock } from "@/hooks/useTextblock";
import { TextblockTarget } from "textblock";

export interface TextblockProps {
    blocks: TextblockTarget[];
    debug?: boolean;
}

export const Textblock = ({ blocks, debug }: TextblockProps) => {
    useTextblock(blocks, { debug });
    return <></>;
};

Then, simply add the Textblock component wherever you need it. I strongly suggest adding it as close to the element you're targeting as possible (versus adding one instance at the application root), so you're not querying a massive component tree for elements that may not exist on every page render.

For example,

export const Title = ({ title }: { title?: string }) => {
    return title ? (
        <>
            <Textblock
                blocks={[
                    {
                container: "self",
                minWidth: 320,
                maxWidth: 1024,
                fontSizeMinWidth: 3.2,
                fontSizeMaxWidth: 3.7,
                fontSizeUnits: "em",
                lineHeightMinWidth: 0.95,
                lineHeightMaxWidth: 1,
                target: "h1"
                    }
                ]}
            />
            <h1>{title}</h1>
        </>
    ) : (
        <></>
    );
};

Cheers! 🍺