From ffca3dfb15f37999d2b751c5b62a90ead65201a3 Mon Sep 17 00:00:00 2001 From: Joris Date: Mon, 13 Feb 2023 15:25:56 +0100 Subject: Use rx to update the view --- src/view/form.ts | 149 +++++++++++++++++++++++++++++++++++----------------- src/view/layout.ts | 13 ----- src/view/options.ts | 22 ++++---- src/view/play.ts | 89 +++++++++++++++++-------------- 4 files changed, 162 insertions(+), 111 deletions(-) delete mode 100644 src/view/layout.ts (limited to 'src/view') diff --git a/src/view/form.ts b/src/view/form.ts index 77a8cb7..5547e0c 100644 --- a/src/view/form.ts +++ b/src/view/form.ts @@ -1,66 +1,121 @@ -import h, { classNames } from 'lib/h' -import * as dom from 'lib/dom' -import * as play from 'view/play' -import * as layout from 'view/layout' -import * as chord from 'chord' -import * as options from 'view/options' +import { h, withVar, Html, Rx } from 'lib/rx' +import * as Options from 'view/options' -// View - -export function view(): Element[] { - let opts = options.load() +interface Params { + options: Options.Model + onSubmit: (options: Options.Model) => void +} - return layout.view( +export function view({ options, onSubmit }: Params): Html { + return withVar(options, (opts, updateOptions) => h('form', - { - className: 'g-Form', - onsubmit + { className: 'g-Form', + onsubmit: opts.map(o => + (event: Event) => { + event.preventDefault() + onSubmit(o) + } + ) }, - chordCheckbox({ name: 'major', label: '', checked: opts.major }), - chordCheckbox({ name: 'minor', label: '-', checked: opts.minor }), - chordCheckbox({ name: 'seventh', label: '7', checked: opts.seventh }), - chordCheckbox({ name: 'minorSeventh', label: '-7', checked: opts.minorSeventh }), - chordCheckbox({ name: 'majorSeventh', label: '7', checked: opts.majorSeventh }), - labelInput({ type: 'number', name: 'bpm', label: 'BPM', value: opts.bpm.toString() }), - labelInput({ type: 'number', name: 'beatsPerChord', label: 'Beats per Chord', value: opts.beatsPerChord.toString() }), + chordCheckbox({ + label: '', + checked: opts.map(o => o.major), + onCheck: (checked => updateOptions(o => { + o.major = checked + return o + })) + }), + chordCheckbox({ + label: '-', + checked: opts.map(o => o.minor), + onCheck: (checked => updateOptions(o => { + o.minor = checked + return o + })) + }), + chordCheckbox({ + label: '7', + checked: opts.map(o => o.seventh), + onCheck: (checked => updateOptions(o => { + o.seventh = checked + return o + })) + }), + chordCheckbox({ + label: '-7', + checked: opts.map(o => o.minorSeventh), + onCheck: (checked => updateOptions(o => { + o.minorSeventh = checked + return o + })) + }), + chordCheckbox({ + label: '7', + checked: opts.map(o => o.majorSeventh), + onCheck: (checked => updateOptions(o => { + o.majorSeventh = checked + return o + })) + }), + numberInput({ + label: 'BPM', + value: opts.map(o => o.bpm.toString()), + onChange: (n => updateOptions(o => { + o.bpm = n + return o + })) + }), + numberInput({ + label: 'Beats per Chord', + value: opts.map(o => o.beatsPerChord.toString()), + onChange: (n => updateOptions(o => { + o.beatsPerChord = n + return o + })) + }), h('input', { type: 'submit', value: 'Play' }) ) ) } -function chordCheckbox({ name, label, checked }: any): Element { - return labelInput({ className: 'g-ChordLabel', type: 'checkbox', name, label, checked }) +interface ChordCheckboxParams { + label: string + checked: Rx + onCheck: (checked: boolean) => void } -function onsubmit(event: Event): void { - event.preventDefault() - let input = (name: String) => document.querySelector(`input[name="${name}"]`) as HTMLInputElement - let opts = { - major: input('major').checked, - minor: input('minor').checked, - seventh: input('seventh').checked, - minorSeventh: input('minorSeventh').checked, - majorSeventh: input('majorSeventh').checked, - bpm: parseInt(input('bpm').value), - beatsPerChord: parseInt(input('beatsPerChord').value) - } - options.save(opts) - dom.show(play.view(opts)) +function chordCheckbox({ label, checked, onCheck }: ChordCheckboxParams): Html { + return h('label', + { className: 'g-ChordLabel' }, + h('input', + { type: 'checkbox', + checked, + onchange: (event: Event) => onCheck((event.target as HTMLInputElement).checked) + } + ), + label + ) } -type LabelInputParams = { - className?: string, - type: string, - name: string, - label: string, - checked?: boolean, - value?: string +interface NumberInputParams { + label: string + value: Rx + onChange: (n: number) => void } -function labelInput({ className, type, name, label, checked, value }: LabelInputParams) { +function numberInput({ label, value, onChange }: NumberInputParams): Html { return h('label', - className !== undefined ? { className } : {}, - h('input', { type, name, checked, value }), + h('input', + { type: 'number', + value, + onchange: (event: Event) => { + const n = parseInt((event.target as HTMLInputElement).value) + if (typeof n === 'number') { + onChange(n) + } + } + } + ), label ) } diff --git a/src/view/layout.ts b/src/view/layout.ts deleted file mode 100644 index ac62290..0000000 --- a/src/view/layout.ts +++ /dev/null @@ -1,13 +0,0 @@ -import h from 'lib/h' -import * as dom from 'lib/dom' -import * as form from 'view/form' - -export function view(main: Element): Element[] { - return [ - h('header', - { onclick: () => dom.show(form.view()) }, - 'Chords' - ), - main - ] -} diff --git a/src/view/options.ts b/src/view/options.ts index 4a57f97..31fd631 100644 --- a/src/view/options.ts +++ b/src/view/options.ts @@ -1,14 +1,14 @@ -export type Options = { - major: boolean, - minor: boolean, - seventh: boolean, - minorSeventh: boolean, - majorSeventh: boolean, - bpm: number, +export interface Model { + major: boolean + minor: boolean + seventh: boolean + minorSeventh: boolean + majorSeventh: boolean + bpm: number beatsPerChord: number } -let defaultOptions: Options = { +let init: Model = { major: true, minor: false, seventh: false, @@ -20,11 +20,11 @@ let defaultOptions: Options = { let key: string = 'options' -export function load(): Options { +export function load(): Model { let str = localStorage[key] - return str && JSON.parse(str) || defaultOptions + return str && JSON.parse(str) || init } -export function save(options: Options): void { +export function save(options: Model): void { localStorage[key] = JSON.stringify(options) } diff --git a/src/view/play.ts b/src/view/play.ts index f0340f7..b85e505 100644 --- a/src/view/play.ts +++ b/src/view/play.ts @@ -1,49 +1,58 @@ -import h, { classNames } from 'lib/h' -import * as dom from 'lib/dom' -import { Options } from 'view/options' -import * as chord from 'chord' -import * as layout from 'view/layout' +import { h, withVar, Html } from 'lib/rx' +import * as Options from 'view/options' +import * as Chord from 'chord' -export function view(options: Options): Element[] { - let chords = h('div', - { className: 'g-Play' }, - chordNode(), - chordNode(options), - chordNode(options) - ) - - let chordBeat = 1 +interface ViewParams { + options: Options.Model +} - dom.triggerAnimation(chords.children[1] as HTMLElement, 'g-Chord--Beat') - setInterval(() => { - if (chordBeat == options.beatsPerChord) { - shiftChords(chords, options) - chords.children[0].classList.remove('g-Chord--Beat') - dom.triggerAnimation(chords as HTMLElement, 'g-Play--Shift') - dom.triggerAnimation(chords.children[1] as HTMLElement, 'g-Chord--Beat') - chordBeat = 1 - } else { - dom.triggerAnimation(chords.children[1] as HTMLElement, 'g-Chord--Beat') - chordBeat += 1 - } - }, 60 / options.bpm * 1000) +export function view({ options }: ViewParams): Html { + const initChords: Array<[string, string]> = [['', ''], Chord.generate(options), Chord.generate(options)] - return layout.view(chords) -} + return withVar(initChords, (chords, updateChords) => + withVar(undefined, (beat, updateBeat) => { + let chordBeat = 1 + const interval = setInterval(() => { + if (chordBeat === options.beatsPerChord) { + updateChords(xs => { + xs.shift() + xs.push(Chord.generate(options)) + return xs + }) + chordBeat = 1 + } else { + updateBeat(_ => undefined) + chordBeat += 1 + } + }, 60 / options.bpm * 1000) -/* Shift chords and generate a new random one. - */ -function shiftChords(chords: Element, options: Options) { - chords.removeChild(chords.children[0]) - chords.appendChild(chordNode(options)) + return h('div', + { className: 'g-Play', + onunmount: () => clearInterval(interval) + }, + chords.map(xs => + h('div', + { className: 'g-Chords' }, + xs.map((chord, i) => { + if (i == 0) { + return viewChord(chord, 'g-Chord--Disappear') + } else if (i == 1) { + return beat.map(_ => viewChord(chord)) + } else { + return viewChord(chord) + } + }) + ) + ) + ) + }) + ) } -function chordNode(options?: Options): Element { - let [base, alteration] = options ? chord.generate(options) : ['', ''] - - return h('div', - { className: 'g-Chord' }, +function viewChord([base, alteration]: [string, string], className: string = ''): Html { + return h('div', + { className: `g-Chord ${className}` }, base, - h('sup', {}, alteration) + h('sup', alteration) ) } -- cgit v1.2.3