Home

Awesome

vanjs-bootstrap


<a name="home" />

<img src="img/form-lib.svg" alt="logo" height="60">  <img src="img/vanjs.svg" alt="logo" height="60"> VanJs Bootstrap Components

vanjs bs


a component library using VanJs and Bootstrap

For a long time I built my UIs with React and Bootstrap. Then I met VanJs and am amazed at how easy it is. So that I don't have to mess with CSS, I'm introducing some Bootstrap components here.

Check out the demo

Installation

npm install vanjs-bootstrap

will also install bootstrap and vanjs-core

Create A App

I recommend Vite as a development environment.

main.js

import 'bootstrap/dist/css/bootstrap.min.css';
import './app.css';
import 'bootstrap'
import van from 'vanjs-core';
import App from './app.js';
const app = document.querySelector("#app");
van.add(app, App());

app.js is something like this

import van      from 'vanjs-core';
import {Navbar} from 'vanjs-bootstrap';
import Page     from './page';

const appMenu = {
    items: [
        {label: 'Home', href: '#home'},
        ...
    ]
}

export default function App() {
    return () => div(
        Navbar({menu: appMenu}),
        div({class: "container"}, 
            div({class: "row justify-content-md-center"},
                div({class: "col-10 p-4 m-2 border rounded-2"}, 
                Page()
            )
        ),
    )
}
<br />
<a name="page-button" />

Button

export function Button ({bsSize, class, color='secondary', outline, dropdown, ...props}, children)

Implements a Bootstrap button

Demo Code

import van from 'vanjs-core';
import { Button, CheckboxInput, Input, RadioSelectInput, SelectInput } from 'vanjs-bootstrap';

const { div, h2, span } = van.tags;

var bsSize = van.state('md');
var color = van.state('secondary');
var disabled = van.state(false);
var outline = van.state(false);
var dropdown = van.state(false);
var label = van.state("Button");

export default function Page() {

    const OptionsBar = div(
        div({ class: "input-group input-group-sm" },
            span({ class: "input-group-text" }, 'bsSize'),
            RadioSelectInput({
                value: bsSize.val,
                oninput: e => bsSize.val = e.target.value,
                options: 'sm,md,lg', inline: true,
            }),

            span({ class: "input-group-text" }, 'color'),
            SelectInput({
                value: color.val,
                oninput: e => color.val = e.target.value,
                options: ',primary,secondary,success,danger,warning,info,light,dark,link',
            }),

            span({ class: "input-group-text" }, 'outline'),
            CheckboxInput({ value: outline.val, oninput: e => outline.val = e.target.value, class: "form-control", style: "max-width: 2em" }),

            span({ class: "input-group-text" }, 'disabled'),
            CheckboxInput({ value: disabled.val, oninput: e => disabled.val = e.target.value, class: "form-control", style: "max-width: 2em" }),

            span({ class: "input-group-text" }, 'dropdown'),
            CheckboxInput({ value: dropdown.val, oninput: e => dropdown.val = e.target.value, class: "form-control", style: "max-width: 2em" }),
        ),
    );

    return div({},
        div({class: "row"},
            h2('Button Demo'),
            OptionsBar,
            div({class: "col"},
                Button({ bsSize, color, outline, disabled, dropdown, class: "mt-3" }, label),
            ),
        ),
    )
}
<br />
<a name="formcontroller" />

FormController

export function FormController ({values})

Function FormController creates a simple object to handle multiple inputs.

<details> <summary>Show code</summary>

FormController Code

export function FormController ({values} = {}) {
    const isVanState = v => van.val(v) !== v;
    var listeners = [];
    var ccount = 1;
    var self = {
        values: {...(values ?? {})},
        onChange (func) { listeners.push(func); return () => self.offChange(func); },
        offChange (func) { listeners = listeners.filter( f => f !== func) },
        emitChange (name, value) { listeners.forEach( f => f(name, value)) },
        handleInput (event) {
            let {name, value} = event.target;
            if (isVanState(self.values[name])) {
                self.values[name].val = value;
            } else {
                self.values[name] = value;
            }
            self.emitChange( name, value );
        },
        args (args = {}) {
            let {name = `v${ccount++}`, value, oninput = self.handleInput} = args;
            value = value ?? self.values[name] ?? '';
            self.values[name] = value;
            return {name, value, oninput}
        }
    }
    return self;
}
</details> <br />
<a name="formgroup" />

FormGroup

export function FormGroup({name, label, type, input, class, bsSize, cols, id, ...props}, ...children})

FormGroup creates a combination of label and input. The most important arguments are "name", "label" and "type". The value for "type" is usually an <input> type. However, special input fields can either be passed as "input" (a van function) or registered in typeMap with their own "type" (see example). The "id" can be specified, but will be generated automatically if not present. All other arguments in "...props" are passed to the input element, such as "oninput" and "value". The elements are displayed on top of each other. If cols is set, they can also be placed next to each other in a row. For example cols: "2 4" uses 2 colums for the label and 4 colums for the input.

See FormBuilder Demo for example.

FormGroup Args

argcomment
namestring becomes input name
labelstring or func becomes a label component
inputa input control alternative to type. See Custom Input
classclass of the enveloping group
inputClassclass of the input control
bsSizeBootstrap size
colsspace separated string "left right" for the Bootstrap column sizes i.e. "3 6"
colBootstrap column size of the group alternative to cols. Used for form class "row g-3"
idHTML id of group. Will get random id if omited. Label id is id+'-l', input id is id + '-i'
separatedspecial for FormCheck. Separate label and check if true
...propspassed to input control
...childrenappendix for input control like :form-text :valid-feedback or :invalid-feedback
<p></p> <details> <summary>Show FormGroup Code</summary>

FormGroup Code

