Skip to content

Commit

Permalink
Allow user to set value for slider.
Browse files Browse the repository at this point in the history
This change allows the value to be set for the slider. The implemented
behavior here is similar to the input/textarea value field.

The value will update the value of the slider only if the value being
passed in changes.

This is for the following use cases:

1. Setting an initial value
2. Updating the value programmatically. For example if you wanted a
   textbox to control the slider value
3. Only updating the value if it changes, allows the slider to move
   smoothly (same issue as the text inputs)
  • Loading branch information
richard-to committed May 25, 2024
1 parent e598a7d commit 3ec45e0
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 19 deletions.
27 changes: 21 additions & 6 deletions demo/slider.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,29 @@

@me.stateclass
class State:
value: float = 0
initial_input_value: str = "50.0"
initial_slider_value: float = 50.0
slider_value: float = 50.0


@me.page(path="/slider")
def app():
state = me.state(State)
with me.box(style=me.Style(display="flex", flex_direction="column")):
me.input(
label="Slider value", value=state.initial_input_value, on_input=on_input
)
me.slider(on_value_change=on_value_change, value=state.initial_slider_value)
me.text(text=f"Value: {me.state(State).slider_value}")


def on_value_change(event: me.SliderValueChangeEvent):
me.state(State).value = event.value
state = me.state(State)
state.slider_value = event.value
state.initial_input_value = str(state.slider_value)


@me.page(path="/slider")
def app():
me.slider(on_value_change=on_value_change)
me.text(text=f"Value: {me.state(State).value}")
def on_input(event: me.InputEvent):
state = me.state(State)
state.initial_slider_value = float(event.value)
state.slider_value = state.initial_slider_value
17 changes: 15 additions & 2 deletions mesop/components/slider/e2e/slider_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,27 @@

@me.stateclass
class State:
value: float = 0
initial_value: float = 50.0
value: float = 50.0


def on_value_change(event: me.SliderValueChangeEvent):
me.state(State).value = event.value


def on_input(event: me.InputEvent):
state = me.state(State)
state.initial_value = float(event.value)
state.value = state.initial_value


@me.page(path="/components/slider/e2e/slider_app")
def app():
me.slider(on_value_change=on_value_change, style=me.Style(width="100%"))
state = me.state(State)
me.input(label="Slider value", on_input=on_input)
me.slider(
on_value_change=on_value_change,
value=state.initial_value,
style=me.Style(width="100%"),
)
me.text(text=f"Value: {me.state(State).value}")
36 changes: 28 additions & 8 deletions mesop/components/slider/e2e/slider_test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
import {test} from '@playwright/test';
import {test, expect} from '@playwright/test';

test('test', async ({page}) => {
test('test slider behavior', async ({page}) => {
await page.goto('/components/slider/e2e/slider_app');
await page.getByRole('slider').fill('57');
await page.getByRole('slider').click();
// TODO: fix the assertion (it's now flaky / off by 1)
// expect(await page.getByText('Value: 57').textContent()).toContain(
// 'Value: 57',
// );

// Make sure the initial slider value is set to 50.
const slider = await page.getByRole('slider');
expect(await slider.evaluate((el) => el.value)).toBe('50');

// Emulate moving the slider.
await slider.evaluate((el, value) => {
el.value = value;
el.dispatchEvent(new Event('input', {bubbles: true}));
el.dispatchEvent(new Event('change', {bubbles: true}));
}, '20');
// Should update the slider value and the text.
expect(await slider.evaluate((el) => el.value)).toBe('20');
expect(await page.getByText('Value: 20').textContent()).toContain(
'Value: 20',
);

// Update the slider value via the textbox.
await page.getByLabel('Slider value').fill('75');
// Need to wait for the input state to be saved first.
await page.waitForTimeout(2000);
// Should update the slider value and the text.
expect(await slider.evaluate((el) => el.value)).toBe('75');
expect(await page.getByText('Value: 75').textContent()).toContain(
'Value: 75',
);
});
6 changes: 5 additions & 1 deletion mesop/components/slider/slider.ng.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@
[step]="config().getStep()"
[style]="getStyle()"
>
<input matSliderThumb (valueChange)="onValueChange($event)" />
<input
matSliderThumb
(valueChange)="onValueChange($event)"
[(ngModel)]="value"
/>
</mat-slider>
1 change: 1 addition & 0 deletions mesop/components/slider/slider.proto
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ message SliderType {
optional bool disable_ripple = 6;
optional double max = 7;
optional double step = 8;
optional float value = 10;

optional string on_value_change_handler_id = 9;
}
3 changes: 3 additions & 0 deletions mesop/components/slider/slider.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class SliderValueChangeEvent(MesopEvent):
def slider(
*,
on_value_change: Callable[[SliderValueChangeEvent], Any] | None = None,
value: float | None = None,
min: float = 0,
max: float = 100,
step: float = 1,
Expand All @@ -53,6 +54,7 @@ def slider(
Args:
on_value_change: An event will be dispatched each time the slider changes its value.
value: Initial value. If updated, the slider will be updated with a new initial value.
min: The minimum value that the slider can have.
max: The maximum value that the slider can have.
step: The values at which the thumb will snap.
Expand All @@ -72,6 +74,7 @@ def slider(
disabled=disabled,
discrete=discrete,
show_tick_marks=show_tick_marks,
value=value if value is not None else min,
min=min,
color=color,
disable_ripple=disable_ripple,
Expand Down
26 changes: 24 additions & 2 deletions mesop/components/slider/slider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {MatSliderModule} from '@angular/material/slider';
import {FormsModule} from '@angular/forms';
import {Component, Input} from '@angular/core';
import {
UserEvent,
Expand All @@ -15,14 +16,19 @@ import {formatStyle} from '../../web/src/utils/styles';
@Component({
templateUrl: 'slider.ng.html',
standalone: true,
imports: [MatSliderModule],
imports: [MatSliderModule, FormsModule],
})
export class SliderComponent {
@Input({required: true}) type!: Type;
@Input() key!: Key;
@Input() style!: Style;

// Need to make this value accessible by ngModel.
value?: number = 0;

private _config!: SliderType;
private changeSubject = new Subject<number>();
private cachedSliderInitialValue?: number = 0;

constructor(private readonly channel: Channel) {
this.changeSubject
Expand All @@ -34,23 +40,39 @@ export class SliderComponent {
this._config = SliderType.deserializeBinary(
this.type.getValue() as unknown as Uint8Array,
);
this.updateValue();
}

config(): SliderType {
return this._config;
}

updateValue(): void {
// Cache initial slider value since we only want to update the slider position
// if the value has changed on the server side. This allows the user to change
// the slider position, but still allows the server to set a default value or update
// the value programmatically.
//
// This emulates behavior similar to the value attribute on the input and
// textarea components.
if (this.cachedSliderInitialValue !== this._config.getValue()) {
this.cachedSliderInitialValue = this._config.getValue();
this.value = this._config.getValue();
}
}

getColor(): 'primary' | 'accent' | 'warn' {
return this.config().getColor() as 'primary' | 'accent' | 'warn';
}

onValueChange(value: number): void {
this.changeSubject.next(value);
this.value = value;
}

onValueChangeDebounced(value: number) {
const userEvent = new UserEvent();

this.value = value;
userEvent.setHandlerId(this.config().getOnValueChangeHandlerId()!);
userEvent.setDoubleValue(value);
userEvent.setKey(this.key);
Expand Down

0 comments on commit 3ec45e0

Please sign in to comment.