Rewrite autosize as an Autosize component with the plugin bundled (#3045)

This commit is contained in:
Paweł Kuna authored and GitHub committed 2026-09-13 01:35:14 +02:00
1 parent 1ffbeeca42
commit a13ebc3621
7 files changed
+199 -27

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
---
"@tabler/core": minor
"@tabler/docs": patch
"@tabler/preview": patch
---
Added `Autosize` component to `tabler.js` with the `autosize` plugin bundled, plus `update()` and `dispose()` methods.
+79 -7
View File
@@ -1,11 +1,83 @@
// js-docs-start autosize-init
const autosizeElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>('[data-bs-toggle="autosize"]')
/**
* --------------------------------------------------------------------------
* Tabler autosize.ts
* Licensed under MIT (https://github.com/tabler/tabler/blob/dev/LICENSE)
* --------------------------------------------------------------------------
*/
if (autosizeElements.length) {
autosizeElements.forEach(function (element: HTMLElement) {
if (window.autosize) {
window.autosize(element)
import autosize from 'autosize'
import BaseComponent from './bootstrap/base-component'
import SelectorEngine from './bootstrap/dom/selector-engine'
import type { ElementSelector } from './bootstrap/types'
type ComponentConfig = Record<string, never>
type ComponentConfigInput = Record<string, unknown>
/**
* Constants
*/
const NAME = 'autosize'
const SELECTOR_DATA_TOGGLE = `[data-bs-toggle="${NAME}"], [data-tblr-toggle="${NAME}"]`
const Default: ComponentConfig = {}
const DefaultType: Record<keyof ComponentConfig, string> = {}
/**
* Class definition
*
* Wraps the `autosize` plugin (https://github.com/jackmoore/autosize), bundled
* with `tabler.js`.
*/
class Autosize extends BaseComponent {
declare _element: HTMLTextAreaElement
declare _config: ComponentConfig
constructor(element: ElementSelector, config?: ComponentConfigInput) {
super(element, config)
if (this._element) {
autosize(this._element)
}
})
}
// Getters
static get Default(): ComponentConfig {
return Default
}
static get DefaultType(): Record<keyof ComponentConfig, string> {
return DefaultType
}
static get NAME(): string {
return NAME
}
// Public
update(): void {
autosize.update(this._element)
}
dispose(): void {
autosize.destroy(this._element)
super.dispose()
}
}
/**
* Data API implementation
*/
// js-docs-start autosize-init
for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) {
Autosize.getOrCreateInstance(element)
}
// js-docs-end autosize-init
export default Autosize
+14 -1
View File
@@ -1,5 +1,18 @@
// The `autosize` package ships no type definitions
declare module 'autosize' {
type AutosizeTarget = Element | ArrayLike<Element>
interface Autosize {
(target: AutosizeTarget): AutosizeTarget
update(target: AutosizeTarget): AutosizeTarget
destroy(target: AutosizeTarget): AutosizeTarget
}
const autosize: Autosize
export default autosize
}
interface Window {
autosize?: (element: HTMLElement | HTMLTextAreaElement) => void
countUp?: {
CountUp: new (
target: HTMLElement,
+3
View File
@@ -12,3 +12,6 @@ import './src/sortable'
// Re-export everything from bootstrap.ts (single source of truth)
export * from './src/bootstrap'
// Tabler's own components
export { default as Autosize } from './src/autosize'
+74
View File
@@ -0,0 +1,74 @@
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest'
import autosize from 'autosize'
import { clearFixture, getFixture } from '../helpers/fixture'
import Autosize from '../../src/autosize'
vi.mock('autosize', () => ({
default: Object.assign(vi.fn(), { update: vi.fn(), destroy: vi.fn() }),
}))
const plugin = vi.mocked(autosize)
describe('Autosize', () => {
let fixtureEl: HTMLElement
beforeAll(() => {
fixtureEl = getFixture()
})
beforeEach(() => {
vi.clearAllMocks()
fixtureEl.innerHTML = '<textarea data-bs-toggle="autosize"></textarea>'
})
afterEach(() => {
clearFixture()
})
const textarea = (): HTMLTextAreaElement => fixtureEl.querySelector('textarea')!
describe('NAME', () => {
it('should return plugin name', () => {
expect(Autosize.NAME).toBe('autosize')
})
})
describe('constructor', () => {
it('should call the autosize plugin with the element', () => {
const instance = new Autosize(textarea())
expect(plugin).toHaveBeenCalledWith(textarea())
expect(Autosize.getInstance(textarea())).toBe(instance)
})
})
describe('update', () => {
it('should call autosize.update with the element', () => {
const instance = new Autosize(textarea())
instance.update()
expect(plugin.update).toHaveBeenCalledWith(textarea())
})
})
describe('dispose', () => {
it('should call autosize.destroy and remove the instance', () => {
const instance = new Autosize(textarea())
const element = textarea()
instance.dispose()
expect(plugin.destroy).toHaveBeenCalledWith(element)
expect(Autosize.getInstance(element)).toBeNull()
})
})
describe('getOrCreateInstance', () => {
it('should reuse the instance and not initialise the plugin twice', () => {
const first = Autosize.getOrCreateInstance(textarea())
const second = Autosize.getOrCreateInstance(textarea())
expect(second).toBe(first)
expect(plugin).toHaveBeenCalledTimes(1)
})
})
})
+21 -18
View File
@@ -1,15 +1,11 @@
---
title: Autosize
summary: The autosize element will automatically adjust the textarea height and make it easier for users to follow as they type.
docs-libs: [autosize]
description: Make textareas grow automatically as users type with the autosize plugin, so longer text stays visible without manual resizing.
related: [/ui/forms/elements]
---
import Example from '@components/Example.astro';
import CodeDocs from '@components/CodeDocs.astro';
import TabsPackage from '@components/TabsPackage.astro';
import { Code } from 'astro:components';
import { site } from '@shared/lib/site.ts';
## Overview
@@ -21,15 +17,7 @@ A textarea keeps its height no matter how much text it holds, so long answers en
## Installation
Install autosize with npm:
<TabsPackage name="autosize" />
Or include it from a CDN:
<Code lang="html" code={`<script src="${site.cdnUrl}/dist/libs/autosize/dist/autosize.min.js"></script>`} />
The plugin only changes the height of the field, so it needs no CSS of its own.
The plugin is bundled with `tabler.js`, so there is nothing extra to install or load. It only changes the height of the field, so it needs no CSS of its own.
## Usage
@@ -43,22 +31,37 @@ Set `rows` to choose the starting height. With `rows="1"` the field starts as a
## JavaScript
Tabler initializes every element with `data-bs-toggle="autosize"` on page load. This is the code that runs:
Tabler wraps the plugin in an `Autosize` component that works like the Bootstrap components: it is created once per element and stored on it. On page load Tabler creates one for every element with `data-bs-toggle="autosize"`. This is the code that runs:
<CodeDocs name="autosize-init" file="core/js/src/autosize.ts" />
Call the plugin yourself when you add a textarea later, for example inside a modal or a row added by JavaScript:
Create the component yourself when you add a textarea later, for example inside a modal or a row added by JavaScript:
```js
autosize(document.getElementById('message'));
const autosize = new tabler.Autosize(document.getElementById('message'));
```
Autosize measures the field, so a hidden textarea gets the wrong height. Run `autosize.update()` once it becomes visible:
`tabler.Autosize.getOrCreateInstance(element)` returns the existing component or creates a new one, so it is safe to call more than once for the same field.
Autosize measures the field, so a hidden textarea gets the wrong height. Call `update()` once it becomes visible:
```js
autosize.update(document.getElementById('message'));
tabler.Autosize.getOrCreateInstance(document.getElementById('message')).update();
```
Call `dispose()` to stop the plugin and remove the component from the element:
```js
tabler.Autosize.getInstance(document.getElementById('message'))?.dispose();
```
| Method | Description |
| --- | --- |
| `update()` | Recalculates the height, for example after the textarea becomes visible or its value changes from code. |
| `dispose()` | Stops the plugin and removes the component from the element. |
| `getInstance(element)` | Static. Returns the component for the element, or `null`. |
| `getOrCreateInstance(element)` | Static. Returns the component for the element and creates it when needed. |
## Accessibility
- Keep the `label` for the field. A growing textarea is still a normal form control and needs its name.
+1 -1
View File
@@ -51,7 +51,7 @@ const rows = [
]
---
<DefaultLayout title="Form elements" pageMenu="form.elements" pageLibs={['nouislider', 'autosize', 'tabler-flags', 'tabler-payments', 'litepicker', 'tom-select', 'imask']}>
<DefaultLayout title="Form elements" pageMenu="form.elements" pageLibs={['nouislider', 'tabler-flags', 'tabler-payments', 'litepicker', 'tom-select', 'imask']}>
<DocsLink slot="page-header-actions" path="/ui/forms/elements" />
<div class="row row-cards">
<div class="col-12">