export function FormGroup({ name, label, input, class: clas, inputClass, bsSize, cols, col, id, separated, ...props}, ...children) {
    const cl = () => {
        let res = '';
        if(van.val(clas)) res += ' ' + van.val(clas);
        return res;
    }
    let g_id = id ?? Math.random().toString(36).substring(2, 9);
    let i_id = g_id + '-i';
    let domInput = input ?? typeMap.get(props.type) ?? Input;
    let ischeck = ['checkbox','radio','switch'].includes(props.type);

    let inputEl, labelEl;
    if(ischeck) {
        if(separated) {
            labelEl = FormLabel({bsSize, for: i_id}, label);
            inputClass = (inputClass ?? '') + ' mt-2';
            inputEl = domInput({bsSize, name, id: i_id, class: inputClass, ...props}, ...children);
        } else {
            labelEl = null;
            inputEl = domInput({bsSize, name, id: i_id, class: inputClass, label, ...props}, ...children);
        }
        children = [];
    } else {
        labelEl = FormLabel({bsSize, for: i_id}, label);
        inputEl = domInput({bsSize, name, id: i_id, class: inputClass, ...props});
    }

    if(cols) {
        let [col_l, col_r] = cols.split(' ');
        col_r = col_r || 12 - Number(col_l);
        var res = [];
        if(labelEl) 
            res.push(FormLabel({bsSize, col: col_l, id: g_id + '-l', for: i_id}, label));
        else
            res.push(div({class: col_l ? `col-${col_l}` : 'col'}));
        res.push(div({class: col_r ? `col-${col_r}` : 'col'}, inputEl, ...children));
        return res;
    }

    if(col) {
        return div({class: col},
            labelEl,
            inputEl,
            ...children
        );
    }

    return div({class: cl, id: g_id, ...props},
        labelEl,
        inputEl,
        ...children
    );
}
</details> <br />
<a name="formbuilder" />

FormBuilder

export function FormBuilder ({dom, values, rowClass="", formClass="", cols, autoRow, bsSize, id, onChange}={})

The FormBuilder inherits from FormController. With it you can easily create forms in Bootstrap grid format.

Usage

var fb = FormBuilder();
var fbValues = van.state({});
fb.onChange( () => {fbValues.val = {...fb.values}} );

fb.addRow("m-2 p-2 border border-primary rounded-2 ");
fb.add({label: 'Name 1', name: 'name1', cols: "2 4"});
fb.add({label: 'Name 2', name: 'name2', cols: "2 4"});

...

