From d2baaf175141e22408063d0d9279859763f13c4c Mon Sep 17 00:00:00 2001 From: Erik Vullings Date: Mon, 13 Jan 2025 15:41:34 +0100 Subject: [PATCH] Simplified element creation in mithril --- packages/example/src/app.ts | 2 + .../src/components/buttons/button-page.ts | 20 +++- .../src/components/inputs/input-page.ts | 15 ++- .../example/src/components/misc/misc-page.ts | 20 ++-- .../src/components/pickers/picker-page.ts | 6 +- packages/lib/src/autocomplete.ts | 11 +- packages/lib/src/button.ts | 47 +++++--- packages/lib/src/carousel.ts | 8 +- packages/lib/src/chip.ts | 91 ++++++++------ packages/lib/src/code-block.ts | 12 +- packages/lib/src/collection.ts | 18 ++- packages/lib/src/dropdown.ts | 12 +- packages/lib/src/input.ts | 51 +++++--- packages/lib/src/label.ts | 21 +++- packages/lib/src/material-box.ts | 7 +- packages/lib/src/modal.ts | 44 ++++--- packages/lib/src/option.ts | 4 +- packages/lib/src/parallax.ts | 2 +- packages/lib/src/pickers.ts | 36 ++++-- packages/lib/src/radio.ts | 16 ++- packages/lib/src/switch.ts | 10 +- packages/lib/src/tabs.ts | 37 +++--- packages/lib/src/timeline.ts | 6 +- packages/lib/src/utils.ts | 112 +----------------- 24 files changed, 317 insertions(+), 291 deletions(-) diff --git a/packages/example/src/app.ts b/packages/example/src/app.ts index daec717..c0af19a 100644 --- a/packages/example/src/app.ts +++ b/packages/example/src/app.ts @@ -7,4 +7,6 @@ import { dashboardSvc } from './services/dashboard-service'; // import '@materializecss/materialize/dist/css/materialize.min.css'; // import '/home/erik/dev/mithril-materialized/node_modules/.pnpm/@materializecss+materialize@2.0.1-alpha/node_modules/@materializecss/materialize/dist/css/materialize.min.css'; +document.documentElement.setAttribute('lang', 'en'); + m.route(document.body, dashboardSvc.defaultRoute, dashboardSvc.routingTable); diff --git a/packages/example/src/components/buttons/button-page.ts b/packages/example/src/components/buttons/button-page.ts index 469e87e..10db7e1 100644 --- a/packages/example/src/components/buttons/button-page.ts +++ b/packages/example/src/components/buttons/button-page.ts @@ -61,15 +61,27 @@ export const ButtonPage = () => { m(Button, { label: 'First Button', onclick }), m(Button, { label: 'Second Button', iconName: 'cloud', onclick }), m(Button, { label: 'Third Button', iconName: 'cloud', iconClass: 'right', onclick }), + m(Button, { + label: 'Fourth Button', + iconName: 'cloud', + attr: { disabled: true }, + onclick, + }), ]), m(CodeBlock, { code: [ `const onclick = () => alert('Button clicked'); m('div', [ - m(Button, { label: 'Button', onclick }), - m(Button, { label: 'Button', iconName: 'cloud', onclick }), - m(Button, { label: 'Button', iconName: 'cloud', iconClass: 'right', onclick }), -]),`, + m(Button, { label: 'First Button', onclick }), + m(Button, { label: 'Second Button', iconName: 'cloud', onclick }), + m(Button, { label: 'Third Button', iconName: 'cloud', iconClass: 'right', onclick }), + m(Button, { + label: 'Fourth Button', + iconName: 'cloud', + attr: { disabled: true }, + onclick, + }), +])`, ], }), m('h3.header[id=flatbutton]', 'FlatButton'), diff --git a/packages/example/src/components/inputs/input-page.ts b/packages/example/src/components/inputs/input-page.ts index 0ae6b16..9f52863 100644 --- a/packages/example/src/components/inputs/input-page.ts +++ b/packages/example/src/components/inputs/input-page.ts @@ -16,7 +16,7 @@ import { import m from 'mithril'; export const InputPage = () => { - const onchange = (v: unknown) => alert(`Input changed. New value: ${v}`); + const onchange = (v: unknown) => console.log(`Input changed. New value: ${v}`); let value = 'click_clear_to_remove.me'; return { view: () => @@ -28,6 +28,7 @@ export const InputPage = () => { '.row', m(TextInput, { label: 'What is your name?', + required: true, helperText: 'Please, be honest!', onchange, autocomplete: 'off', @@ -39,6 +40,7 @@ export const InputPage = () => { m(CodeBlock, { code: ` m(TextInput, { label: 'What is your name?', + required: true, helperText: 'Please, be honest!', onchange, onkeyup: (ev, value) => console.log(value), @@ -93,8 +95,9 @@ export const InputPage = () => { Apple: null, Google: null, Facebook: null, - PHILIPS: 'http://hdlighting-suriname.com/wp-content/uploads/2013/12/philips.png', - TNO: 'https://github.com/TNOCS/spec-tool/raw/master/src/assets/tno.png', + PHILIPS: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8c/Philips_logo.svg/800px-Philips_logo.svg.png', + TNO: 'https://tno.github.io/crime_scripts/f418cfa539199976.svg', }, onchange, }) @@ -107,8 +110,8 @@ export const InputPage = () => { Apple: null, Google: null, Facebook: null, - PHILIPS: 'http://hdlighting-suriname.com/wp-content/uploads/2013/12/philips.png', - TNO: 'https://github.com/TNOCS/spec-tool/raw/master/src/assets/tno.png', + PHILIPS: 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8c/Philips_logo.svg/800px-Philips_logo.svg.png', + TNO: 'https://tno.github.io/crime_scripts/f418cfa539199976.svg', }, onchange, } as IInputOptions)`, @@ -118,7 +121,7 @@ export const InputPage = () => { m( '.row', m(TextArea, { - label: 'Please, could you describe yourself', + label: 'Please, describe yourself', helperText: `Don't be shy`, maxLength: 100, onchange, diff --git a/packages/example/src/components/misc/misc-page.ts b/packages/example/src/components/misc/misc-page.ts index 91d76f6..5ae1f25 100644 --- a/packages/example/src/components/misc/misc-page.ts +++ b/packages/example/src/components/misc/misc-page.ts @@ -143,21 +143,21 @@ export const MiscPage = () => { '.row', m(Carousel, { items: [ - { href: '#!/one!', src: 'https://lorempixel.com/250/250/nature/1' }, - { href: '#!/two!', src: 'https://lorempixel.com/250/250/nature/2' }, - { href: '#!/three!', src: 'https://lorempixel.com/250/250/nature/3' }, - { href: '#!/four!', src: 'https://lorempixel.com/250/250/nature/4' }, - { href: '#!/five!', src: 'https://lorempixel.com/250/250/nature/5' }, + { href: '#!/one!', src: 'https://picsum.photos/id/301/200/300' }, + { href: '#!/two!', src: 'https://picsum.photos/id/302/200/300' }, + { href: '#!/three!', src: 'https://picsum.photos/id/306/200/300' }, + { href: '#!/four!', src: 'https://picsum.photos/id/304/200/300' }, + { href: '#!/five!', src: 'https://picsum.photos/id/305/200/300' }, ], }) ), m(CodeBlock, { code: ` m(Carousel, { items: [ - { href: '#!/one!', src: 'https://lorempixel.com/250/250/nature/1' }, - { href: '#!/two!', src: 'https://lorempixel.com/250/250/nature/2' }, - { href: '#!/three!', src: 'https://lorempixel.com/250/250/nature/3' }, - { href: '#!/four!', src: 'https://lorempixel.com/250/250/nature/4' }, - { href: '#!/five!', src: 'https://lorempixel.com/250/250/nature/5' }, + { href: '#!/one!', src: 'https://picsum.photos/id/301/200/300' }, + { href: '#!/two!', src: 'https://picsum.photos/id/302/200/300' }, + { href: '#!/three!', src: 'https://picsum.photos/id/306/200/300' }, + { href: '#!/four!', src: 'https://picsum.photos/id/304/200/300' }, + { href: '#!/five!', src: 'https://picsum.photos/id/305/200/300' }, ] })`, }), diff --git a/packages/example/src/components/pickers/picker-page.ts b/packages/example/src/components/pickers/picker-page.ts index 43b156e..cd580f2 100644 --- a/packages/example/src/components/pickers/picker-page.ts +++ b/packages/example/src/components/pickers/picker-page.ts @@ -15,7 +15,7 @@ export const PickerPage = () => { label: 'Disable pickers', left: 'enable', right: 'disable', - onchange: v => (state.disabled = v), + onchange: (v) => (state.disabled = v), }) ), m('h3.header', 'DatePicker'), @@ -25,7 +25,7 @@ export const PickerPage = () => { disabled: state.disabled, format: 'mmmm d, yyyy', label: 'What is your birthday?', - yearRange: [1900, new Date().getFullYear() - 17], + yearRange: [1970, new Date().getFullYear() + 20], initialValue: new Date(), onchange, }) @@ -34,7 +34,7 @@ export const PickerPage = () => { code: ` m(DatePicker, { format: 'mmmm d, yyyy', label: 'What is your birthday?', - yearRange: [1900, new Date().getFullYear() - 17], + yearRange: [1970, new Date().getFullYear() + 20], initialValue: new Date().toDateString(), onchange, })`, diff --git a/packages/lib/src/autocomplete.ts b/packages/lib/src/autocomplete.ts index a70bb56..deb85aa 100644 --- a/packages/lib/src/autocomplete.ts +++ b/packages/lib/src/autocomplete.ts @@ -1,5 +1,5 @@ import m, { FactoryComponent } from 'mithril'; -import { uniqueId, toAttrs } from './utils'; +import { uniqueId } from './utils'; import { IInputOptions } from './input-options'; import { Label, HelperText } from './label'; @@ -11,7 +11,7 @@ export const Autocomplete: FactoryComponent = () => { return { view: ({ attrs }) => { const id = attrs.id || state.id; - const attributes = toAttrs(attrs); + // const attributes = toAttrs(attrs); const { label, helperText, @@ -22,11 +22,16 @@ export const Autocomplete: FactoryComponent = () => { style, iconName, isMandatory, + ...params } = attrs; const cn = newRow ? className + ' clear' : className; return m(`.input-field${newRow ? '.clear' : ''}`, { className: cn, style }, [ iconName ? m('i.material-icons.prefix', iconName) : '', - m(`input.autocomplete[type=text][tabindex=0]${attributes}`, { + m('input', { + ...params, + className: 'autocomplete', + type: 'text', + tabindex: 0, id, oncreate: ({ dom }) => { M.Autocomplete.init(dom, attrs); diff --git a/packages/lib/src/button.ts b/packages/lib/src/button.ts index 9caae80..02ed170 100644 --- a/packages/lib/src/button.ts +++ b/packages/lib/src/button.ts @@ -1,5 +1,4 @@ import m, { FactoryComponent, Attributes } from 'mithril'; -import { toAttributeString } from './utils'; import { Icon } from './icon'; export interface IHtmlAttributes { @@ -36,28 +35,44 @@ export interface IMaterialButton extends Attributes { * * @example FlatButton = ButtonFactory('a.waves-effect.waves-teal.btn-flat'); */ -export const ButtonFactory = - (defaultClassNames: string, attributes: string = ''): FactoryComponent => - () => { - const dca = `${defaultClassNames}${attributes}`; +export const ButtonFactory = ( + element: string, + defaultClassNames: string, + type: string = '' +): FactoryComponent => { + return () => { return { view: ({ attrs }) => { - const { modalId, tooltip, tooltipPostion, iconName, iconClass, label, attr, ...passThrough } = attrs; + const { modalId, tooltip, tooltipPostion, iconName, iconClass, label, attr, ...params } = attrs; + const cn = [modalId ? 'modal-trigger' : '', tooltip ? 'tooltipped' : '', defaultClassNames] + .filter(Boolean) + .join(' ') + .trim(); return m( - `${dca}${modalId ? `.modal-trigger[href=#${modalId}]` : ''}${ - tooltip ? `.tooltipped[data-position=${tooltipPostion || 'top'}][data-tooltip=${tooltip}]` : '' - }${toAttributeString(attr)}`, - passThrough, + element, + { + ...params, + ...attr, + className: cn, + href: modalId ? `#${modalId}` : undefined, + 'data-position': tooltip ? tooltipPostion || 'top' : undefined, + 'data-tooltip': tooltip || undefined, + type, + }, + // `${dca}${modalId ? `.modal-trigger[href=#${modalId}]` : ''}${ + // tooltip ? `.tooltipped[data-position=${tooltipPostion || 'top'}][data-tooltip=${tooltip}]` : '' + // }${toAttributeString(attr)}`, {} iconName ? m(Icon, { iconName, className: iconClass || 'left' }) : undefined, label ? label : undefined ); }, }; }; +}; -export const Button = ButtonFactory('a.waves-effect.waves-light.btn', '[type=button]'); -export const LargeButton = ButtonFactory('a.waves-effect.waves-light.btn-large', '[type=button]'); -export const SmallButton = ButtonFactory('a.waves-effect.waves-light.btn-small', '[type=button]'); -export const FlatButton = ButtonFactory('a.waves-effect.waves-teal.btn-flat', '[type=button]'); -export const RoundIconButton = ButtonFactory('button.btn-floating.btn-large.waves-effect.waves-light', '[type=button]'); -export const SubmitButton = ButtonFactory('button.btn.waves-effect.waves-light', '[type=submit]'); +export const Button = ButtonFactory('a', 'waves-effect waves-light btn', 'button'); +export const LargeButton = ButtonFactory('a', 'waves-effect waves-light btn-large', 'button'); +export const SmallButton = ButtonFactory('a', 'waves-effect waves-light btn-small', 'button'); +export const FlatButton = ButtonFactory('a', 'waves-effect waves-teal btn-flat', 'button'); +export const RoundIconButton = ButtonFactory('button', 'btn-floating btn-large waves-effect waves-light', 'button'); +export const SubmitButton = ButtonFactory('button', 'btn waves-effect waves-light', 'submit'); diff --git a/packages/lib/src/carousel.ts b/packages/lib/src/carousel.ts index e7e9922..1614073 100644 --- a/packages/lib/src/carousel.ts +++ b/packages/lib/src/carousel.ts @@ -5,6 +5,8 @@ export interface ICarouselItem extends Attributes { href: string; /** Image source */ src: string; + /** Alternative name */ + alt?: string; } export interface ICarousel extends Partial, Attributes { @@ -14,8 +16,8 @@ export interface ICarousel extends Partial, Attributes { export const CarouselItem: FactoryComponent = () => { return { - view: ({ attrs: { href, src } }) => { - return m('a.carousel-item', { href }, m(`img[src=${src}]`)); + view: ({ attrs: { href, src, alt, ...params } }) => { + return m('a.carousel-item', { ...params, href }, m('img', { src, alt })); }, }; }; @@ -36,7 +38,7 @@ export const Carousel: FactoryComponent = () => { M.Carousel.init(dom, attrs); }, }, - items.map(item => m(CarouselItem, item)) + items.map((item) => m(CarouselItem, item)) ) : undefined; }, diff --git a/packages/lib/src/chip.ts b/packages/lib/src/chip.ts index cf78944..c346cff 100644 --- a/packages/lib/src/chip.ts +++ b/packages/lib/src/chip.ts @@ -12,43 +12,66 @@ export interface IChipsOptions extends Partial, Attributes { /** Chips and tags */ export const Chips: FactoryComponent = () => { return { - oncreate: ({ attrs, dom }) => { - const { onchange, onChipAdd, onChipDelete } = attrs; - const chips = M.Chips.getInstance(dom.children[0]) as M.Chips; - const onChipAddBound = onChipAdd ? (onChipAdd.bind(chips) as (el: Element, chip: Element) => void) : undefined; - attrs.onChipAdd = function (this: M.Chips, el: Element, chip: Element) { - if (onchange) { - onchange(this.chipsData); - } - if (onChipAddBound) { - onChipAddBound(el, chip); - } - }; - const onChipDeleteBound = onChipDelete - ? (onChipDelete.bind(chips) as (el: Element, chip: Element) => void) - : undefined; - attrs.onChipDelete = function (this: M.Chips, el: Element, chip: Element) { - if (onchange) { - onchange(this.chipsData); - } - if (onChipDeleteBound) { - onChipDeleteBound(el, chip); - } - }; - M.Chips.init(dom.children[0], attrs); - }, - onupdate: ({ dom, attrs: { data } }) => { - if (!data || data.length === 0) { - return; - } - const chips = M.Chips.getInstance(dom.children[0]) as M.Chips; - data.forEach((d) => chips.addChip(d)); - }, view: ({ - attrs: { placeholder, required, isMandatory = required, data, className = 'col s12', label, helperText }, + attrs: { + placeholder, + required, + isMandatory = required, + // data = [], + className = 'col s12', + label, + helperText, + onchange, + ...params + }, }) => { + const cn = [ + 'chips chips-autocomplete', + placeholder ? 'chips-placeholder' : '', + params.data ? 'chips-initial' : '', + ] + .filter(Boolean) + .join(' ') + .trim(); return m('.input-field', { className }, [ - m(`.chips.chips-autocomplete${placeholder ? '.chips-placeholder' : ''}${data ? '.chips-initial' : ''}`), + m('div', { + className: cn, + oncreate: ({ dom }) => { + const { onChipAdd, onChipDelete } = params; + const chips = M.Chips.getInstance(dom) as M.Chips; + const onChipAddBound = onChipAdd + ? (onChipAdd.bind(chips) as (el: Element, chip: Element) => void) + : undefined; + params.onChipAdd = function (this: M.Chips, el: Element, chip: Element) { + if (onchange) { + onchange(this.chipsData); + } + if (onChipAddBound) { + onChipAddBound(el, chip); + } + }; + const onChipDeleteBound = onChipDelete + ? (onChipDelete.bind(chips) as (el: Element, chip: Element) => void) + : undefined; + params.onChipDelete = function (this: M.Chips, el: Element, chip: Element) { + if (onchange) { + onchange(this.chipsData); + } + if (onChipDeleteBound) { + onChipDeleteBound(el, chip); + } + }; + M.Chips.init(dom, params); + // data.forEach((d) => chips.addChip(d)); + }, + onupdate: ({ dom }) => { + if (!params.data || params.data.length === 0) { + return; + } + const chips = M.Chips.getInstance(dom); + params.data.forEach((d) => chips.addChip(d)); + }, + }), label ? m(Label, { label, isMandatory, className: 'active' }) : undefined, helperText ? m(HelperText, { helperText }) : undefined, ]); diff --git a/packages/lib/src/code-block.ts b/packages/lib/src/code-block.ts index 01eb8ab..5032eef 100644 --- a/packages/lib/src/code-block.ts +++ b/packages/lib/src/code-block.ts @@ -10,13 +10,21 @@ export interface ICodeBlock extends Attributes { /** A simple code block without syntax high-lighting */ export const CodeBlock: FactoryComponent = () => ({ view: ({ attrs }) => { - const { newRow, code, language } = attrs; + const { newRow, code, language, className, ...params } = attrs; const lang = language || 'lang-TypeScript'; const label = lang.replace('lang-', ''); const cb = code instanceof Array ? code.join('\n') : code; + const cn = [newRow ? 'clear' : '', lang, className].filter(Boolean).join(' ').trim(); return m(`pre.codeblock${newRow ? '.clear' : ''}`, attrs, [ m('div', m('label', label)), - m(`code.${lang}`, cb), + m( + 'code', + { + ...params, + className: cn, + }, + cb + ), ]); }, }); diff --git a/packages/lib/src/collection.ts b/packages/lib/src/collection.ts index c8c36ea..d993822 100644 --- a/packages/lib/src/collection.ts +++ b/packages/lib/src/collection.ts @@ -66,8 +66,9 @@ export const ListItem: FactoryComponent<{ item: ICollectionItem; mode: Collectio const { title, content = '', active, iconName, avatar, className, onclick } = item; return mode === CollectionMode.AVATAR ? m( - `li.collection-item.avatar${active ? '.active' : ''}`, + 'li.collection-item.avatar', { + className: active ? 'active' : '', onclick: onclick ? () => onclick(item) : undefined, }, [ @@ -80,7 +81,10 @@ export const ListItem: FactoryComponent<{ item: ICollectionItem; mode: Collectio ] ) : m( - `li.collection-item${active ? '.active' : ''}`, + 'li.collection-item', + { + className: active ? 'active' : '', + }, iconName ? m('div', [title, m(SecondaryContent, item)]) : title ); }, @@ -90,7 +94,7 @@ export const ListItem: FactoryComponent<{ item: ICollectionItem; mode: Collectio const BasicCollection: FactoryComponent = () => { return { view: ({ attrs: { header, items, mode = CollectionMode.BASIC, ...params } }) => { - const collectionItems = items.map(item => m(ListItem, { key: item.id, item, mode })); + const collectionItems = items.map((item) => m(ListItem, { key: item.id, item, mode })); return header ? m('ul.collection.with-header', params, [m('li.collection-header', m('h4', header)), collectionItems]) : m('ul.collection', params, collectionItems); @@ -120,9 +124,13 @@ const LinksCollection: FactoryComponent = () => { return header ? m('.collection.with-header', params, [ m('.collection-header', m('h4', header)), - items.map(item => m(AnchorItem, { key: item.id, item })), + items.map((item) => m(AnchorItem, { key: item.id, item })), ]) - : m('.collection', params, items.map(item => m(AnchorItem, { key: item.id, item }))); + : m( + '.collection', + params, + items.map((item) => m(AnchorItem, { key: item.id, item })) + ); }, }; }; diff --git a/packages/lib/src/dropdown.ts b/packages/lib/src/dropdown.ts index 6fc4653..1a8bbca 100644 --- a/packages/lib/src/dropdown.ts +++ b/packages/lib/src/dropdown.ts @@ -81,8 +81,10 @@ export const Dropdown = (): Component { @@ -92,10 +94,14 @@ export const Dropdown = (): Component m( - `li${i.divider ? '.divider[tabindex=-1]' : ''}`, + 'li[tabindex=-1]', + { + className: i.divider ? 'divider' : '', + }, i.divider ? undefined : m( diff --git a/packages/lib/src/input.ts b/packages/lib/src/input.ts index 6734686..8f3aefa 100644 --- a/packages/lib/src/input.ts +++ b/packages/lib/src/input.ts @@ -1,5 +1,5 @@ import m, { FactoryComponent, Attributes } from 'mithril'; -import { uniqueId, toAttrs } from './utils'; +import { uniqueId } from './utils'; import { IInputOptions } from './input-options'; import { Label, HelperText } from './label'; import './styles/input.css'; @@ -25,10 +25,13 @@ export const TextArea: FactoryComponent> = () => { style, ...params } = attrs; - const attributes = toAttrs(params); - return m(`.input-field`, { className, style }, [ + // const attributes = toAttrs(params); + return m('.input-field', { className, style }, [ iconName ? m('i.material-icons.prefix', iconName) : '', - m(`textarea.materialize-textarea[tabindex=0][id=${id}]${attributes}`, { + m('textarea.materialize-textarea', { + ...params, + id, + tabindex: 0, oncreate: ({ dom }) => { M.textareaAutoResize(dom); if (attrs.maxLength) { @@ -111,10 +114,16 @@ const InputField = validate, ...params } = attrs; - const attributes = toAttrs(params); - return m(`.input-field${newRow ? '.clear' : ''}${defaultClass}`, { className, style }, [ + // const attributes = toAttrs(params); + const cn = [newRow ? 'clear' : '', defaultClass, className].filter(Boolean).join(' ').trim(); + return m('.input-field', { className: cn, style }, [ iconName ? m('i.material-icons.prefix', iconName) : undefined, - m(`input.validate[type=${type}][tabindex=0][id=${id}]${attributes}`, { + m('input.validate', { + ...params, + type, + tabindex: 0, + id, + // attributes, oncreate: ({ dom }) => { if (focus(attrs)) { (dom as HTMLElement).focus(); @@ -230,14 +239,14 @@ export const FileInput: FactoryComponent = () => { placeholder, onchange, className = 'col s12', - accept, + accept: acceptedFiles, label = 'File', } = attrs; - const accepted = accept ? (accept instanceof Array ? accept.join(', ') : accept) : undefined; - const acc = accepted ? `[accept=${accepted}]` : ''; - const mul = multiple ? '[multiple]' : ''; - const dis = disabled ? '[disabled]' : ''; - const ph = placeholder ? `[placeholder=${placeholder}]` : ''; + const accept = acceptedFiles + ? acceptedFiles instanceof Array + ? acceptedFiles.join(', ') + : acceptedFiles + : undefined; return m( '.file-field.input-field', { @@ -246,7 +255,11 @@ export const FileInput: FactoryComponent = () => { [ m('.btn', [ m('span', label), - m(`input[type=file]${mul}${dis}${acc}`, { + m('input[type=file]', { + title: label, + accept, + multiple, + disabled, onchange: onchange ? (e: UIEvent) => { const i = e.target as HTMLInputElement; @@ -260,7 +273,8 @@ export const FileInput: FactoryComponent = () => { ]), m( '.file-path-wrapper', - m(`input.file-path.validate${ph}[type=text]`, { + m('input.file-path.validate[type=text]', { + placeholder, oncreate: ({ dom }) => { i = dom as HTMLInputElement; if (initialValue) i.value = initialValue; @@ -271,7 +285,12 @@ export const FileInput: FactoryComponent = () => { m( 'a.waves-effect.waves-teal.btn-flat', { - style: 'float: right;position: relative;top: -3rem; padding: 0', + style: { + float: 'right', + position: 'relative', + top: '-3rem', + padding: 0, + }, onclick: () => { canClear = false; i.value = ''; diff --git a/packages/lib/src/label.ts b/packages/lib/src/label.ts index 64d1d2a..e53a846 100644 --- a/packages/lib/src/label.ts +++ b/packages/lib/src/label.ts @@ -17,12 +17,17 @@ export interface IMaterialLabel extends Attributes { /** Simple label element, used for most components. */ export const Label: FactoryComponent = () => { return { - view: ({ attrs: { label, id, isMandatory, isActive, ...params } }) => + view: ({ attrs: { label, id, isMandatory, isActive, className, ...params } }) => label - ? m(`label${isActive ? '.active' : ''}${id ? `[for=${id}]` : ''}`, params, [ - m.trust(label), - isMandatory ? m(Mandatory) : undefined, - ]) + ? m( + 'label', + { + ...params, + className: [className, isActive ? 'active' : ''].filter(Boolean).join(' ').trim(), + for: id, + }, + [m.trust(label), isMandatory ? m(Mandatory) : undefined] + ) : undefined, }; }; @@ -38,7 +43,11 @@ export const HelperText: FactoryComponent = () => { return { view: ({ attrs: { helperText, dataError, dataSuccess, className } }) => { return helperText || dataError || dataSuccess - ? m('span.helper-text', { className, dataError, dataSuccess }, helperText ? m.trust(helperText) : '') + ? m( + 'span.helper-text', + { className, 'data-error': dataError, 'data-success': dataSuccess }, + helperText ? m.trust(helperText) : '' + ) : undefined; }, }; diff --git a/packages/lib/src/material-box.ts b/packages/lib/src/material-box.ts index 6d6a0b3..f91592d 100644 --- a/packages/lib/src/material-box.ts +++ b/packages/lib/src/material-box.ts @@ -24,11 +24,6 @@ export const MaterialBox: FactoryComponent = () => { oncreate: ({ dom, attrs }) => { M.Materialbox.init(dom, attrs); }, - view: ({ attrs }) => { - const { src, width, height } = attrs; - const w = width ? `[width=${width}]` : ''; - const h = height ? `[height=${height}]` : ''; - return m(`img.materialboxed[src=${src}]${w}${h}`, attrs); - }, + view: ({ attrs }) => m('img.materialboxed', attrs), }; }; diff --git a/packages/lib/src/modal.ts b/packages/lib/src/modal.ts index 638a5b0..0899c1f 100644 --- a/packages/lib/src/modal.ts +++ b/packages/lib/src/modal.ts @@ -27,21 +27,33 @@ export const ModalPanel: FactoryComponent = () => ({ onCreate(modal); } }, - view: ({ attrs: { id, title, description, fixedFooter, bottomSheet, buttons, richContent } }) => { - const ff = fixedFooter ? '.modal-fixed-footer' : ''; - const bs = bottomSheet ? '.bottom-sheet' : ''; - return m(`.modal${ff}${bs}[id=${id}]`, [ - m('.modal-content', [ - m('h4', title), - richContent && typeof description === 'string' - ? m.trust(description || '') - : typeof description === 'string' - ? m('p', description) - : description, - ]), - buttons - ? m('.modal-footer', buttons.map(props => m(FlatButton, { ...props, className: 'modal-close' }))) - : undefined, - ]); + view: ({ attrs: { id, title, description, fixedFooter, bottomSheet, buttons, richContent, className } }) => { + const cn = [className, fixedFooter ? 'modal-fixed-footer' : '', bottomSheet ? 'bottom-sheet' : ''] + .filter(Boolean) + .join(' ') + .trim(); + return m( + '.modal', + { + id, + className: cn, + }, + [ + m('.modal-content', [ + m('h4', title), + richContent && typeof description === 'string' + ? m.trust(description || '') + : typeof description === 'string' + ? m('p', description) + : description, + ]), + buttons + ? m( + '.modal-footer', + buttons.map((props) => m(FlatButton, { ...props, className: 'modal-close' })) + ) + : undefined, + ] + ); }, }); diff --git a/packages/lib/src/option.ts b/packages/lib/src/option.ts index d38a3f4..201f4a6 100644 --- a/packages/lib/src/option.ts +++ b/packages/lib/src/option.ts @@ -121,7 +121,6 @@ export const Options = (): Component> => state.checkedId = checkedId; state.checkedIds = checkedId instanceof Array ? checkedId : [checkedId]; } - const clear = newRow ? '.clear' : ''; const onchange = callback ? (propId: T, checked: boolean) => { const checkedIds = state.checkedIds.filter((i) => i !== propId); @@ -132,7 +131,8 @@ export const Options = (): Component> => callback(checkedIds); } : undefined; - return m(`div${clear}`, { className, style }, [ + const cn = [newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim(); + return m('div', { className: cn, style }, [ m('div', { className: 'input-field options' }, m(Label, { id, label, isMandatory })), m(HelperText, { helperText: description }), ...options.map((option) => diff --git a/packages/lib/src/parallax.ts b/packages/lib/src/parallax.ts index 39535e4..6de9669 100644 --- a/packages/lib/src/parallax.ts +++ b/packages/lib/src/parallax.ts @@ -16,6 +16,6 @@ export const Parallax: FactoryComponent = () => { oncreate: ({ dom, attrs }) => { M.Parallax.init(dom, attrs); }, - view: ({ attrs: { src } }) => (src ? m('.parallax-container', m('.parallax', m(`img[src=${src}]`))) : undefined), + view: ({ attrs: { src } }) => (src ? m('.parallax-container', m('.parallax', m('img', { src }))) : undefined), }; }; diff --git a/packages/lib/src/pickers.ts b/packages/lib/src/pickers.ts index d95fc56..2313e84 100644 --- a/packages/lib/src/pickers.ts +++ b/packages/lib/src/pickers.ts @@ -1,6 +1,6 @@ import m, { FactoryComponent } from 'mithril'; import { IInputOptions } from './input-options'; -import { uniqueId, toAttrs } from './utils'; +import { uniqueId } from './utils'; import { Label, HelperText } from './label'; /** Component to pick a date */ @@ -22,22 +22,27 @@ export const DatePicker: FactoryComponent & Partial { const id = state.id; - const attributes = toAttrs(props); - // const {} = attrs; - const clear = newRow ? '.clear' : ''; + // const attributes = toAttrs(props); const onClose = onchange ? () => state.dp && onchange(state.dp.date) : undefined; + const cn = [newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim(); return m( - `.input-field${clear}`, + '.input-field', { - className, + className: cn, onremove: () => { - // console.log('removing dp'); return state.dp && state.dp.destroy(); }, }, [ iconName ? m('i.material-icons.prefix', iconName) : '', - m(`input.datepicker[type=text][tabindex=0][id=${id}]${attributes}${disabled ? '[disabled]' : ''}`, { + m('input', { + ...props, + type: 'text', + tabindex: 0, + className: 'datepicker', + id, + // attributes, + disabled, oncreate: ({ dom }) => { state.dp = M.Datepicker.init(dom, { format: 'yyyy/mm/dd', @@ -77,21 +82,26 @@ export const TimePicker: FactoryComponent { const id = state.id; - const attributes = toAttrs(props); - const clear = newRow ? '.clear' : ''; + // const attributes = toAttrs(props); const now = new Date(); const onCloseEnd = onchange ? () => state.tp && onchange(state.tp.time || initialValue || `${now.getHours()}:${now.getMinutes()}`) : undefined; + const cn = ['input-field', 'timepicker', newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim(); return m( - `.input-field.timepicker${clear}`, + 'div', { - className, + className: cn, onremove: () => state.tp && state.tp.destroy(), }, [ iconName ? m('i.material-icons.prefix', iconName) : '', - m(`input[type=text][tabindex=0][id=${id}]${attributes}${disabled ? '[disabled]' : ''}`, { + m('input', { + ...props, + type: 'text', + tabindex: 0, + id, + disabled, value: initialValue, oncreate: ({ dom }) => { state.tp = M.Timepicker.init(dom, { diff --git a/packages/lib/src/radio.ts b/packages/lib/src/radio.ts index de56ce9..5cfa26d 100644 --- a/packages/lib/src/radio.ts +++ b/packages/lib/src/radio.ts @@ -40,17 +40,15 @@ export interface IRadioButton extends Attributes { export const RadioButton = (): Component> => ({ view: ({ attrs: { id, groupId, label, onchange, className = 'col s12', checked, disabled } }) => { return m( - `div`, + 'div', { className }, m('label', [ - m( - `input[type=radio][tabindex=0][name=${groupId}]${checked ? '[checked=checked]' : ''}${ - disabled ? '[disabled]' : '' - }`, - { - onclick: onchange ? () => onchange(id) : undefined, - } - ), + m('input[type=radio][tabindex=0]', { + name: groupId, + disabled, + checked, + onclick: onchange ? () => onchange(id) : undefined, + }), m('span', m.trust(label)), ]) ); diff --git a/packages/lib/src/switch.ts b/packages/lib/src/switch.ts index 672be79..9e254e3 100644 --- a/packages/lib/src/switch.ts +++ b/packages/lib/src/switch.ts @@ -1,5 +1,5 @@ import m, { FactoryComponent } from 'mithril'; -import { uniqueId, disable } from './utils'; +import { uniqueId } from './utils'; import { IInputOptions } from './input-options'; import { Label } from './label'; import './styles/switch.css'; @@ -31,14 +31,18 @@ export const Switch: FactoryComponent = () => { className = 'col s12', ...params } = attrs; - return m(`div${newRow ? '.clear' : ''}`, { className }, [ + const cn = [newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim(); + return m('div', { className: cn }, [ label ? m(Label, { label: label || '', id, isMandatory }) : undefined, m( '.switch', params, m('label', [ left || 'Off', - m(`input[id=${id}][type=checkbox]${disable({ disabled })}${checked ? '[checked]' : ''}`, { + m('input[type=checkbox]', { + id, + disabled, + checked, onclick: onchange ? (e: Event) => { if (e.target && typeof (e.target as HTMLInputElement).checked !== 'undefined') { diff --git a/packages/lib/src/tabs.ts b/packages/lib/src/tabs.ts index 9e2ab07..7b0e80a 100644 --- a/packages/lib/src/tabs.ts +++ b/packages/lib/src/tabs.ts @@ -51,15 +51,16 @@ export const Tabs: FactoryComponent = () => { const createId = (title: string, id?: string) => (id ? id : title.replace(/ /g, '').toLowerCase()); return { view: ({ - attrs: { tabWidth, selectedTabId, tabs, className: cn, style, duration, onShow, swipeable, responsiveThreshold }, + attrs: { tabWidth, selectedTabId, tabs, className, style, duration, onShow, swipeable, responsiveThreshold }, }) => { const activeTab = tabs.filter((t) => t.active).shift(); const select = selectedTabId || (activeTab ? createId(activeTab.title, activeTab.id) : ''); + const cn = [tabWidth === 'fill' ? 'tabs-fixed-width' : '', className].filter(Boolean).join(' ').trim(); return m('.row', [ m( '.col.s12', m( - `ul.tabs${tabWidth === 'fill' ? '.tabs-fixed-width' : ''}`, + 'ul.tabs', { className: cn, style, @@ -81,25 +82,29 @@ export const Tabs: FactoryComponent = () => { }, onremove: () => state.instance.destroy(), }, - tabs.map(({ className, title, id, active, disabled, target, href }) => - m( - `li.tab${disabled ? '.disabled' : ''}${ - tabWidth === 'fixed' ? `.col.s${Math.floor(12 / tabs.length)}` : '' - }`, - { className }, - m( - `a[id=tab_${createId(title, id)}]${active ? '.active' : ''}`, - { target, href: href || `#${createId(title, id)}` }, - title - ) - ) - ) + tabs.map(({ className, title, id, active, disabled, target, href }) => { + const cn = [tabWidth === 'fixed' ? `col s${Math.floor(12 / tabs.length)}` : '', className] + .filter(Boolean) + .join(' ') + .trim(); + const anchorId = createId(title, id); + const tabId = `tab_${anchorId}`; + const cnA = active ? 'active' : ''; + return m( + 'li.tab', + { + className: cn, + disabled, + }, + m('a', { id: tabId, className: cnA, target, href: href || `#${anchorId}` }, title) + ); + }) ) ), tabs .filter(({ href }) => typeof href === 'undefined') .map(({ id, title, vnode, contentClass }) => - m(`.col.s12[id=${createId(title, id)}]`, { className: contentClass }, vnode) + m('.col.s12', { id: createId(title, id), className: contentClass }, vnode) ), ]); }, diff --git a/packages/lib/src/timeline.ts b/packages/lib/src/timeline.ts index d1cc711..c627444 100644 --- a/packages/lib/src/timeline.ts +++ b/packages/lib/src/timeline.ts @@ -35,11 +35,11 @@ const TimelineItem: FactoryComponent = () => { view: ({ attrs: { id, title, datetime, active, content, iconName, dateFormatter, timeFormatter, onSelect } }) => { const onclick = onSelect ? () => onSelect({ id, title, datetime, active, content }) : undefined; const style = onSelect ? 'cursor: pointer;' : undefined; - return m(`li${active ? '.active' : ''}${id ? `[id=${id}]` : ''}`, { onclick, style }, [ + return m('li', { id, className: active ? 'active' : undefined, onclick, style }, [ m('.mm_time', { datetime }, [m('span', dateFormatter(datetime)), m('span', timeFormatter(datetime))]), iconName ? m('.mm_icon', m('i.material-icons', iconName)) : undefined, m('.mm_label', [ - title ? typeof title === 'string' ? m('h5', title) : title : undefined, + title ? (typeof title === 'string' ? m('h5', title) : title) : undefined, content ? (typeof content === 'string' ? m('p', content) : content) : undefined, ]), ]); @@ -58,7 +58,7 @@ export const Timeline: FactoryComponent = () => { view: ({ attrs: { items, onSelect, timeFormatter = tf, dateFormatter = df } }) => { return m( 'ul.mm_timeline', - items.map(item => m(TimelineItem, { onSelect, dateFormatter, timeFormatter, ...item })) + items.map((item) => m(TimelineItem, { onSelect, dateFormatter, timeFormatter, ...item })) ); }, }; diff --git a/packages/lib/src/utils.ts b/packages/lib/src/utils.ts index 6044152..2277695 100644 --- a/packages/lib/src/utils.ts +++ b/packages/lib/src/utils.ts @@ -1,5 +1,3 @@ -import { IInputOptions } from './input-options'; - /** * Create a unique ID * @see https://stackoverflow.com/a/2117523/319711 @@ -27,86 +25,9 @@ export const uuid4 = () => { }); }; -export const compose = any, T>(...functions: F[]) => (data: T) => - functions.reduceRight((value, func) => func(value), data); - -export const map = (f: (...args: any[]) => any) => (x: T[]) => Array.prototype.map.call(x, f); - -export const join = (seperator: string) => (list: T[]): string => Array.prototype.join.call(list, seperator); - -/** - * Convert camel case to snake case. - * - * @param {string} cc: Camel case string - */ -export const camelToSnake = (cc: string) => cc.replace(/([A-Z])/g, ($1) => '-' + $1.toLowerCase()); - -const encodeAttribute = (x = '') => x.toString().replace(/"/g, '"'); - -/** Convert an object to a string of HTML attributes */ -export const toAttributeString = (x?: T) => - x - ? compose( - join(''), - map((attribute: string) => `[${camelToSnake(attribute)}="${encodeAttribute(x[attribute])}"]`), - Object.keys - )(x) - : ''; - -/** Options that we want to convert to attributes */ -const inputAttributes = [ - 'min', - 'max', - 'minLength', - 'maxLength', - 'rows', - 'cols', - 'placeholder', - 'autocomplete', - 'pattern', - 'readOnly', - 'step', -]; - -const isInputAttribute = (key: string) => inputAttributes.indexOf(key) >= 0; - -const isDefinedAttribute = (opt: IInputOptions) => (key: string) => typeof (opt as any)[key] !== 'undefined'; - -const toProps = (o: IInputOptions) => { - const isAttributeDefined = isDefinedAttribute(o); - return Object.keys(o) - .filter(isInputAttribute) - .filter(isAttributeDefined) - .reduce((p, c) => { - const value = (o as any)[c]; - p.push(`[${c.toLowerCase()}=${value}]`); - return p; - }, [] as string[]) - .join(''); -}; - -/** Add a character counter when there is an input restriction. */ -const charCounter = (o: IInputOptions) => (o.maxLength ? `[data-length=${o.maxLength}]` : ''); - -/** Add the disabled attribute when required */ -export const disable = ({ disabled }: { disabled?: boolean }) => (disabled ? '[disabled]' : ''); - -/** Add the required and aria-required attribute when required */ -export const req = ({ required, isMandatory }: { required?: boolean; isMandatory?: boolean }) => - required || isMandatory ? '[required][aria-required=true]' : ''; - -/** Add the autofocus attribute when required */ -const focus = ({ autofocus }: { autofocus?: (() => boolean) | boolean }) => - (typeof autofocus === 'boolean' && autofocus) || (autofocus && autofocus()) ? '[autofocus]' : ''; - -/** Convert input options to a set of input attributes */ -export const toAttrs = (o: IInputOptions) => toProps(o) + charCounter(o) + disable(o) + req(o) + focus(o); - /** Check if a string or number is numeric. @see https://stackoverflow.com/a/9716488/319711 */ export const isNumeric = (n: string | number) => !isNaN(parseFloat(n as string)) && isFinite(n as number); -export const pipe = (...fncs: Array<(x: any) => any>) => (x: T) => fncs.reduce((y, f) => f(y), x); - /** * Pad left, default width 2 with a '0' * @@ -116,35 +37,4 @@ export const pipe = (...fncs: Array<(x: any) => any>) => (x: T) => fncs.reduc * @param {string} [z='0'] * @returns */ -export const padLeft = (n: string | number, width = 2, z = '0') => { - n = n + ''; - return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; -}; - -/** - * Swap two elements at index i and j. - * Mutates the original array. - * - * @param arr array of items - * @param i from index - * @param j to index - */ -export const swap = (arr: T[], i: number, j: number) => { - const temp = arr[i]; - arr[i] = arr[j]; - arr[j] = temp; -}; - -/** - * Move an element at index i to index j. - * Mutates the original array. - * - * @param arr array of items - * @param i from index - * @param j to index - */ -export const move = (arr: T[], i: number, j: number) => { - const temp = arr[i]; - arr.splice(i, 1); - arr.splice(j, 0, temp); -}; +export const padLeft = (n: string | number, width: number = 2, z: string = '0') => String(n).padStart(width, z);