0.16.9 • Published 14 days ago

@nichoth/components v0.16.9

Weekly downloads
-
License
MIT
Repository
github
Last release
14 days ago

components

module types Socket Badge license

A collection of UI components made with preact and tonic.

See a live demo

install

npm i -S @nichoth/components

use

I recommend using this with vite + ESM, because it is easy. These are preact and tonic components; you will need to install preact or tonic.

The preact version is recommended, because some of the animations do not work well in the tonic version. In particular the radio-group, and hamburger components do not work well.

preact + htm

Use preact with tagged template literals.

!IMPORTANT
This is the preferred way to consume these, with htm + preact.

Import components from @nichoth/components/htm/*.

import { html } from 'htm/preact'
import { render, FunctionComponent } from 'preact'
import { TextInput } from '@nichoth/components/htm/text-input'
import '@nichoth/components/text-input.css'

const Example:FunctionComponent<{}> = function () {
    return html`
        <div>
            <h3>Text Input</h3>
            <${TextInput}
                displayName="htm text input"
                required=${true}
                minLength=${3}
                maxLength=${7}
                name=${'htm-text-input'}
            ><//>
        </div>
    `
}

render(html`<${Example}><//>`, document.getElementById('root')!)

preact + JSX

Use preact with JSX

import { render, FunctionComponent } from 'preact'
import { TextInput } from '@nichoth/components/preact/text-input'
import '@nichoth/components/text-input.css'

const Example:FunctionComponent = function () {
    return <div>
        <h3>Text Input</h3>
        <form className="example-form">
            <TextInput name="text" displayName="Input test" />
        </form>
    </div>
}

render(<Example />, document.getElementById('root')!)

tonic

Use web components with Tonic

import Tonic from '@nichoth/tonic'
import { SpinningButton } from '@nichoth/components/tonic/spinning-button'
import '@nichoth/components/text-input.css'

export class TonicExample extends Tonic {
    state = {
        isSpinning: false,
    }

    render () {
        return this.html`<div>
            <spinning-button
                isSpinning=${this.state.isSpinning}
                data-event="click-the-button"
            >
                click here
            </spinning-button>
        </div>`
    }
}

Tonic.add(SpinningButton)
Tonic.add(TonicExample)

globals

We use these CSS variables

:root {
    --transition-time: 0.2s;
    --button-outine-color: black;
    --button-primary-outline: #0077ff;
    --button-outine-primary-bg-hover: rgb(0 255 255 / 28%);
    --button-outline-disabled-ol: #0077ff5c;
    --text-input-error-border: red;
    --text-input-error-text: red;
    --hamburger-color: black;
    --fade-in-time: 0.2s;  /* for the mobile nav fade-in animation */
}

Define them in your application to customize.

API

css

Import the css variables in addition to the per-component css.

With Vite as bundler:

import '@nichoth/components/variables.css'

Switch

See ./src/htm/switch

css

Affected by the transition variable.

:root {
    --transition-time: 0.2s;
}

example

import { html } from 'htm/preact'
import { Switch } from '@nichoth/components/htm/switch'
import '@nichoth/components/switch.css'

