Skip to content

Instantly share code, notes, and snippets.

@fabiospampinato
Last active November 4, 2021 20:44
Show Gist options
  • Save fabiospampinato/4ddcc0d878452af7daa2321d30823009 to your computer and use it in GitHub Desktop.
Save fabiospampinato/4ddcc0d878452af7daa2321d30823009 to your computer and use it in GitHub Desktop.
An attempt at having Preact components with Solid-like performance (while mounted).
/* IMPORT */
import _ from 'lodash';
import {Text} from '~/components';
import {Utils} from '~/lib';
import {useRef, useClass} from '~/hooks';
import {observable, computed, Component} from '~/view';
import {Class, Styles} from './styles';
import {Props} from './types';
/* MAIN */
class Numbox extends Component<Props> {
/* DEFAULTS */
static defaults: Required<Props> = {
min: 0,
max: 100,
step: 1,
value: 0,
onChange: _.noop
};
/* STATE */
state = {
value: observable ( this.props.value ), //TODO: Sanitize
refs: {
numbox: useRef (),
minus: useRef (),
plus: useRef ()
},
class: {
minus: {
[Class.disabled]: observable ( false )
},
plus: {
[Class.disabled]: observable ( false )
}
}
};
/* API */
get = (): number => {
return this.state.value ();
}
set = ( value: number ): void => {
value = isNaN ( value ) ? 0 : value;
value = Utils.roundCloser ( value, this.props.step );
value = _.clamp ( value, this.props.min, this.props.max );
if ( value === this.state.value () ) return;
this.state.value ( value );
}
decrement = (): void => {
const valueNext = this.state.value () - this.props.step;
this.set ( valueNext );
}
increment = (): void => {
const valueNext = this.state.value () + this.props.step;
this.set ( valueNext );
}
/* RENDER */
render = (): JSXE => {
console.log ( '%c RENDER 😱!', 'background: black; color: white;' );
const {value} = this.state;
computed ( () => this.props.onChange ( value () ) );
computed ( () => this.state.class.minus[Class.disabled] ( this.state.value () === this.props.min ) );
computed ( () => this.state.class.plus[Class.disabled] ( this.state.value () === this.props.max ) );
useClass ( this.state.refs.minus, this.state.class.minus );
useClass ( this.state.refs.plus, this.state.class.plus );
return (
<Styles.Numbox ref={this.state.refs.numbox}>
<Styles.Minus ref={this.state.refs.minus} onClick={this.decrement}>-</Styles.Minus>
<Styles.Input>
<Text>{this.state.value}</Text>
</Styles.Input>
<Styles.Plus ref={this.state.refs.plus} onClick={this.increment}>+</Styles.Plus>
</Styles.Numbox>
);
}
}
/* EXPORT */
export default Component.make ( Numbox );
/* IMPORT */
import _ from 'lodash';
import Widget from '~/components/widget';
import {useObservableEffect, useProperties} from '~/hooks';
import {Keyboard} from '~/lib';
import {observable} from '~/observable';
import {ref} from '~/view';
import {Classes, Styles} from './styles';
import {Props} from './types';
/* MAIN */
class Numbox extends Widget<Props> {
/* STATE */
state = {
min: observable ( 0 ),
max: observable ( 100 ),
step: observable ( 1 ),
value: observable ( 0 ),
onChange: observable ( _ => {} ),
classes: {
minus: {
[Classes.disabled]: observable ( false )
},
plus: {
[Classes.disabled]: observable ( false )
}
},
refs: {
numbox: ref<HTMLDivElement> (),
input: ref<HTMLInputElement> (),
minus: ref<HTMLDivElement> (),
plus: ref<HTMLDivElement> ()
}
};
/* EVENTS API */
onInputChange = (): void => {
const input = this.state.refs.input.current;
if ( !input ) return;
this.set ( Number ( input.value ) );
input.value = String ( this.state.value () );
}
onInputKeyDown = ( { keyCode }: KeyboardEvent ): void => {
if ( keyCode === Keyboard.ESC || keyCode === Keyboard.ENTER ) {
this.state.refs.input.current?.blur ();
}
}
/* API */
get = (): number => {
return this.state.value ();
}
set = ( value: number ): void => {
value = value || 0;
value = value - ( value % this.state.step () );
value = _.clamp ( value, this.state.min (), this.state.max () );
this.state.value ( value );
}
decrement = (): void => {
this.set ( this.state.value () - this.state.step () );
}
increment = (): void => {
this.set ( this.state.value () + this.state.step () );
}
update = ( props: Props ): void => {
const min = props.min ?? 0;
const max = props.max ?? 100;
const step = props.step ?? 1;
const value = props.value ?? 0;
const onChange = props.onChange ?? _.noop;
if ( min >= max ) throw new Error ( '"min" must be lower than "max"' );
if ( min % step !== 0 ) throw new Error ( '"min" must be a multiple of "step"' );
if ( max % step !== 0 ) throw new Error ( '"max" must be a multiple of "step"' );
this.state.min ( min );
this.state.max ( max );
this.state.step ( step );
this.state.value ( value );
this.state.onChange ( () => onChange );
this.set ( value );
}
/* RENDER */
render = (): JSXE => {
const {onInputChange, onInputKeyDown, decrement, increment} = this;
const {min, max, step, value, onChange} = this.state;
const {classes, refs} = this.state;
useObservableEffect ( () => classes.minus[Classes.disabled]( value () === min () ) );
useObservableEffect ( () => classes.plus[Classes.disabled]( value () === max () ) );
useObservableEffect ( () => onChange ()( value () ) );
useProperties ( refs.input, { min, max, step, value } );
return (
<Styles.Numbox ref={refs.numbox}>
<Styles.Button ref={refs.minus} onClick={decrement}>-</Styles.Button>
<Styles.Input ref={refs.input} type="number" onChange={onInputChange} onKeyDown={onInputKeyDown} />
<Styles.Button ref={refs.plus} onClick={increment}>+</Styles.Button>
</Styles.Numbox>
);
}
}
/* EXPORT */
export default Widget.wrap ( Numbox );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment