html-elements
contains four custom HTML elementscheck-box
emulates <input type="checkbox">
plus additional features.check-tri
is a tri-state checkbox, adding a form of indeterminate
as the third state.state-btn
is a multi-state button with user-defined states and shapes.input-num
is a numeric input that emulates a spreadsheet.There are three JavaScript files for the four elements. check-box
and check-tri
fit into one file. Here is some sample HTML that includes all four elements in a page:
<head>
<script src="https://github.com/sidewayss/html-elements/raw/main/html-elements/multi-check.js" type="module"></script>
<script src="https://github.com/sidewayss/html-elements/raw/main/html-elements/multi-button.js" type="module"></script>
<script src="https://github.com/sidewayss/html-elements/raw/main/html-elements/input-num.js" type="module"></script>
</head>
NOTE: html-elements
uses await
at the top level of modules. The browser support grid is here. The workaround is funky enough that I'm not inclined to make any changes. Current global support is 92.93%, and while support for await
without this feature goes back another 4 or 5 years, it only increases the global support to 94.66%.
html-elements
also uses private properties, which also have ~93% support, but can be transpiled out without too much trouble. If you want to use html-elements
but require either of these workarounds, please submit an issue or a pull request.
There are built-in template files in the root directory:
check-box
and check-tri
state-btn
input-num
Instead of modifying those, you should create an /html-templates
directory as a sibling of your /html-elements
directory. Store your template files there. That keeps the source and user files separate and preserves the source files as fallbacks.
When the page is loading, if the element doesn't find its template file in /html-templates
, the DOM generates an unsupressable HTML 404 "file not found" error. Then it falls back to the built-in template file. I would suggest copying the built-in files to your templates directory as a starting point. Then edit them to create your own designs within the template structure.
NOTE: The CSS files in the css
sub-directory are samples. They are not used directly by the elements, as the template files are.
check-box
check-tri
state-btn
input-num
If you want to jump right in, the test/demo app is here.
Based on an informal survey and my own repeated frustrations, it became clear to me that <input type="number"/>
isn't just "the worst" HTML input, it's a complete waste of time. I needed an alternative. I spent over a decade programming for finance executives and financial analysts, so I spent a lot of time in Excel. Regardless of the brand, spreadsheets have a model for inputting numbers that is tried and true. I decided to create a custom element that imitates a spreadsheet, while maintaining consistency with the default <input type="number"/>
as implemented by the major browsers.
<input type="number"/>
emulation:
max
and min
attributes prevent input outside their range.step
attribute defines the spin increment.Tab
key). Clicking on the spinners activates the outer element and keeps the spinners visible until it loses focus. See "For keyboard users" below for some differences.keyup
event adds the "NaN"
class if the contents are not a number. Styling .NaN
is up to you.!Number.isNaN(val ? Number(val) : NaN)
val
is the attribute value: a string or null
. It allows any number, but it's strict about the conversion, excluding ""
, null
, and numbers with text prefixes or suffixes that parseFloat()
converts.Spreadsheet emulation:
Number
separate from the formatted display, which is a String
that includes non-numeric characters. Inputting via keyboard/pad reveals the underlying, unformatted value.Enter
key or an OK
button. The user can cancel the input via the Esc
key, a Cancel
button, or by setting the focus elsewhere. Cancelling reverts to the previous value.<input-num>
has data-digits
, data-locale
, data-currency
,data-accounting
, and data-units
attributes for formatting. I have not implemented any of the Intl.NumberFormat
rounding options yet.Additional features:
max
, min
, digits
, units
, locale
, currency
, accounting
, and CSS font properties.Intl.NumberFormat
units because I don't see a way to do exponents. If someone can explain that to me and convince me that they need it, I'll add it.data-delay
and data-interval
properties to control the timing of spinning.For keyboard users:
Tab
key to activate the element, the outer element, not the <input>
gets the focus. In this state you can spin via the up and down arrow keys. Pressing Tab
again sets focus to the <input>
. The next Tab
blurs the element entirely.Shift+Tab
to activate the element, the <input>
gets the focus. The next Shift+Tab
activates the outer element. The next one blurs the element.HTML Attributes / JavaScript Properties, property names excludes the "data-" prefix:
max
is the enforced maximum value, defaults to Infinity
.min
is the enforced minimum value, defaults to -Infinity
.step
is the the amount to increment or decrement when spinning, defaults to the smallest number defined by the data-digits
attribute, e.g. when digits
is 3, step
defaults to 0.001.value
is the value as a Number.data-digits
is the number of decimal places to display, as in number.toFixed(digits)
.data-units
are the string units to append to the formatted text.data-locale
is the locale string to use for formatting. If set to "" it uses the users locale: navigator.language
. If set to null (removed), formatting is not based on locale. It uses the locales
option, but only sets one locale.data-currency
, when set, sets style
= "currency"
and currency = the attribute value. It is only relevant when data-locale
is set. NOTE: currencyDisplay
is always set to "narrowSymbol"
.data-accounting
is a boolean that toggles currencySign
= "accounting"
, which encloses negative numbers in parentheses. Only relevent when data-currency
and data-locale
are set.data-notation
sets notation
to the attribute value. Defaults to "standard"
. Untested! I don't understand the implications of the various options.data-delay
is the number of milliseconds between mousedown
and the start of spinning.data-interval
is the number of milliseconds between steps when spinning.These boolean attributes/properties are inverted. The default is: attribute = unset / property = true:
data-no-keys
/ keyboards
disables/enables keyboard input. Combining it with data-no-spin
effectively disables the element.data-no-spin
/ spins
controls the diplay of the spinner on hover (when not inputting via keyboard).data-no-confirm
/ confirms
controls the diplay of the confirm/cancel buttons on hover during keyboard input.data-no-scale
/ autoScale
scales (or not) the buttons to match font size.data-no-width
/ autoWidth
auto-determines the width (or not).data-no-align
/ autoAlign
auto-aligns left/right and auto-adjusts padding-right
for right-alignment.data-no-resize
/ autoResize
used to disable the resize()
function while loading the page or setting a batch of element properties.JavaScript only:
text
(read-only) is the formatted text value, including currency, units, etc.useLocale
(read-only) returns true
if data-locale
is set.resize(forceIt)
forces the element to resize. When autoResize
is true
, it runs automatically after setting any attribute that affects the element's width or alignment. When autoResize
is false
, you must set forceIt
to true
or the function won't run. Call it after you change CSS font properties, for example.The only event that you can listen to is change
. There is no need for an input
event, as the element does not enforce any limitations on keyboard entry as it happens, it only formats via the "NaN"
class.
The change
event fires every time the value changes:
When the user inputs via the spinner, the event object has two additional properties:
isSpinning
is set to true
.isUp
is true
when it's spinning up (incrementing) and false
or undefined
when spinning down (decrementing).Before the value is committed and the change
event is fired, you can insert your own validation function, to take full control of that process. Because it runs before committing the value, it happens before the isNaN()
validation done internally. Invalid value styling is up to you.
The property is named validate
, and it must be an instance of Function
or undefined
. The function takes two arguments: value
and isSpinning
. value
is a string from the keyboard or a number from spinning. The return value falls into three categories:
false
for invalid valuesundefined
or value
to accept the current valuemax
and min
.You can obviously style the element itself, but you can also style some of its parts via the ::part
pseudo-element. Remember that ::part
overrides the shadow DOM elements' style. You must use !important
if you want to override ::part
. See the html-elements/css/input-num.css
file for example styling.
The available parts:
input
is the <input type="text">
.buttons
is the container for the spinner and confirm buttons.border
is the vertical border between the input and the buttons.My preference, as illustrated in the sample css/input-num.css
file, is to not display the spinner or confirm buttons on devices that can't hover:
@media (hover:none) {
input-num::part(buttons) { display:none }
}
Those devices are all touchscreen, and focusing the element will focus the <input>
, which will display the appropriate virtual keyboard. Touch and hold has system meanings on touch devices, which conflicts with spinning. At font-size:1rem
the buttons are smaller than recommended for touch. So unless you create oversized buttons or use a much larger font size, it's best not to display them at all.
NOTE: Auto-sizing only works if the element is displayed. If the element or any of it's ancestors is set to display:none
, the element and its shadow DOM have a width and height of zero. During page load, don't set display:none
until after your elements have resized.
NOTE: If you load the font for your element in JavaScript using document.fonts.add()
, it will probably not load before the element. So resize()
won't be using the correct font, and you'll have to run it again after the fonts have loaded. Something like this:
document.addEventListener("DOMContentLoaded", load);
function load() {
const whenDefined = customElements.whenDefined("in-number");
Promise.all([whenDefined, document.fonts.ready]).then(() => {
for (const elm of document.getElementsByTagName("input-num"))
elm.resize();
});
}
If you are doing this and want to be more efficient, set the data-no-resize
attribute on the element:
<input-num data-no-resize></input-num>
Then turn on the autoResize
property prior to calling resize()
:
for (const elm of document.getElementsByTagName("input-num")) {
elm.autoResize = true;
elm.resize();
}
template-number.html
I am more familiar with SVG than other image formats, so the spin and confirm buttons in the built-in template are in SVG. You can create your own template file that uses JPEG or whatever image format you prefer.
The core of the template is a flex <div>
, with three children:
<input type="text" id="input" part="input"/>
- the text input<svg part="buttons">
- the spinner and confirm buttons<svg viewBox="0 0 0 0">
- three <text>
elements used to calculate auto-widthDo not modify:
<input>
<svg viewBox="0 0 0 0">
<g id="controls">
and its child <use>
<rect class="events/>
Everything else is user-configurable, though you'll probably want to keep the flex container:
<style>
(optional, but recommended)<defs>
<defs>
defines the shapes, and <style>
formats them. There are two pairs of buttons:
The definitions are setup as a single block containing each pair. This allows you to create a single image that responds differently when interacting with the top or bottom button. That kind of design makes more sense for the spinner than the confirm buttons...
There is a definition for each cell in this matrix for a total of 19 ids. Confirm has an additional set of definitions for when the ok button is disabled due to user input == NaN . Those are labeled cancel . The <rect> elements that handle events have the ids "top" and "bot" (for "bottom"): |
Pair | State | #id/href |
---|---|---|---|
spinner | idle | #spinner-idle |
|
spinner | keydown* | #spinner-key-top #spinner-key-bot |
|
spinner | hover | #spinner-hover-top #spinner-hover-bot |
|
spinner | active | #spinner-active-top #spinner-active-bot |
|
spinner | spin | #spinner-spin-top †#spinner-spin-bot |
|
confirm | idle | #confirm-idle |
|
confirm | hover | #confirm-hover-top #confirm-hover-bot |
|
confirm | active | #confirm-active-top #confirm-active-bot |
|
cancel | idle | #cancel-idle |
|
cancel | hover | #cancel-hover-top #cancel-hover-bot |
|
cancel | active | #cancel-active-top #cancel-active-bot |
* ArrowUp or ArrowDown key, initial image is state:key, full-speed spin uses spin-
† spin-top
and spin-bot
are the full-speed spin images, used after data-delay
expires