Here is a tiny little helper from the React ecosystem, which is now available in Svelte. It is not a 1:1 port, since I concluded, that the textfit component should better be an action. I will explain the reasons below. In this post, I also cover some parts of the lifecycle of Svelte components and especially some praise for the
function.
tick
>Check out the ported library
svelte-textfit
Introduction
exports a single component, which automatically adjusts the font size of its content to fill the whole element. It currently has 333 stars on GitHub. There are not so many use-cases for such a library, but I use it in one of my projects, which I would like to move over to Svelte.
react-textfit
In my project, I have a schedule filled with events. These can be short or take longer and they can spread over several days. This means that the cell size in the scheduling grid varies a lot. The content is only text, but it also can be only a short notice or contain a lot of details. The grid is quite large, so I use
. As a consequence, I cannot easily adjust row and column sizes. react-virtualized
helps me to make the text as readable as possible while it still respects the cell boundaries.
react-textfit
Porting react-textfit
I have covered the basics on how to port a library from React to Svelte in a previous post, where I ported
.react-virtualized-auto-sizer
is also implemented as a React class component, so I already discussed most of the steps you need to port it (like react-textfit
code goes to componentDidMount
lifecycle, onMount
code is marked as reactive, etc.).
render
What is interesting about the library is how it works with the component lifecycle. When the component mounts or the props change, a function called
is started. The process
interpolates the highest fitting font size. It does so by repeatedly adding callbacks to the process
function. It sets the font size via setState
, renders the component, and looks if the text fits into the cell. Then it tries a different font size based on the previous results, renders again, and so on until the largest fitting font-size is found.
setState
I already wrote about the additional callback in
, but here it is entered on several occasions, so I thought about a more generalized way of porting. My first attempt (which I have implemented in setState
... TODO: change it!) was to add a state array svelte-window
. A afterUpdateCallbacks
call like this
setState
setState({a:1},callback);
would be translated to
a = 1; afterUpdateCallbacks.push(callback);
and then in Sveltes
lifecycle, the list would be processed:
afterUpdate
afterUpdate(()=>{ afterUpdateCallbacks.forEach(v=>v()); afterUpdateCallbacks.length=0; })
That is ok, but then I read one more page from the Svelte tutorial, which brought me to a much more elegant solution:
>the trick is the
tick
The
is also listed as a lifecycle function like tick
or onMount
but it works a bit differently. It is a function, which returns a promise. Promises are a great concept in javascript. They allow you, to execute code asynchronously. They are used, when some part of the code needs to wait for something to happen before it can continue to execute. For example, if you query a database, some part of the code would wait for a result. While it is waiting for the database to respond, other parts of the code could still be executed. Promises make that possible. In this case, we would want the callback to be called, when the component is rendered, so that we have access to the newly computed css values to check if the text fits.
afterUpdate
There are usually two ways, how you can interact with a promise. One is the
syntax, which is often cleaner when you deal with a lot of promises. Here I work with the other option: async/await
. The .then
resolves, when the state changes have been applied to the DOM, so, we can ditch the state array tick
and the afterUpdateCallbacks
lifecycle and the afterUpdate
execution shown above translates to just this very convenient solution:
setState
a=1; tick().then(callback)
Poll: Which React library would you love to see in Svelte?
You can select more than one, or add a new entry
Render ... render?
The render function returns an outer
and a nested inner div
and inside of the inner div
the children are rendered. Unlike div
it is not possible here to dynamically choose the html element types here. Let's say we would like to adjust the label of a button. The final rendered DOM would look like this:
react-window
<button> <div /*textfit-outer-div*/ > <div /*textfit-inner-div*/> button text </div> </div> </button>
That is a lot of stuff to render for a little button. In most cases that is not a performance problem, but when you have a lot of components, it might become one, see excessive DOM size. What is really necessary?
Well, that depends. Css can be a piece of work, and different html elements behave differently. Eg. by default a
element uses div
. In the width:100%
setting with two react-textfit
s the idea is that the outer div
sets the boundary with this maximum width, while the inner div
is set to div
so that the width shrinks to the actual occupied width. The library provides one use-case, which usually works. Doesn't it always work? No, you can easily kill it with custom css.
display:inline-block
To me, that means that the process involves some trial and error anyway. That's why I decided to put it in the hands of the user.
does not export a component, but an action. To me, fitting text into some element is functionality, so an action also makes more sense semantically.
svelte-textfit
Action
An action in svelte is something which you attach to an html element with the
directive. You can only add it to one element, but use:
adds two react-textfit
. The solution for me is that the action is applied to the inner divs
(which now can be any html element). If the setting with an outer div is wanted, you can fill the new div
prop like this:
parent
<div bind:this={outer}> <div use:textfit={{parent:outer}}>...some text</div> </div>
The use of the outer parent is to provide width and height boundaries. Alternatively, you can set fixed pixel values with the new
and width
prop.
height
<button use:textfit={{width:300,height:100}}>click me</button>
What needs to change in the code?
The action is not a Svelte component in a .svelte file, but a function in a .js file. It takes two inputs,
and node
. props
is a reference to the html element, to which we want to apply the action, and props is an object which contains the formerly exported props of the Svelte component. Other than that, there are very few changes to the code. The function is executed once when it is mounted, so the node
stuff just goes into the body of the function. onMount
code is put into the onDestroy
attribute in the return object, just like the destroy
code after the check if the props did change goes into the componentDidUpdate
attribute. What about the other lifecycle, which we handle with update
? Does that still work? Yes, it actually does! I'm not sure, how I would have implemented it without tick
, perhaps hacking with tick
. But that this works is another big reason why setTimeout
is great.
tick
Additional props
I already wrote about
, parent
and width
but I decided to open up a few other things as well.
height
First, I added a prop
. The value of this is not used, but when it is reassigned, a re-run is triggered. Simple example, you have an input field and the text is shown in a update
, you would want the p
element to re-render on text changes:
p
<p use:textfit={{width:400,height:200,update:text}}>{text}</p> <input type="text" bind:value={text}/>
Expample:
The next change is the
prop. Here you can pass a function, how the style should change based on a value. The default is to just set the style
of the node, but maybe you want to do something different, eg. also adjust the font-size
style or increase the line-height
. The library is applying an interpolation cycle: choose next candidate border-width
-> apply style(node,value
) -> check if element fits -> set next candidate value
...etc. You could do some crazy things here, use your imagination.
value
The last change is regarding the definition of 'fitting'.
only looks at the react-textfit
and scrollHeight
are lower than the boundaries. With a simple style change to scrollWidth
the display:inline
is not applicable anymore. For such a case I added a fallback to use the elements scrollWidth
instead. But maybe you would like to look at something completely different? In that case you can pass functions into the offsetWidth
& elementFitsWidth
props. These functions take the elementFitsHeight
and the node
as input and should return a boolean, whether the component fits or not.
width
Typescript
The original library didn't have typescript definitions. I created a
file for this library and also one for d.ts
. PR is ongoing.
react-textfit
Conclusion
If you need to dynamically fit your text into something, you now got a little helper in Svelte. Hope you like it.
What I definitely like is Sveltes
function. It makes lifecycle manipulations super easy and it even works within actions. Kudos.
tick
Share on Twitter - Discuss on Reddit - Vote on next topics - Support me
This might also be interesting for you:
- How to Implement Custom React Hooks in Svelte
- React Hooks in Svelte
- tree-shake-css - project page
- How to merge cells with svelte-window
- Porting React to Svelte - From react-window 1:1 to svelte-window
- Poll: Which React library would you love to see in Svelte?
- Porting React components to Svelte - It might be easier than you think
- Svelte and Typescript
- How this blog is made - Basic Structure, Elder and Markdown
- How this blog is made part 2 - Make your website sketchy with Tailwind and Roughjs
- Showcase: The Descent Ripple Effect or The React Descent Ripple Effect
- Why are component libraries so complicated with React compared to Svelte?