return div({class: 'row p-2 border'},
    fb.dom,  // form dom
    div({class: "row mt-1"},
        p(JSON.stringify(fbValues.val),  // show form values
        ()=>fb.emitChange(),    // effect to call fb.onChange to show initial values
    ),
)

FormBuilder Args

argcomment
domContainer for the form groups to be inserted. If omitted a <form class=formClass /> is created.
valuesOptional initial values as object {name: value,..}
formClassClass for the generated form tag
rowClassClass for the generated rows if autoRow is true
colsDefault cols attribute for addRow
autoRowIf true, will add a new row if the columns in a row exceeds 12
bsSizeDefault Bootstrap size for inserted components. '' or 'sm' or 'lg'
idHTML id of the form. Random id if omitted. It is the prefix for label id (formId-groupId-l) and input id (formId-groupId-i)
onChangeOptional callback function(name,value) for input change

FormBuilder Props

Properties inherited from FormControl.

propertycomment
valuesinput values as object {name: value,..}
onChange(func)Subscribe input value change events. func=function(name,value)
emitChange(name, value)Notify change to all subscribers
handleInput(event)input element oninpup(event) handler
args(props)Function returns completetd props for a input control. Generate 'value', 'oninput' and 'name'.

Own properties.

propertycomment
domContainer for the form groups to be inserted.
rowActual row element
rowClassDefault ow class
colsDefault cols attribute for inserted groups
autoRowIf true, will add a new row if the columns in a row exceeds 12
colCountNumber of colums in actual row
idForm tag id
validContainer for form validation
onvalidateContainer for form validation
add(props, ...children)The main function used to insert a form group.
addGroup(args,...children)Finally insert a group
addRow(arg)Add a new row with ...args injected. i.e. addRow({class: "m-2"})
getFormValid()Function returns true/false when all input values are valid

Function add(props, ...children)

The main function used to insert a form group. The props argument decides how the inserted form group is created. The further arguments are added as an appendix to the input component.

Add Properties

propertycomment
namestring becomes input name
labelstring or func becomes a label component
typeType attribute for the input control
oninputOptional for special usage. Is generally generated automatically
inputa input control alternative to type. See Custom Input
classclass of the enveloping group
inputClassclass of the input control
bsSizeBootstrap size
colsspace separated string "left right" for the Bootstrap column sizes i.e. "3 6"
colBootstrap column size of the group alternative to cols. Used for form class "row g-3"
idHTML id of group. Will get random id if omited. Label id is "formId-id-l", input id is "formId-id-i"
separatedspecial for FormCheck. Separate label and check if true
isvalidBoolean add :is-valid or :is-invalid to input class
onvalidatefunction(value) {return value_is-valid }. Add a validation check for input and add isvalid argument

See also Form Layout and Form Validation in the online demo.

<details> <summary>Show FormBuilder Code</summary>

FormBuilder Code

export function FormBuilder ({dom, values, rowClass="", formClass="", cols, autoRow, bsSize, id, onChange}={}) {
    
    const toInt = v => {let n=Number(v); return isNaN(n) ? 0 :n};
    
    const splitCols = c => {let [l, r] = c.split(' '); l=toInt(l); r=toInt(r);
       r = r ? r : 12-l; 
       return {l,r,t:l+r,s:l+' '+r}
    };

    var fc = FormController({values});
    onChange && fc.onChange(onChange);

    var self = {
        ...fc,
        dom: dom ?? van.tags.form({class: formClass}),
        row: null,
        rowClass,
        cols,
        autoRow,
        colCount: 0,
        id: id ?? Math.random().toString(36).substring(2, 9),

        valid: {},
        onvalidate: {},
        getFormValid: () => Object.keys(self.valid).every( k => self.valid[k].val ),

        add (props, ...children) {
            let {name, value, oninput, cols, onvalidate, ...rest} = props;
            if(value === undefined && name) value = self.values[name];
            let args = {...rest, ...fc.args({name, value, oninput}) };
            args.id = args.id || self.id + '-' + name;
            if(bsSize && args.bsSize === undefined) args.bsSize = bsSize;
            args.cols = cols || self.cols;
            if(self.autoRow) {
                cols = splitCols(args.cols);
                if(!self.row) { self.addRow(); self.colCount = 0; } 
                self.colCount += cols.t;
                args.cols = cols.s;
                if(self.colCount > 12) {
                    self.addRow();
                    self.colCount = cols.t;
                }
            }

            if(onvalidate) {
                self.onvalidate[args.name] = onvalidate;
                self.valid[args.name] = van.state(onvalidate(self.values[args.name]));
                args.isvalid = self.valid[args.name];
            }
    
            self.addGroup(args,...children);
            return self;
        },
        addGroup(args,...children) {
            van.add(self.row ?? self.dom, FormGroup(args,...children));
        },
        addRow (arg) {
            if(arg === null) return self.row=null;
            self.row = div({class: "row" + (self.rowClass ? ' '+self.rowClass : '') + (arg ? ' '+arg : '')});
            van.add(self.dom, self.row);
            return self;
        },
    }

    self.onChange((name,value) => {
        if(self.onvalidate[name]) self.valid[name].val = self.onvalidate[name](value);
    });

    return self;
}
</details> <br />
<a name="inputcontrols" />

Input Controls

The special Bootstrap form controls are implemented in the library and can be imported. In the FormGroup the controls are specified as “type”. The mapping is the Map "typeMap".

A special feature of SelectInput, RadioSelectInput and ComboboxInput is that the options are not specified as children, but as an attribute "options". The value here is a list. See selectOptions for details.

API

export function Input({class: clas, bsSize, ...props})

export function FormLabel({class: clas, bsSize, col, ...props}, children)

export function Textarea({class: clas, bsSize, ...props})

export function SelectInput ({class: clas, bsSize, ...props})

export function FormCheckInput ({label: clabel, type, class: clas, style, bsSize, reverse, id, value, ...props})

export const SwitchInput = props => FormCheckInput({...props, type: 'switch'})

export const CheckboxInput = props => FormCheckInput({...props, type: 'checkbox'})`

export const RadioInput = props => FormCheckInput({...props, type: 'radio'})

export function ComboboxInput (options = [], style, bsSize, onItemClick, ...props)

export function RadioSelectInput ({options, class: clas, bsSize, inline, id, ...props})

typeMap

typecontrol
'text'Input
'textarea'Textarea
'select'SelectInput
'radioselect'RadioSelectInput
'checkbox'CheckboxInput
'radio'RadioInput
'switch'FormCheckInput
'combobox'ComboboxInput
<br />
<a name="custominput" />

Custom Input

This example shows how to build a custom input control and register it as type 'align' for FormBuilder.

AlignInput Source

import van from 'vanjs-core';
import {Button} from 'vanjs-bootstrap';


// const t = i18n.tPath('align-input');        // use current language
const t = t=>t;                                // use dummy translate

const {svg, path} = van.tagsNS("http://www.w3.org/2000/svg");

// define button props
const LEFT = {
    value:      'left',
    title:      'align left',
    icon:       svg({width: "1em", height: "1em", viewBox: "0 0 448 512", stroke:"currentColor", fill:"currentColor"},
        path({d: "M288 64c0 17.7-14.3 32-32 32H32C14.3 96 0 81.7 0 64S14.3 32 32 32H256c17.7 0 32 14.3 32 32zm0 256c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H256c17.7 0 32 14.3 32 32zM0 192c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 448c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"})
    ),
}

const CENTER = {
    value:      'center',
    title:      'align center',
    icon:       svg({width: "1em", height: "1em", viewBox: "0 0 448 512", stroke:"currentColor", fill:"currentColor"},
        path({d: "M352 64c0-17.7-14.3-32-32-32H128c-17.7 0-32 14.3-32 32s14.3 32 32 32H320c17.7 0 32-14.3 32-32zm96 128c0-17.7-14.3-32-32-32H32c-17.7 0-32 14.3-32 32s14.3 32 32 32H416c17.7 0 32-14.3 32-32zM0 448c0 17.7 14.3 32 32 32H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H32c-17.7 0-32 14.3-32 32zM352 320c0-17.7-14.3-32-32-32H128c-17.7 0-32 14.3-32 32s14.3 32 32 32H320c17.7 0 32-14.3 32-32z"})
    ),
}

const RIGHT = {
    value:      'right',
    title:      'align right',
    icon:       svg({width: "1em", height: "1em", viewBox: "0 0 448 512", stroke:"currentColor", fill:"currentColor"},
        path({d: "M448 64c0 17.7-14.3 32-32 32H192c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32zm0 256c0 17.7-14.3 32-32 32H192c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32zM0 192c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 448c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"})
    ),
}

const JUSTIFY = {
    value:      'justify',
    title:      'align justify',
    icon:       svg({width: "1em", height: "1em", viewBox: "0 0 448 512", stroke:"currentColor", fill:"currentColor"},
        path({d: "M448 64c0-17.7-14.3-32-32-32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32zm0 256c0-17.7-14.3-32-32-32H32c-17.7 0-32 14.3-32 32s14.3 32 32 32H416c17.7 0 32-14.3 32-32zM0 192c0 17.7 14.3 32 32 32H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H32c-17.7 0-32 14.3-32 32zM448 448c0-17.7-14.3-32-32-32H32c-17.7 0-32 14.3-32 32s14.3 32 32 32H416c17.7 0 32-14.3 32-32z"})
    ),
}

// validate function
const isValidValue = (value) => [LEFT.value, CENTER.value, RIGHT.value,JUSTIFY.value].includes(value);


// the custom align control

export default function AlignInput ({value, bsSize, class: clas, style, color="primary", ...props}) {

    const type = 'textalign';   // input type

    var select = {
        left:   van.state(),
        right:  van.state(),
        center: van.state(),
        justify:van.state()
    }
    const setSelect = value => Object.keys(select).forEach( k => select[k].val = k !== value); 

    if (!isValidValue(value)) value = 'left'; // default
    setSelect(value);

    // setup bootstrap control frame
    var divProps = {
        className:  () => {           // merge className
            let res = "d-flex flex-row form-control"; 
            if(van.val(bsSize)) res += ' form-control-' + van.val(bsSize);
            if(van.val(clas)) res += ' ' + van.val(clas);
            return res;
        },
        style: () => {                   // merge style
            let res = "max-width: max-content";
            if(van.val(style)) res += '; ' + van.val(style);
            return res;
        }
    };

    // setup button props
    const Btn = (param) => Button({
        bsSize,
        class: "py-1 px-2 me-1",
        onclick: () => {
            props.oninput({
                target: {
                    name:   props.name,
                    type,
                    value:  param.value
                }                
            });
            setSelect(param.value);
        },
        color,
        outline: select[param.value],
        title:  t(param.title)
    }, param.icon);
    
    // return component
    return van.tags.div({...divProps},
        Btn(LEFT), Btn(RIGHT), Btn(CENTER), Btn(JUSTIFY),
    )
}

Demo Source

import van from 'vanjs-core';
import {FormBuilder, typeMap} from 'vanjs-bootstrap';
import AlignInput from './align-input';

const {div, h3, h5} = van.tags;

typeMap.set('align', AlignInput);  // register AlignInput as type 'align';

export default function demo () {

    const state = {
        bsSize: van.state(''),
        color:  van.state('primary'),
        align:  van.state('left'),
    }

    const fb = FormBuilder(); // create builder

    fb.onChange( (name,value) => {state[name].val = value} );

    fb.addRow("m-2");
    fb.add({label: 'bsSize', name: 'bsSize', type: 'radioselect', cols: "2 4", bsSize: 'sm',
        options: [['none',''],'sm','lg'], inline: true
    });
    fb.add({label: 'color', name: 'color', type: 'select', cols: "2 4", bsSize: 'sm',
        options: 'primary,secondary,success,danger,warning,info,light,dark,link',
    });

    fb.addRow("m-2");
    fb.add({label: 'Align', name: 'align', type: 'align', cols: "2 5", bsSize: state.bsSize, color: state.color});

    return div(
        h3('Align Demo'),
        div({class: "row my-4 border rounded-2"},
            fb.dom,
            h5(() => `align value: ${state.align.val}`)
        ),
    )
}

<br />
<a name="navbar" />

Navbar

This component simplifies the use of the Bootstrap navigation bar.

function Navbar({class: clas, sticky = true, menu, t = t=>t, ...props})

function NavItem({href='#', label, icon, items, level=0, Comp, hidden=false, t=t=>t, ...props})

function NavLink({label, class: clas, active, disabled, divider, level=0, ...props})

function NavMenu ({label, items, t=t=>t, level, ...props})

Navbar Arguments

item props

The object 'item' is passed as an argument to the function NavItem.

Usage Examples

See nav bar above in this app.

App Code

import van from 'vanjs-core';
import Navbar from 'vanjs-bootstrap';
import ToggleTheme from './app/toggle-theme';
import Page from './app/home';
import {tPath} from './i18n';

const {div} = van.tags;

const appMenu = {

    brand: van.tags.a({ class: "nav-brand me-3", href: "#", onclick: ()=>false },
            van.tags.img({ src: "img/form-lib.svg", alt: "Home", height: "30" }),
    ),
    
    items: [
        {label: 'home', href: '#home'},
        {label: 'Form', items: [
            {label: 'formbuilder', href: '#formbuilder'},
            {label: 'aligndemo', href: '#aligndemo'},
        ]},
        {label: 'about', href: '#about'},
        {label: 'theme', Comp: ToggleTheme},
    ]
}


function AppNav () {
    return Navbar({t: tPath('nav'), menu: appMenu})
}

export default function App() {
    return () => div(
        AppNav,
        div({class: "container"},
            Page
        )
    )
}

ToggleTheme Code

import van from 'vanjs-core';
import {NavLink} from 'vanjs-bootstrap';
import {tPath} from '../i18n';

const {svg, path} = van.tagsNS("http://www.w3.org/2000/svg");

const sun = svg({width: "1em", height: "1em", fill: "currentColor", viewBox: "0 0 16 16"},
    path({d: "M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6m0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8M8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0m0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13m8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5M3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8m10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0m-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707M4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"})
);
const moon = svg({width: "1em", height: "1em", fill: "currentColor", viewBox: "0 0 16 16"},
    path({d: "M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278M4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063-4.61 0-8.343-3.714-8.343-8.29 0-1.167.242-2.278.681-3.286z"})
);

export default function ToggleTheme () {
    const t = tPath('theme');
    const toggle = (v) => v === 'dark' ? 'light' : 'dark';
    const getTheme = () => document.documentElement.getAttribute('data-bs-theme');
    const setTheme = (v) => document.documentElement.setAttribute('data-bs-theme', v);
    const btnColor = van.state(toggle(getTheme()));
    const toggleClick = () => {
        btnColor.val = toggle(btnColor.val);
        setTheme(toggle(getTheme()));
        return false;
    }
    const Icon = v => v === 'dark' ? sun : moon;

    return NavLink({
        label: Icon(btnColor.val), 
        onclick: toggleClick,
        title: t(`${btnColor.val}-title`),
    });
}
<br />
<a name="modal" />

Modal

Work with Bootstrap Modal.

ModalFrame

export function ModalFrame (options)

The function ModalFrame creates a complete modal dom struture like:

<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="exampleModalLabel">Modal title</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <p>Modal body text goes here.</p>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Save changes</button>
      </div>
    </div>
  </div>
</div>

You only need to specify the van functions for header, body and footer:

div(
    // Button trigger modal
    Button({"data-bs-toggle": "modal", "data-bs-target": "#exampleModal"}, "Launch demo modal"),

    // Modal
    ModalFrame({
        id:      "exampleModal",
        header:  "Modal title",
        body:    "Modal body text goes here.",
        footer: [
            Button({"data-bs-dismiss": "modal"}, 'Close'),
            Button({color: "primary"}, 'Save changes'),
        ]
    })
);

ModalFrame Options

<br/>

Modal Controller

You can achieve more flexibility by using the Modal function.

export function Modal (args, options)

Where args are the options used for ModalFrame and options are controller options. The function returns an object with properties and functions.

Modal Controller Object

You can also use the bootstrap events

dom.addEventListener('shown.bs.modal', onShown)

Modal Controller Options

<br/>

Confirm Example

const ConfirmDlg = Modal({
    close: false,               // don't show any header
    color: "text-bg-danger",
    centered: false,
    body: h5("Are you sure ?"),
    footer: div({class: ""},
        Button({onclick: ()=>ConfirmDlg.close(true), bsSize: "sm", class: "me-2"}, 'Yes'),
        Button({onclick: ()=>ConfirmDlg.close(false), bsSize: "sm"}, 'No'),
    )
});

const DemoConfirm = () => {
    const result = van.state("");
    return div({class: "row"},
        div({class: "col-3"},
            Button({
                onclick: async () => {
                    result.val = await ConfirmDlg.asyncOpen();
                }
            }, "Confirm")
        ),
        div({class: "col-3"}, () => `Result: ${result.val}`),
    )
}

Form Example


const LoginDlg = ({name, pw}) => {
    var fb = FormBuilder();
    fb.add({label: 'Name', name: 'name', value: name, id: 'login-dlg-name'});
    fb.add({label: 'Password', name: 'pw', value: pw, type: 'password'});

    var dlg = Modal({
        header: 'Login',
        body: fb.dom,
        footer: div({class: "btn-group btn-group-sm"},
            Button({onclick: ()=>dlg.close(fb.values)}, 'Ok'),
            Button({onclick: ()=>dlg.close(false)}, 'Cancel'),
        )
    });

    // close on Enter pressed
    fb.dom.addEventListener('keypress', event => {
        if (event.key === 'Enter') dlg.close(fb.values);
    });

    // focus first input after open
    dlg.onShown = () => {
        document.getElementById(fb.id + '-name-i').focus()
    };

    return dlg;
};

const DemoForm = () => {
    const result = van.state({name: 'Jack', pw: 'secret'});
    return div({class: "row"},
        div({class: "col-3"},
            Button({onclick: async () => {
                let res = await LoginDlg(result.val).asyncOpen();
                if (res) result.val = res;
            }
            }, "Login")
        ),
        div({class: "col-3"}, () => `Result: ${JSON.stringify(result.val)}`),
    )
}

<br />
<a name="menu" />

Menu

In addition to the dropdown menu, a popup menu and a context menu are also implemented based on the modal dialog. The MenuItem function simplifies the creation of menus.

MenuItem

export function MenuItem ({label, header, divider, text, active, disabled, tag, class, ...props})

This function returns dom like

<li>
    <button class="dropdown-item" role="button" .../>label</button>
</li>

MenuItem Props

DropdownMenu

export function DropdownMenu (...items)

For items functions or objects are accepted.

 DropdownMenu(
    {label: 'Header', header:1},
    MenuItem({label: 'Menu Item', onclick:()=>result.val='Menu Item'}),
    {label: 'Item1',onclick:()=>result.val='Item1'},
)

Dropdown Example

const DropdownDemo = () => {
    const result = van.state("");
    return div({class: "row"},
        div({class: "col-3"},
            div({class: "dropdown"},
                Button({dropdown: true}, "Dropdown button"),
                DropdownMenu(
                    {label: 'Header', header:1},
                    {label: 'Item1',onclick:()=>result.val='Item1'},
                    {label: 'active item',active:1,onclick:()=>result.val='active item'},
                    {label: 'Item2',onclick:()=>result.val='Item2'},
                    {divider:1},
                    {label: 'Text',text:1},
                    {label: 'Disabled',disabled:1},
                )
            )
        ),
        div({class: "col-3"}, () => `Result: ${result.val}`),
    )
}

PopupMenu

export function PopupMenu (...items)

For items functions or objects are accepted. This function returns a Modal object. Call function open to show.

Popup Example

const PopupDemo = () => {
    const result = van.state("");
    return div({class: "row"},
        div({class: "col-3"},
            Button({
                onclick: () => PopupMenu(
                    {label: 'Popup Menu', header:1},
                    MenuItem({label: 'Menu Item', onclick:()=>result.val='Menu Item'}),
                    {label: 'Item1',onclick:()=>result.val='Item1'},
                    {label: 'active item',active:1,onclick:()=>result.val='active item'},
                    {label: 'Item2',onclick:()=>result.val='Item2'},
                    {divider:1},
                    {label: 'Text',text:1},
                    {label: 'Disabled',disabled:1},
                    ).open()
            }, "Popup")
        ),
        div({class: "col-3"}, () => \`Result: \${result.val}\`),
    )
}

ContextMenu

export function ContextMenu (...items)

For items functions or objects are accepted. This function returns a Modal object. Call function open(event) to show. Function open accepts the click event to open the menu at click position.

Context Menu Example

const ContextDemo = () => {
    const result = van.state("");

    const menu = ContextMenu(
        {label: 'Context Menu', header:1},
        MenuItem({label: 'Menu Item', onclick:()=>result.val='Menu Item'}),
        {label: 'Item1',onclick:()=>result.val='Item1'},
        {label: 'Item2',onclick:()=>result.val='Item2'},
    );

    return div({class: "row"},
        div({
            class: "col-12 p-4 mt-3 border rounded-2",
            oncontextmenu: event => menu.open(event),
        },
            h3('right click in this area to open context menu')
        ),
        div({class: "col-3"}, () => \`Result: \${result.val}\`),
    )
}
<br />
<a name="taginput" />

TagInput

A form multi select control to edit a list of tag items.

PropsDefault
valuerequired. Format "a,b,c" or ["a", "b", "c"]
options[]optional array of string to be used in select list<br />or [ ["text1", "value1"], ["text2", "value2"]]<br />comma separated options are allowed<br />see Select Options
oninput( event )required, value change event feedback
commaValuetruefalse: value is array of string<br />true: value is comma separated string
dragSorttrueallow items drag'n drop
delIcon'none'"Remove Cross" position 'left', 'right', 'none'
orderIconfalseshow minus sign as icon
allowCreatetrueenable edit for new items
multitrueenable multi select
closeOnCheckfalseclose option list on check
bsSize'sm'bootstrap size
placeholdershow text if no value
tagColor"text-bg-primary"badges color like "text-bg-primary" or "text-bg-secondary"
loadingshow spinner on "true"
styleextra control style<br/>style:"height:5em;"
icons{}overwrite icons
tt=>toverwrite translate function
...propsextra props
<br/>

Keyboard

icons

icons = {IconDelete, IconCheck, IconUncheck, IconAsc,IconDsc}

For example overwrite IconDelete with trash can

import { TagInput, Icon, SvgStrIcon} from 'vanjs-bootstrap';

const IconDelete = props => Icon(SvgStrIcon(
    '<svg viewBox="0 0 448 512"><path d="M160 400C160 408.8 152.8 416 144 416C135.2 416 128 408.8 128 400V192C128 183.2 135.2 176 144 176C152.8 176 160 183.2 160 192V400zM240 400C240 408.8 232.8 416 224 416C215.2 416 208 408.8 208 400V192C208 183.2 215.2 176 224 176C232.8 176 240 183.2 240 192V400zM320 400C320 408.8 312.8 416 304 416C295.2 416 288 408.8 288 400V192C288 183.2 295.2 176 304 176C312.8 176 320 183.2 320 192V400zM317.5 24.94L354.2 80H424C437.3 80 448 90.75 448 104C448 117.3 437.3 128 424 128H416V432C416 476.2 380.2 512 336 512H112C67.82 512 32 476.2 32 432V128H24C10.75 128 0 117.3 0 104C0 90.75 10.75 80 24 80H93.82L130.5 24.94C140.9 9.357 158.4 0 177.1 0H270.9C289.6 0 307.1 9.358 317.5 24.94H317.5zM151.5 80H296.5L277.5 51.56C276 49.34 273.5 48 270.9 48H177.1C174.5 48 171.1 49.34 170.5 51.56L151.5 80zM80 432C80 449.7 94.33 464 112 464H336C353.7 464 368 449.7 368 432V128H80V432z" /></svg>',
    {style: "vertical-align: bottom;", fill: 'currentColor', stroke: 'currenColor', ...props}
));

const tagInput = TagInput({ value, oninput, icons: {IconDelete}})

t

Use your own translate function.

Example

const myT = t => {
    const i18nWords = {
        'remove':               'entfernen',
        'ascending':            'aufsteigend',
        'descending':           'absteigend',
        'edplaceholder':        'neuen Tag eingeben',
    };
    return i18nWords[t] ?? t;
}

const tagInput = TagInput({ value, oninput, t: myT})

<br/> <details> <summary>Show Demo Code</summary>

import van from 'vanjs-core';
import { TagInput, RadioSelectInput, SelectInput, CheckboxInput } from 'vanjs-bootstrap';
import { Icon, SvgStrIcon} from 'vanjs-bootstrap';

const { div, h2, span } = van.tags;

var bsSize = van.state('md');
var color = van.state('');
var allowCreate = van.state(true);
var multi = van.state(true);
var delIcon = van.state('none');
var orderIcon = van.state(false);
var tagValue = van.state("1,2");
// var tagValue = van.state("1,2,aaa,bbb,ccccccccc,dddddddd,eeeeeee,fffffff,ggggggg,hhhhhhh,iiiiiii,jjjjjjj,kkkkkkk");
var value = tagValue.val;

van.derive( ()=> console.log('tagValue change', tagValue.val))


const IconDelete = props => Icon(SvgStrIcon(
    '<svg viewBox="0 0 448 512"><path d="M160 400C160 408.8 152.8 416 144 416C135.2 416 128 408.8 128 400V192C128 183.2 135.2 176 144 176C152.8 176 160 183.2 160 192V400zM240 400C240 408.8 232.8 416 224 416C215.2 416 208 408.8 208 400V192C208 183.2 215.2 176 224 176C232.8 176 240 183.2 240 192V400zM320 400C320 408.8 312.8 416 304 416C295.2 416 288 408.8 288 400V192C288 183.2 295.2 176 304 176C312.8 176 320 183.2 320 192V400zM317.5 24.94L354.2 80H424C437.3 80 448 90.75 448 104C448 117.3 437.3 128 424 128H416V432C416 476.2 380.2 512 336 512H112C67.82 512 32 476.2 32 432V128H24C10.75 128 0 117.3 0 104C0 90.75 10.75 80 24 80H93.82L130.5 24.94C140.9 9.357 158.4 0 177.1 0H270.9C289.6 0 307.1 9.358 317.5 24.94H317.5zM151.5 80H296.5L277.5 51.56C276 49.34 273.5 48 270.9 48H177.1C174.5 48 171.1 49.34 170.5 51.56L151.5 80zM80 432C80 449.7 94.33 464 112 464H336C353.7 464 368 449.7 368 432V128H80V432z" /></svg>',
    {style: "vertical-align: bottom;", fill: 'currentColor', stroke: 'currenColor', ...props}
));

const myT = t => {
    const i18nWords = {
        'remove':               'entfernen',
        'ascending':            'aufsteigend',
        'descending':           'absteigend',
        'edplaceholder':        'neuen Tag eingeben',
    };
    return i18nWords[t] ?? t;
}

export default function Page() {

    const OptionsBar = div(
        div({ class: "input-group input-group-sm" },

            span({ class: "input-group-text" }, 'bsSize'),
            RadioSelectInput({
                value: bsSize.val,
                oninput: e => bsSize.val = e.target.value,
                options: 'sm,md,lg', 
                // inline: true,
            }),

            span({ class: "input-group-text" }, 'tagColor'),
            SelectInput({
                value: color.val,
                oninput: e => color.val = e.target.value,
                options: ",text-bg-primary,text-bg-secondary,text-bg-success,text-bg-danger,text-bg-warning,text-bg-info,text-bg-light,text-bg-dark",
                style: "max-width: 10em",
            }),

            span({ class: "input-group-text" }, 'delIcon'),
            RadioSelectInput({
                value: delIcon.val,
                oninput: e => delIcon.val = e.target.value,
                options: 'none,left,right', 
                // inline: true,
            }),


            span({ class: "input-group-text" }, 'orderIcon'),
            CbControl({ value: orderIcon.val, oninput: e => orderIcon.val = e.target.value}),

            span({ class: "input-group-text" }, 'multi'),
            CbControl({ value: multi.val, oninput: e => multi.val = e.target.value}),

            span({ class: "input-group-text" }, 'allowCreate'),
            CbControl({ value: allowCreate.val, oninput: e => allowCreate.val = e.target.value}),

        ),
    );

    return div({},
        div({class: "row"},
            h2('TagInput Demo'),
            OptionsBar,
            div({class: "col"},
                TagInput({ bsSize, tagColor: color.val, multi: multi.val,
                allowCreate: allowCreate.val,
                orderIcon: orderIcon.val,
                class: "mt-3",
                placeholder: "placeholder",
                options: [["one","1"],["two","2"],["three","3"],"","x","y"],
                value,
                oninput: ev => {tagValue.val = ev.target.value; value = ev.target.value},
                delIcon: delIcon.val,
                // loading:    true,
                tabindex: "1",
                icons: {IconDelete},
                t: myT,
            }),
            ),
        ),
        ValueState,
    )
}


function ValueState() {
    return div({class: "row"},
        div({class: "col"}, ()=>`value: ${tagValue.val}` )
    )
}


function CbControl({class: clas, bsSize, ...props}) {
    return div(
        {class: "form-control d-flex align-items-center", style: "max-width: 2.25em"},
        CheckboxInput(props)
    )
}
</details> <br />
<a name="dragsort" />

DragSort

A helper to sort a list per drag and drop.

export function DragSort (list, setList)

Function DragSort returns an object with all required function. The function dragProps of the object can inject all html attributes like draggable, ondragstart and so on.

The function used to be a React Hook, but works great in VanJs and is now used in this library by TagInput.

During dragging CSS :dragging is set.

Usage example:

import van          from 'vanjs-core';
import { DragSort } from 'vanjs-bootstrap';

const {div, h2} = van.tags;

const gitems = van.state(["A","B","C"])

function MyList () {
    const drag = DragSort(gitems.val, v => gitems.val=v);  // create drag sort object
    const items = gitems.val.map( (item,index) => {
        return van.tags.li({
            class: () => "list-group-item",
            ...drag.dragProps(index)                // inject properties
            },
            item
        )
    });
    return van.tags.ul({class: "list-group"}, ...items)
}

export default function Page() {
    return div({},
        h2('DragSort Demo'),
        MyList,
    )
}

<br />
<a name="i18n" />

i18n

Translation tool. A very simple object for multilingual texts. The texts are stored in the variable dict and are divided into language and chapter.

The current language is set with the setLanguage function. The texts are retrieved with the function t. Texts are added with the addWords function.

Interface

Example

Create a translation instance and add imported JSON files.

import {I18n}  from 'vanjs-bootstrap';
import lang_en from './en.json';
import lang_de from './de.json';

const i18n = I18n();

i18n.addWords( lang_en, 'en' );
i18n.addWords( lang_de, 'de' );

let nl = (navigator?.language || 'en').substring(0,2);
i18n.setLanguage(nl);

const {t, tPath, getLanguage, setLanguage} = i18n;
export { t, tPath, getLanguage, setLanguage };
<p/>

Toggle language in your App.

import van  from 'vanjs-core';
import { tPath, getLanguage, setLanguage }  from './i18n'

const tNav = tPath('nav');  // translate function for chapter 'nav'

const lang = van.state(getLanguage());

// use this in NavBar
const toggleLanguage = () => a({
    class: "nav-link", 
    href: "#", 
    onclick: () => {
        let l = (lang.val === 'en') ? 'de' : 'en'
        setLanguage(l);
        lang.val = l;
        return false;
    },
    title: tNav('lang-title') 
    }, 
    tNav('lang')
);


export default function App() {
    return () => div(
        {lang: lang.val}, // inject lang will redraw app on language change
        AppNav,
        div({class: "container"}, 
            div({class: "row justify-content-md-center"},
                div({class: "col-10 p-4 m-2 border rounded-2"}, 
                ctrl.pageDom.val())
            )
        ),
    )
}
<br />
<a name="icons" />

Icons

Icons, the small images used on buttons or in the text, make the UI appealing and clear. One or more libraries such as Font Awesom, Bootstrap or Simple Icons serve as the source. However, it is not uncommon for additional icons to be required that you create yourself, such as your own logo.

The library contains simple functions for displaying and managing icons from different sources.

Icon

function Icon( anything, {size, ...props} )

The function icon displays icons on different sources.

  anything = GenIcon({tag: "svg",attr: {viewBox:"0 0 30 10"},child: [  
    {tag: "circle", attr: {cx:"5", cy:"5", r:"3", stroke:"green"}},  
    {tag: "circle", attr: {cx:"15", cy:"5", r:"3", stroke:"green", "stroke-width": "3"}}  
    ]})  
  const {path} = van.tagsNS("http://www.w3.org/2000/svg");  
  anything = props => SvgIconBase({viewBox: "0 0 16 16", ...props},  
    path({d: "M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6m0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8M8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0m0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13m8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5M3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8m10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0m-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707M4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"})  
  );  

Argument size puts width and height and is a CSS value.

size = '16px' || '2.5em' || '60%' || 'inherit'

Other '...props' like 'class' or 'style' are passed on.


Icon Map

export var iconMap = new Map()

Variable iconMap is a JavaScript Map to store icon by key. It is recommended to store icons here and display them with Icon('key'). The best approach is to create an icon library.

file 'icons.js'

import van from 'vanjs-core';
import {setIcon, ImgIcon, SvgStrIcon, SvgIconBase} from 'vanjs-bootstrap';
import FormLogo from './form-lib.svg?raw';

const {path} = van.tagsNS("http://www.w3.org/2000/svg");

const sun = props => SvgIconBase({viewBox: "0 0 16 16", ...props},
    path({d: "M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6m0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8M8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0m0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13m8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5M3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8m10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0m-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707M4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"})
);

const Beer = '<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M352 200v240a40.12 40.12 0 01-40 40H136a40.12 40.12 0 01-40-40V224"></path><path fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M352 224h40a56.16 56.16 0 0156 56v80a56.16 56.16 0 01-56 56h-40"></path><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M224 256v160m64-160v160M160 256v160m160-304a48 48 0 010 96c-13.25 0-29.31-7.31-38-16H160c-8 22-27 32-48 32a48 48 0 010-96 47.91 47.91 0 0126 9"></path><path fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M91.86 132.43a40 40 0 1160.46-52S160 91 160 96m-14.17-31.29C163.22 44.89 187.57 32 216 32c52.38 0 94 42.84 94 95.21a95 95 0 01-1.67 17.79"></path></svg>';


setIcon('VANLOGO', ImgIcon({src: "img/vanjs.svg"}));
setIcon('BEER', SvgStrIcon(Beer));
setIcon('BEER_BLUE', SvgStrIcon(Beer, {fill: 'blue', stroke: 'blue'}));
setIcon('FORM', SvgStrIcon(FormLogo, {fill: 'currentColor', stroke: 'currentColor'}));
setIcon('SUN', sun)

file 'app.js'

import van from 'vanjs-core';
import {Icon} from 'vanjs-bootstrap';
import './icons';  // initialize iconMap

const {div, h1} = van.tags;

export default function App() {
    return () => div(
      h1('VanJs ',Icon('VANLOGO'),' is nice'),
    )
}

function setIcon (key, icon)

Sets an icon to a key. icon should be a function to pass arguments of the function Icon. Icons can also be derived from other icons.

setIcon( 'LOGO', props => Icon('VANLOGO', props) )

function getIcon (key)

Gets an icon by key.

function setWarning (value)

This is useful for debugging. Value is

Show Icons Demo

Use this Button to display all icons.

import van from 'vanjs-core';
import {Button, Modal, Icon, iconMap} from 'vanjs-bootstrap';

function ShowIconMap() {
    const keys  = [...iconMap.keys()];
    const {div} = van.tags;
    const dlg = Modal({
        header: "Icon Map",
        body: div(
            ...keys.map( key => div({class: "row"},
                div({class: "col"}, key ),
                div({class: "col bg-white text-black"}, Icon(key) ),
                div({class: "col bg-black text-white"}, Icon(key) ),
                div({class: "col col bg-secondary text-black"}, Icon(key) ),
            ))
        ),
        scrollable: true,
    });

    return Button({onclick: ()=>dlg.open()}, "Show Icons");
}

Icon Transformers

function SvgIconBase (props = {}, ...children)

Is a base component wrapper for SVG icons build as VanJs function. The idea behind is to have a SVG image that size is 1em. So it fit to font-size. "fill" should be "currentColor" to use the font-color. The icon size (width and height) can be changed by "size" property.

main props

function MyIcon (props) {
  const {rect} = van.tagsNS("http://www.w3.org/2000/svg");
  return SvgIconBase({viewBox: "0 0 100 100", ...props},
    rect({x:"11.5", y:"9.4", width:"80", height:"80", rx:"4.4444", ry:"4.4444" stroke-width:"8"})
  )
}

function GenIcon(data)

This function returns a Icon function, where data is a svg data tree:

{tag: "svg", attr: {viewBox: "0 0 16 16"}, child: [{tag: ..}, ..]}

This data format was found by react-icons. The advantage is the generality of this format, which can be easily transformed, here into a VanJs function. The first tag is usually "svg". In this function, if the first tag is missing, an SvgIconBase function is generated, otherwise a standard SVG function.

const moon = GenIcon({
  attr: {viewBox: "0 0 16 16"},
  child:[
    {tag: 'path', attr: {d: "M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278M4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063-4.61 0-8.343-3.714-8.343-8.29 0-1.167.242-2.278.681-3.286z"}}
  ]
});

function ImgIcon ({src, alt, ...rest})

Create a Icon function by image url. The "size" is injected as style so we can use units like "em". For original size use "inherit". If "alt" is not specified, the last part of the URL is used for this

`setIcon( 'KEY', ImgIcon({src: "img/key.png"}))``

function SvgStrIcon (str, svgargs = {})

Create a Icon function from a SVG string. svgargs will overwrite svg attributes like fill and stroke.

SvgStrIcon(
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"><circle cx="5" cy="5" r="3"/></svg>',
  {fill: 'currentColor', stroke: 'currenColor'}
)
<br />
<a name="selectoptions" />

SelectOptions

function selectOptions (list, dom=false, selected)

All form controls in this library use the selectOptions function. This converts different kind of lists into a uniform format that can easily be mapped into a dom. Is the dom parameter true, the function returns a ready mapped dom option list.

The list parameter accepts four notations:

  1. 'TextValue1,TextValue2'
    a komma separated list
  2. ['TextValue1', 'TextValue2'...]
    a array of values
  3. [['Text1', Value1], ['Text2', Value2]...]
    a array of display / value pairs
  4. [ { value: 1, ['children' || 'text' || 'displayValue'] }
    a array of objects

Notations can be mixed inside a array.

For example [ '', ['text 1', 1], ['text 2', 2] ] will be converted to:

[
  {
    value: '',
    children: '',
  },
  {
    value: 1,
    children: 'text 1',
  },
  {
    value: 2,
    children: 'text 2',
  }
]

and with dom=true and selected: "1":

[
 option({value=""}),
 option({value="1", selected: true}, "text 1"),
 option({value="2"}, "text 2"),
]

an other example:

import {selectOptions} from 'lib';
..
return input({type: 'select', value: fontName}, selectOptions(fontNames, true, fontName))
<br />
<a name="history" />

History

<details> <summary>older</summary> </details>