function Example () {
    return html`<div id="switch">
        <form
            onSubmit=${ev => {
                ev.preventDefault()
                const el = ev.target.elements['test-switch']
                console.log('el.checked', el.checked)
            }}>
                <${Switch} name="test-switch" />
                <button type="submit">submit</button>
        </form>
    </div>`

checkbox

See ./src/htm/checkbox.ts

Pass in the checkbox label as child text.

Optionally Pass in a signal as the checked value, to use as a controlled input. Or do not pass in a checkedState for a non-controlled input.

const Checkbox:FunctionComponent<{
    checkedState?:Signal<boolean>;
} & JSX.HTMLAttributes<HTMLCheckbox>> = function (props)

example

import { html } from 'htm/preact'
import { Checkbox } from '@nichoth/components/htm/checkbox'
import '@nichoth/components/checkbox.css'

function Example () {
    // note we pass in the checkbox label text as a child
    return html`<form onSubmit=${ev => {
        ev.preventDefault()
        const testbox = ev.target.elements.testbox
        console.log('testbox value', testbox.checked)
    }}>
        <fieldset>
            <legend>checkbox demo</legend>
            ${/* Can pass in `checkedState` here */}
            <${Checkbox} name="testbox">Testing checkbox<//>
        </fieldset>

        <button type="submit">submit</button>
    </form>`
}

css

CSS selectors:

label.checkbox

and

input.checkbox

Accordion

Available in preact/htm only.

Accordion CSS

It is affected by the --accordion-transition-time, and --x-transition-time CSS variables

:root {
    --accordion-transition-time: 0.4s;
    --x-transition-time: 0.2s;
}

example

import { html } from 'htm/preact'
import { Accordion } from '@nichoth/components/htm/accordion'
import '@nichoth/components/accordion.css'

function Example () {
    return html`<${Accordion}>
        <summary>Trying accordion example</summary>
        <p>This is the nested paragraph element in the accordion demo</p>
    <//>`
}

Results in UI like this:

closed: Screenshot of accordion closed

open: Screenshot of accordion open

Button

Show resolving state with a spinner in the button. Either pass in a signal to use as resolving state, or just return a promise from the click event handler.

Button example

import { FunctionComponent } from 'preact'
import { html } from 'htm/preact'
import { useSignal } from '@preact/signals'
import { Button } from '@nichoth/components/preact/button'
const isResolving = useSignal(false)

const MyComponent:FunctionComponent = function () {
    return html`<Button
        isSpinning={resolving} ${/*<- note we are passing a signal, not boolean*/}
        onClick={clicker}
    >
        example
    </Button>`
}

Button Outline

If you return a promise from the onClick event handler, then the button will spin until the promise resolves.

Takes an optional signal for isSpinning. To control the spinning state of the button, eg for a form submit, pass in a signal and update its value.

Looks for a few css variables:

:root {
    --button-outine-color: black;
    --button-primary-outline: #0077ff;
    --button-outine-primary-bg-hover: rgb(0 255 255 / 28%);
    --button-outline-disabled-ol: #0077ff5c;
}
interface ButtonProps extends JSX.HTMLAttributes<HTMLButtonElement> {
    isSpinning?: Signal<boolean>,
    className?: string,
    onClick?: (ev:MouseEvent) => Promise<any>
}

example

import { ButtonOutine } from '@nichoth/components/preact/button-outline'

<ButtonOutline
    type="submit"
    onClick={ev => {
        ev.preventDefault()
        console.log('click')
        // if you return a promise, then the button
        // will spin until it resolves
        return sleep(2000)
    }}
>
    example
</ButtonOutline>

ButtonLink

A link that looks like a button. Use with htm.

import { html } from 'htm/preact'
import { FunctionComponent } from 'preact'

/**
 * A link that looks like a button
 *
 * @param {HTMLAttributes<HTMLAnchorElement>} props
 * @returns {FunctionComponent}
 */
const ButtonLink:FunctionComponent<
    HTMLAttributes<HTMLAnchorElement>
> = function (props) {
    const className = [props.class, 'btn-link'].join(' ').trim()
    return html`<a href=${props.href} class=${className}>${props.children}</a>`
}

ButtonLink example

import { html } from 'htm/preact'
import { ButtonLink } from '@nichoth/components/htm/button-link'
import '@nichoth/components/button.css'

// ...

return html`<div>
    <ButtonLink href="#" class="my-button">button text</ButtonLink>
</div>`

CopyBtn

import { CopyBtn } from '@nichoth/components/preact/copy-btn'

<CopyBtn payload="copying things">copy</CopyBtn>

CopyIconBtn

import { CopyIconBtn } from '@nicohoth/components/preact/copy-icon-btn'
// ...
<span>
    Copy this text
    <CopyIconBtn payload="copy this text" />
</span>

Editable Field

interface Props extends JSX.HTMLAttributes<HTMLInputElement> {
    onSave:(value:string) => Promise<any>
    name:string
}

const EditableField:FunctionComponent<Props> = function EditableField (props)
import { EditableField } from '@nichoth/components/preact/editable-field'
// ...
<EditableField
    name="editable-field"
    value="edit this"
    onSave={saver}
/>

Text Input

This looks for 2 css variables:

:root {
    --text-input-error-border: red;
    --text-input-error-text: red;
}

Pass in an attribute title; this determines the invalid hint text that is shown below the input.

interface InputProps extends JSX.HTMLAttributes<HTMLInputElement> {
    displayName: string;
    name: string;
    className?: string;
}

const TextInput:FunctionComponent<InputProps> = function (props:InputProps)

example

import { TextInput } from '@nichoth/components/preact/text-input'
// ...
function MyElement () {
    return html`<form className="example-form">
        <${TextInput}
            displayName="htm text input"
            title="At least 3 characters, but less than 7"
            required=${true}
            minLength=${3}
            maxLength=${7}
            name=${'htm-text-input-example'}
        ><//>
    </form>`
}

PencilBtn

import { PencilBtn } from '@nichoth/components/preact/pencil-btn'
<PencilBtn onClick={(ev) => {
    // we are passed a `click` event
    ev.preventDefault()
    console.log('click')
}} />

RadioGroup

interface Props {
    id?:string
    name:string
    legend:string
    options:string[]
    required:boolean
}

const RadioGroup:FunctionComponent<Props> = function (props)
import { RadioGroup } from '@nichoth/components/preact/radio-group'

<RadioGroup
    name="test-radio"
    legend="testing radio group"
    options={['aaa', 'bbb', 'ccc']}
    required={true}
/>

NumberInput

interface Props {
    name:string;
    min:number;
    max:number;
    value:Signal<number>;
    onIncrease?:(ev:MouseEvent)=>any;
    onDecrease?:(ev:MouseEvent)=>any;
    onChange?:(ev:JSX.TargetedEvent)=>any;
}

const NumberInput:FunctionComponent<Props> = function NumberInput (props)
import { NumberInput } from '@nichoth/components/preact/number-input'

const count = useSignal(3)
// ...
<NumberInput
    min={0}
    max={7}
    name="test-input"
    value={count}
    onIncrease={() => {
        console.log('increase')
    }}
    onDecrease={() => {
        console.log('decrease')
    }}
/>

ReactiveForm

A form element that uses HTML attributes to check validity, and enables or disables the submit button as appropriate.

type Props = {
    onInput?:(event:InputEvent)=>any;
    onSubmit:(event:SubmitEvent)=>any;
    controls?:boolean;
    buttonText?:string;
} & Readonly<Attributes & {
    children?: ComponentChildren
}>

const ReactiveForm:FunctionComponent<Props> = function (props:Props)
import { ReactiveForm } from '@nichoth/components/preact/reactive-form'

<ReactiveForm
    onSubmit={async (ev:SubmitEvent) => {
        ev.preventDefault()
        const text = ((ev.target as HTMLFormElement)
            .elements
            .namedItem('text') as HTMLInputElement)

        await sleep(2000)

        console.log('resolved...', text.value)
    }}
>
    <TextInput
        required={true}
        displayName="text input"
        name="text"
    />
</ReactiveForm>

example

import { render } from 'preact'
import { useSignal } from '@preact/signals'
import HamburgerWrapper from '@nichoth/components/preact/hamburger'
import MobileNav from '@nichoth/components/preact/mobile-nav-menu'
import { CopyBtn, CopyIconBtn } from '@nichoth/components/preact/copy-btn'
import '@nichoth/components/variables.css'
import '@nichoth/components/copy-btn.css'
import '@nichoth/components/hamburger.css'
import '@nichoth/components/mobile-nav-menu.css'
import '@nichoth/components/z-index.css'

const App = function App () {
    const isOpen = useSignal(false)

    function mobileNavHandler (ev) {
        ev.preventDefault()
        isOpen.value = !isOpen.value
    }

    return <div class="app">
        <HamburgerWrapper isOpen={isOpen} onClick={mobileNavHandler} />
        <MobileNav isOpen={isOpen}>
            <a href="/baloney">baloney</a>
            <a href="/test">testing</a>
        <//>

        <CopyBtn payload="hurray">copy something</CopyBtn>

        <p>Copy this <CopyIconBtn payload="Copy this" /></p>
    </div>
}

const el = document.getElementById('root')
if (el) render(<App />, el)

css

We look for a css variable --hamburger-color, or by default use a black color.

/* in your css file */
:root {
    --hamburger-color: #FAFAFA;
}

see also

0.16.9

14 days ago

0.16.6

1 month ago

0.16.7

1 month ago

0.16.3

3 months ago

0.16.4

3 months ago

0.16.5

3 months ago

0.16.0

3 months ago

0.16.1

3 months ago

0.16.2

3 months ago

0.15.5

4 months ago

0.15.6

4 months ago

0.15.7

4 months ago

0.15.4

4 months ago

0.15.3

4 months ago

0.15.2

4 months ago

0.15.1

5 months ago

0.15.0

5 months ago

0.14.1

5 months ago

0.13.6

5 months ago

0.13.4

5 months ago

0.13.5

5 months ago

0.14.0

5 months ago

0.9.4

6 months ago

0.4.9

7 months ago

0.9.3

6 months ago

0.4.8

7 months ago

0.9.6

6 months ago

0.9.5

6 months ago

0.11.0

6 months ago

0.12.0

6 months ago

0.11.1

6 months ago

0.13.0

6 months ago

0.11.2

6 months ago

0.13.1

6 months ago

0.13.2

6 months ago

0.13.3

6 months ago

0.10.0

6 months ago

0.3.0

8 months ago

0.2.0

10 months ago

0.9.0

6 months ago

0.8.1

6 months ago

0.7.2

7 months ago

0.4.5

7 months ago

0.8.0

7 months ago

0.7.1

7 months ago

0.4.4

8 months ago

0.9.2

6 months ago

0.8.3

6 months ago

0.4.7

7 months ago

0.9.1

6 months ago

0.8.2

6 months ago

0.4.6

7 months ago

0.4.1

8 months ago

0.3.2

8 months ago

0.4.0

8 months ago

0.3.1

8 months ago

0.7.0

7 months ago

0.4.3

8 months ago

0.4.2

8 months ago

0.3.3

8 months ago

0.1.1

1 year ago

0.1.0

1 year ago

0.0.5

1 year ago

0.0.4

1 year ago

0.0.3

1 year ago

0.0.2

1 year ago

0.0.1

1 year ago

0.0.0

1 year ago